From a329baaa5495c51aafe2f2214f15b5992e17c4a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 23 Dec 2025 03:17:16 -0800 Subject: [PATCH 001/243] =?UTF-8?q?=F0=9F=91=B7=20Update=20secrets=20check?= =?UTF-8?q?=20(#14592)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pre-commit.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index e628ce541..b397912e6 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -7,7 +7,8 @@ on: - synchronize env: - IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + # Forks and Dependabot don't have access to secrets + HAS_SECRETS: ${{ secrets.PRE_COMMIT != '' }} jobs: pre-commit: @@ -19,7 +20,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v5 name: Checkout PR for own repo - if: env.IS_FORK == 'false' + if: env.HAS_SECRETS == 'true' with: # To be able to commit it needs to fetch the head of the branch, not the # merge commit @@ -31,7 +32,7 @@ jobs: # pre-commit lite ci needs the default checkout configs to work - uses: actions/checkout@v5 name: Checkout PR for fork - if: env.IS_FORK == 'true' + if: env.HAS_SECRETS == 'false' with: # To be able to commit it needs the head branch of the PR, the remote one ref: ${{ github.event.pull_request.head.sha }} @@ -56,7 +57,7 @@ jobs: run: uvx prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure continue-on-error: true - name: Commit and push changes - if: env.IS_FORK == 'false' + if: env.HAS_SECRETS == 'true' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -68,7 +69,7 @@ jobs: git push fi - uses: pre-commit-ci/lite-action@v1.1.0 - if: env.IS_FORK == 'true' + if: env.HAS_SECRETS == 'false' with: msg: 🎨 Auto format - name: Error out on pre-commit errors From e55f223b4695b80ad164a150489b3c3a26a35e4d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 23 Dec 2025 11:17:37 +0000 Subject: [PATCH 002/243] =?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 a08ac1cd8..09e6341aa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo). * 👷 Run CodSpeed tests in parallel to other tests to speed up CI. PR [#14586](https://github.com/fastapi/fastapi/pull/14586) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update scripts and pre-commit to autofix files. PR [#14585](https://github.com/fastapi/fastapi/pull/14585) by [@tiangolo](https://github.com/tiangolo). From 7203e860b3adacc09dfd2171ea1b3ed1a9589fa9 Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Wed, 24 Dec 2025 11:28:19 +0100 Subject: [PATCH 003/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20de=20(update-outdated)=20(#14581)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sync with #14575 (Drop support for Pydantic v1) * Add a word and fix a typo Found while syncing. --- .../path-operation-advanced-configuration.md | 36 +----------------- docs/de/docs/advanced/settings.md | 38 ------------------- ...migrate-from-pydantic-v1-to-pydantic-v2.md | 14 ++++--- .../docs/how-to/separate-openapi-schemas.md | 4 +- docs/de/docs/tutorial/body-updates.md | 16 -------- docs/de/docs/tutorial/body.md | 8 ---- docs/de/docs/tutorial/extra-models.md | 29 +++++--------- .../tutorial/query-params-str-validations.md | 14 ------- docs/de/docs/tutorial/response-model.md | 14 ------- docs/de/docs/tutorial/schema-extra-example.md | 24 +----------- 10 files changed, 21 insertions(+), 176 deletions(-) diff --git a/docs/de/docs/advanced/path-operation-advanced-configuration.md b/docs/de/docs/advanced/path-operation-advanced-configuration.md index e72252600..c7ac1cf61 100644 --- a/docs/de/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/de/docs/advanced/path-operation-advanced-configuration.md @@ -48,7 +48,7 @@ Sie können die verwendeten Zeilen aus dem Docstring einer *Pfadoperation-Funkti Das Hinzufügen eines `\f` (ein maskiertes „Form Feed“-Zeichen) führt dazu, dass **FastAPI** die für OpenAPI verwendete Ausgabe an dieser Stelle abschneidet. -Sie wird nicht in der Dokumentation angezeigt, aber andere Tools (z. B. Sphinx) können den Rest verwenden. +Sie wird nicht in der Dokumentation angezeigt, aber andere Tools (wie z. B. Sphinx) können den Rest verwenden. {* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} @@ -153,48 +153,16 @@ Und Sie könnten dies auch tun, wenn der Datentyp im Request nicht JSON ist. In der folgenden Anwendung verwenden wir beispielsweise weder die integrierte Funktionalität von FastAPI zum Extrahieren des JSON-Schemas aus Pydantic-Modellen noch die automatische Validierung für JSON. Tatsächlich deklarieren wir den Request-Content-Type als YAML und nicht als JSON: -//// tab | Pydantic v2 - {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *} - -//// - -/// info | Info - -In Pydantic Version 1 hieß die Methode zum Abrufen des JSON-Schemas für ein Modell `Item.schema()`, in Pydantic Version 2 heißt die Methode `Item.model_json_schema()`. - -/// - Obwohl wir nicht die standardmäßig integrierte Funktionalität verwenden, verwenden wir dennoch ein Pydantic-Modell, um das JSON-Schema für die Daten, die wir in YAML empfangen möchten, manuell zu generieren. -Dann verwenden wir den Request direkt und extrahieren den Body als `bytes`. Das bedeutet, dass FastAPI nicht einmal versucht, den Request-Payload als JSON zu parsen. +Dann verwenden wir den Request direkt und extrahieren den Body als `bytes`. Das bedeutet, dass FastAPI nicht einmal versucht, die Request-Payload als JSON zu parsen. Und dann parsen wir in unserem Code diesen YAML-Inhalt direkt und verwenden dann wieder dasselbe Pydantic-Modell, um den YAML-Inhalt zu validieren: -//// tab | Pydantic v2 - {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *} - -//// - -/// info | Info - -In Pydantic Version 1 war die Methode zum Parsen und Validieren eines Objekts `Item.parse_obj()`, in Pydantic Version 2 heißt die Methode `Item.model_validate()`. - -/// - /// tip | Tipp Hier verwenden wir dasselbe Pydantic-Modell wieder. diff --git a/docs/de/docs/advanced/settings.md b/docs/de/docs/advanced/settings.md index ebacf76f4..ea4540e10 100644 --- a/docs/de/docs/advanced/settings.md +++ b/docs/de/docs/advanced/settings.md @@ -60,24 +60,8 @@ Auf die gleiche Weise wie bei Pydantic-Modellen deklarieren Sie Klassenattribute Sie können dieselben Validierungs-Funktionen und -Tools verwenden, die Sie für Pydantic-Modelle verwenden, z. B. verschiedene Datentypen und zusätzliche Validierungen mit `Field()`. -//// tab | Pydantic v2 - {* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} -//// - -//// tab | Pydantic v1 - -/// info | Info - -In Pydantic v1 würden Sie `BaseSettings` direkt von `pydantic` statt von `pydantic_settings` importieren. - -/// - -{* ../../docs_src/settings/tutorial001_pv1_py39.py hl[2,5:8,11] *} - -//// - /// tip | Tipp Für ein schnelles Copy-and-paste verwenden Sie nicht dieses Beispiel, sondern das letzte unten. @@ -215,8 +199,6 @@ APP_NAME="ChimichangApp" Und dann aktualisieren Sie Ihre `config.py` mit: -//// tab | Pydantic v2 - {* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip | Tipp @@ -225,26 +207,6 @@ Das Attribut `model_config` wird nur für die Pydantic-Konfiguration verwendet. /// -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *} - -/// tip | Tipp - -Die Klasse `Config` wird nur für die Pydantic-Konfiguration verwendet. Weitere Informationen finden Sie unter Pydantic Model Config. - -/// - -//// - -/// info | Info - -In Pydantic Version 1 erfolgte die Konfiguration in einer internen Klasse `Config`, in Pydantic Version 2 erfolgt sie in einem Attribut `model_config`. Dieses Attribut akzeptiert ein `dict`. Um automatische Codevervollständigung und Inline-Fehlerberichte zu erhalten, können Sie `SettingsConfigDict` importieren und verwenden, um dieses `dict` zu definieren. - -/// - Hier definieren wir die Konfiguration `env_file` innerhalb Ihrer Pydantic-`Settings`-Klasse und setzen den Wert auf den Dateinamen mit der dotenv-Datei, die wir verwenden möchten. ### Die `Settings` nur einmal laden mittels `lru_cache` { #creating-the-settings-only-once-with-lru-cache } diff --git a/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md index 7f60492ee..a8eff3b2b 100644 --- a/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md +++ b/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -2,21 +2,23 @@ Wenn Sie eine ältere FastAPI-App haben, nutzen Sie möglicherweise Pydantic Version 1. -FastAPI unterstützt seit Version 0.100.0 sowohl Pydantic v1 als auch v2. +FastAPI Version 0.100.0 unterstützte sowohl Pydantic v1 als auch v2. Es verwendete, was auch immer Sie installiert hatten. -Wenn Sie Pydantic v2 installiert hatten, wurde dieses verwendet. Wenn stattdessen Pydantic v1 installiert war, wurde jenes verwendet. +FastAPI Version 0.119.0 führte eine teilweise Unterstützung für Pydantic v1 innerhalb von Pydantic v2 (als `pydantic.v1`) ein, um die Migration zu v2 zu erleichtern. -Pydantic v1 ist jetzt deprecatet und die Unterstützung dafür wird in den nächsten Versionen von FastAPI entfernt, Sie sollten also zu **Pydantic v2 migrieren**. Auf diese Weise erhalten Sie die neuesten Features, Verbesserungen und Fixes. +FastAPI 0.126.0 entfernte die Unterstützung für Pydantic v1, während `pydantic.v1` noch eine Weile unterstützt wurde. /// warning | Achtung -Außerdem hat das Pydantic-Team die Unterstützung für Pydantic v1 in den neuesten Python-Versionen eingestellt, beginnend mit **Python 3.14**. +Das Pydantic-Team hat die Unterstützung für Pydantic v1 in den neuesten Python-Versionen eingestellt, beginnend mit **Python 3.14**. + +Dies schließt `pydantic.v1` ein, das unter Python 3.14 und höher nicht mehr unterstützt wird. Wenn Sie die neuesten Features von Python nutzen möchten, müssen Sie sicherstellen, dass Sie Pydantic v2 verwenden. /// -Wenn Sie eine ältere FastAPI-App mit Pydantic v1 haben, zeige ich Ihnen hier, wie Sie sie zu Pydantic v2 migrieren, und die **neuen Features in FastAPI 0.119.0**, die Ihnen bei einer schrittweisen Migration helfen. +Wenn Sie eine ältere FastAPI-App mit Pydantic v1 haben, zeige ich Ihnen hier, wie Sie sie zu Pydantic v2 migrieren, und die **Features in FastAPI 0.119.0**, die Ihnen bei einer schrittweisen Migration helfen. ## Offizieller Leitfaden { #official-guide } @@ -44,7 +46,7 @@ Danach können Sie die Tests ausführen und prüfen, ob alles funktioniert. Fall ## Pydantic v1 in v2 { #pydantic-v1-in-v2 } -Pydantic v2 enthält alles aus Pydantic v1 als Untermodul `pydantic.v1`. +Pydantic v2 enthält alles aus Pydantic v1 als Untermodul `pydantic.v1`. Dies wird aber in Versionen oberhalb von Python 3.13 nicht mehr unterstützt. Das bedeutet, Sie können die neueste Version von Pydantic v2 installieren und die alten Pydantic‑v1‑Komponenten aus diesem Untermodul importieren und verwenden, als hätten Sie das alte Pydantic v1 installiert. diff --git a/docs/de/docs/how-to/separate-openapi-schemas.md b/docs/de/docs/how-to/separate-openapi-schemas.md index 31653590b..16f9c8a14 100644 --- a/docs/de/docs/how-to/separate-openapi-schemas.md +++ b/docs/de/docs/how-to/separate-openapi-schemas.md @@ -1,6 +1,6 @@ # Separate OpenAPI-Schemas für Eingabe und Ausgabe oder nicht { #separate-openapi-schemas-for-input-and-output-or-not } -Bei Verwendung von **Pydantic v2** ist die generierte OpenAPI etwas genauer und **korrekter** als zuvor. 😎 +Seit der Veröffentlichung von **Pydantic v2** ist die generierte OpenAPI etwas genauer und **korrekter** als zuvor. 😎 Tatsächlich gibt es in einigen Fällen sogar **zwei JSON-Schemas** in OpenAPI für dasselbe Pydantic-Modell, für Eingabe und Ausgabe, je nachdem, ob sie **Defaultwerte** haben. @@ -100,5 +100,3 @@ Und jetzt wird es ein einziges Schema für die Eingabe und Ausgabe des Modells g
- -Dies ist das gleiche Verhalten wie in Pydantic v1. 🤓 diff --git a/docs/de/docs/tutorial/body-updates.md b/docs/de/docs/tutorial/body-updates.md index aa62199fe..d260998e9 100644 --- a/docs/de/docs/tutorial/body-updates.md +++ b/docs/de/docs/tutorial/body-updates.md @@ -50,14 +50,6 @@ Wenn Sie Teil-Aktualisierungen entgegennehmen, ist der `exclude_unset`-Parameter Wie in `item.model_dump(exclude_unset=True)`. -/// info | Info - -In Pydantic v1 hieß diese Methode `.dict()`, in Pydantic v2 wurde sie deprecatet (aber immer noch unterstützt) und in `.model_dump()` umbenannt. - -Die Beispiele hier verwenden `.dict()` für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen `.model_dump()` verwenden, wenn Sie Pydantic v2 verwenden können. - -/// - Das wird ein `dict` erstellen, mit nur den Daten, die gesetzt wurden, als das `item`-Modell erstellt wurde, Defaultwerte ausgeschlossen. Sie können das verwenden, um ein `dict` zu erstellen, das nur die (im Request) gesendeten Daten enthält, ohne Defaultwerte: @@ -68,14 +60,6 @@ Sie können das verwenden, um ein `dict` zu erstellen, das nur die (im deprecatet (aber immer noch unterstützt) und in `.model_copy()` umbenannt. - -Die Beispiele hier verwenden `.copy()` für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen `.model_copy()` verwenden, wenn Sie Pydantic v2 verwenden können. - -/// - Wie in `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} diff --git a/docs/de/docs/tutorial/body.md b/docs/de/docs/tutorial/body.md index 0ad95b038..cdf3122f2 100644 --- a/docs/de/docs/tutorial/body.md +++ b/docs/de/docs/tutorial/body.md @@ -127,14 +127,6 @@ Innerhalb der Funktion können Sie alle Attribute des Modellobjekts direkt verwe {* ../../docs_src/body/tutorial002_py310.py *} -/// info | Info - -In Pydantic v1 hieß die Methode `.dict()`, sie wurde in Pydantic v2 deprecatet (aber weiterhin unterstützt) und in `.model_dump()` umbenannt. - -Die Beispiele hier verwenden `.dict()` zur Kompatibilität mit Pydantic v1, aber Sie sollten stattdessen `.model_dump()` verwenden, wenn Sie Pydantic v2 nutzen können. - -/// - ## Requestbody- + Pfad-Parameter { #request-body-path-parameters } Sie können Pfad-Parameter und den Requestbody gleichzeitig deklarieren. diff --git a/docs/de/docs/tutorial/extra-models.md b/docs/de/docs/tutorial/extra-models.md index 967e8535b..889fdb9a3 100644 --- a/docs/de/docs/tutorial/extra-models.md +++ b/docs/de/docs/tutorial/extra-models.md @@ -22,21 +22,13 @@ Hier ist eine allgemeine Idee, wie die Modelle mit ihren Passwortfeldern aussehe {* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} -/// info | Info +### Über `**user_in.model_dump()` { #about-user-in-model-dump } -In Pydantic v1 hieß die Methode `.dict()`, in Pydantic v2 wurde sie deprecatet (aber weiterhin unterstützt) und in `.model_dump()` umbenannt. - -Die Beispiele hier verwenden `.dict()` für die Kompatibilität mit Pydantic v1, aber Sie sollten `.model_dump()` verwenden, wenn Sie Pydantic v2 verwenden können. - -/// - -### Über `**user_in.dict()` { #about-user-in-dict } - -#### Die `.dict()`-Methode von Pydantic { #pydantics-dict } +#### Pydantics `.model_dump()` { #pydantics-model-dump } `user_in` ist ein Pydantic-Modell der Klasse `UserIn`. -Pydantic-Modelle haben eine `.dict()`-Methode, die ein `dict` mit den Daten des Modells zurückgibt. +Pydantic-Modelle haben eine `.model_dump()`-Methode, die ein `dict` mit den Daten des Modells zurückgibt. Wenn wir also ein Pydantic-Objekt `user_in` erstellen, etwa so: @@ -47,7 +39,7 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com und dann aufrufen: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() ``` haben wir jetzt ein `dict` mit den Daten in der Variablen `user_dict` (es ist ein `dict` statt eines Pydantic-Modellobjekts). @@ -103,20 +95,20 @@ UserInDB( #### Ein Pydantic-Modell aus dem Inhalt eines anderen { #a-pydantic-model-from-the-contents-of-another } -Da wir im obigen Beispiel `user_dict` von `user_in.dict()` bekommen haben, wäre dieser Code: +Da wir im obigen Beispiel `user_dict` von `user_in.model_dump()` bekommen haben, wäre dieser Code: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() UserInDB(**user_dict) ``` gleichwertig zu: ```Python -UserInDB(**user_in.dict()) +UserInDB(**user_in.model_dump()) ``` -... weil `user_in.dict()` ein `dict` ist, und dann lassen wir Python es „entpacken“, indem wir es an `UserInDB` mit vorangestelltem `**` übergeben. +... weil `user_in.model_dump()` ein `dict` ist, und dann lassen wir Python es „entpacken“, indem wir es an `UserInDB` mit vorangestelltem `**` übergeben. Auf diese Weise erhalten wir ein Pydantic-Modell aus den Daten eines anderen Pydantic-Modells. @@ -125,7 +117,7 @@ Auf diese Weise erhalten wir ein Pydantic-Modell aus den Daten eines anderen Pyd Und dann fügen wir das zusätzliche Schlüsselwort-Argument `hashed_password=hashed_password` hinzu, wie in: ```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) ``` ... was so ist wie: @@ -180,7 +172,6 @@ Wenn Sie eine deprecatet (aber immer noch unterstützt) und in `.model_dump()` umbenannt. - -Die Beispiele hier verwenden `.dict()` für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen `.model_dump()` verwenden, wenn Sie Pydantic v2 verwenden können. - -/// - -/// info | Info - -FastAPI verwendet `.dict()` von Pydantic Modellen, mit dessen `exclude_unset`-Parameter, um das zu erreichen. - -/// - -/// info | Info - Sie können auch: * `response_model_exclude_defaults=True` diff --git a/docs/de/docs/tutorial/schema-extra-example.md b/docs/de/docs/tutorial/schema-extra-example.md index e2ffed292..07fe8c5d9 100644 --- a/docs/de/docs/tutorial/schema-extra-example.md +++ b/docs/de/docs/tutorial/schema-extra-example.md @@ -8,36 +8,14 @@ Hier sind mehrere Möglichkeiten, das zu tun. Sie können `examples` („Beispiele“) für ein Pydantic-Modell deklarieren, welche dem generierten JSON-Schema hinzugefügt werden. -//// tab | Pydantic v2 - {* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *} - -//// - Diese zusätzlichen Informationen werden unverändert zum für dieses Modell ausgegebenen **JSON-Schema** hinzugefügt und in der API-Dokumentation verwendet. -//// tab | Pydantic v2 - -In Pydantic Version 2 würden Sie das Attribut `model_config` verwenden, das ein `dict` akzeptiert, wie beschrieben in Pydantic-Dokumentation: Configuration. +Sie können das Attribut `model_config` verwenden, das ein `dict` akzeptiert, wie beschrieben in Pydantic-Dokumentation: Configuration. Sie können `json_schema_extra` setzen, mit einem `dict`, das alle zusätzlichen Daten enthält, die im generierten JSON-Schema angezeigt werden sollen, einschließlich `examples`. -//// - -//// tab | Pydantic v1 - -In Pydantic Version 1 würden Sie eine interne Klasse `Config` und `schema_extra` verwenden, wie beschrieben in Pydantic-Dokumentation: Schema customization. - -Sie können `schema_extra` setzen, mit einem `dict`, das alle zusätzlichen Daten enthält, die im generierten JSON-Schema angezeigt werden sollen, einschließlich `examples`. - -//// - /// tip | Tipp Mit derselben Technik können Sie das JSON-Schema erweitern und Ihre eigenen benutzerdefinierten Zusatzinformationen hinzufügen. From 2b212ddd7604891d42e2ffbafe764c334230bdb7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 10:28:45 +0000 Subject: [PATCH 004/243] =?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 09e6341aa..30d5c4403 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🌐 Update translations for de (update-outdated). PR [#14581](https://github.com/fastapi/fastapi/pull/14581) by [@nilslindemann](https://github.com/nilslindemann). + ### Internal * 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo). From c264467efec71548d4feeecbfb29da78cceb1be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 25 Dec 2025 03:01:37 -0800 Subject: [PATCH 005/243] =?UTF-8?q?=F0=9F=93=9D=20Add=20documentary=20to?= =?UTF-8?q?=20website=20(#14600)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++++ docs/en/docs/img/fastapi-documentary.jpg | Bin 0 -> 191709 bytes docs/en/docs/index.md | 6 ++++++ 3 files changed, 12 insertions(+) create mode 100644 docs/en/docs/img/fastapi-documentary.jpg diff --git a/README.md b/README.md index a42cedae6..1057b8694 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,12 @@ The key features are: --- +## FastAPI mini documentary + +There's a FastAPI mini documentary released at the end of 2025, you can watch it online: + +FastAPI Mini Documentary + ## **Typer**, the FastAPI of CLIs diff --git a/docs/en/docs/img/fastapi-documentary.jpg b/docs/en/docs/img/fastapi-documentary.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3ddbfdb389794fd3b2d87e086bb6e73145ed3d1e GIT binary patch literal 191709 zcmeFad0bQ1);E3%7E5#tyQd=(AsJhtMsag1J;O& z5Ya}(2ni4^t=3Srt*sL{v{WX&mJTXfXKU+to!_=l%Wh-jBUECOJ80 zpS{;!>$|^eIGbmupPi+;$m0^?C?1cZcf+{3d$@P( z=-#tyx6X7QpMHJ&`1J1WJ7Ca2U;kJ8_wFqkDt_7hR*?!8?#f9HV<;3%+_?|o`Pu{b`R6ppafJgqL*#CHZCuf1M zgNv(MM>wIf3&rO-Iq{vH1OjJgIC}y7PC0uDy84C22)ijKckrJ@hw0aDb?HB9=K(KO z-Jb)(Gd@`C>eju7cTb;yz=0xhkYq^2(8ys?v7=>iW8xF!>a?-rULXHP`joe)PMfaL zX3n0IHTT1L^Rt&MU6!-_;}wSd0^_>E^&37h6_;47rDf%|ZC~uNSAMyB&)%;N9y(lA zedOq|-L?y_wN7m!9yGuk8)y$1^+SwYd0=W z7#H8!*~z&Bj*G|t5MEB6&H}$sVb>UChsm?L`G@IU=uvC8?mXbyKV0>vSH=f*Zruk+ z&ISC1Ln9;m-wkZ>|EG~T2G%mJCd%E32b1UINsXi)JT-`=fjTP8BOoDH{4i|}rIgYC z)i0GkPeYj&hLl~Ade;4yk5wrXIoDFc7>g}**+}_notvomzx$;K^Psw=R?|;J{HXu( zvG!OKEAQR;Txp%O^We3)>i_c5x=oDs2%xF6XBT;h|DK!iiHxF^|J|>_eRGVF{SGA; z>-xUc-@Ej`e6;j8**cC4K zc&P&~b>O8Aywri0I`C2lUh2S09eAk&FLmIh4!qQXmpbrL2VUyHOC5Nr121*pr4GE* zftNb)QU_k@z)Kx?sRJ){;H3_{)Pa{e@KOg}>cC4Kc&P&~b>ROQ9VqEgpR3@>=+-au zG+kH2o29Et^%Lacxuysczgd}RkiQyIY!Ff8wf>!KW#v8*Ph@Rc;L$5}tJ%4Dz4ne| z#t0*2RNs3>?Rr?Z`^lD5>-JBjpL}=yseNO@jWyLPbxq=*J6HDVTp7|!_S)ooC6n)& zg>@;X7nH;Qd3N=e{W9IwNxecipPm8rL$T)uUp zqI<{O;w`^#NguxJTKu3e!=l0k8aT?EWmG@jb!`lMBfmeYe=x^=wPDfv1p7mUlcvmb zb#+1CY8c*xk9q2%J33&bG{V77Am$+`Cl-I`$FbF1%^OvwgW zSep)2ww}5iobKeY5YIM9g1B82B(T zGw2!gXFYn!MrukXmCb|`9tnC0@PvDA$;B&|RoA67iJHU{21OVa*2Nu2IY)kpa2wR4 zr)=aT3-im|9;a57oZ5WwyvEVoYnl>G@i~zEj*(w!9xVDw3oRwydQ^Ah-8SQ#yXE~K zSC##I8#Sf-s8(n3%-j2Nwhu#4p+VARJ-U*8g*QJcvfxae|iOW-hGKk{>Fnk z`=f<(C=l+5lH2t4kJi-dddgnw@MKGUZohkrSydeJ&6!Z7#D zqTl1-1;()1p8bppidgg3?@uKZDCC&WZ#|=Y9N(+POnGdJ02$!qeQQ~mvHtz&F_+-F zExPEDC(g%p*nR04^$)X9nY#bp<7d>vJ2U#8+478<2qP1FM_SB$165*W)K*n~tZL+> z`(~HRt+mL*dju-RgnqGcor#%usI6Oa84U+i&Dic4wn3e(U<*os{}TG_w`C8|;%4;y z?)a*be?0&B{u5cR=pz?JIQBags<#+pSfUzfkd;T_! zqFA_PL&^IMFjIJ!hYK@S;SV}6WPG1-zuyK#m!JO_85(zH)&m1fANgU~0x(FhG_YII zTdj`&%L`ug!=ELM3#UBRx~)+@So2rQZI~I`edd93_nEt-k1!4~H$9-GC8(DKHNT5; z@dvqz_s;^|71nXnGwGv+i=JdtQ|arS-?|M7iIa~#-+LwQL*!@$MEQ+}^?(0%ONCQUS;AMQ`8N?&=}8Y21Zuot))nD# z9V+0$vpx2Xv`X&c5#CMa(AS{IaSb;R;8K3R`OuIwTaWVUytM=no>)iDsOP45mmPVKGkGixBYp!@5T7c$0W;=X60E5E8qs zl^tw`2QS@Q7H6ZlKh6SR;uEb~Uf3dpa^;dMQUM4{bz@*9e7&*iN?gN}CYS_EKY3C= zIp{VlnXTW;u3KJNZSQnQV{Kk7>gXyPOwU5ZpJyiDXoYXF(S4Kpn{T6PokV5tO6o)A zgSbT~^5`p9JD9=XZ1UVWUOm-CoTa%&(v%U`!-#&FIQwK}0^5k(>NH%fVU}QSDQ^G1 zsmt0Tkz+w+8hAzx{w=RmI2+WU{ppVHA*q=ZXVfWUk6oCReBj4W5X9M~4Tise(@DdR z693$*-a!3!%kLRg6#9%hipn*zbCgukB0ZVT|M&A z1@-MX4R6czB~Rxac}D%+Fycgq$A7JP`hb31XQHP6boRyV=z89pH}X>MqWecnuK&5{ zg!eP*=GxSno@U|vF3%|D>zK8j9Sx1+6^$NA?xwsD+p0S&sS_>T>*wghf}W*Lj(%_* z4w8%bBVTlOzJYol?sY5m(FLaU0_gp9>^&aEp^#3ANFR@u`0L$?bd*Fo{{0?Y*mD19 z*#)IZ7=Cp5)A<8IH01XLl*U|5jc+f~Wo(qRGXO>Tn#4|>fNOsTNs9me@uExli_Z9O zmWA}XH;?(eX4fGYCth@Qri9n;r*j}j(&C5{OjpxBDQR>3Eyo3q>o4m1bF-u5I_|=!qQ?LhQhPhwGcT}JN%N+{9{`^q%A0BN{ zA*KbdKurxbh8sYOqNt|j);ce8%r-aE#6>oA2fqj*urJhmZ^;E*W6X znR&*aABsAzeb9xqg+y-DFbfl2Y(*msU|!}H^TOK@Gff~9?fBYnx^B_;#K2V3@^w91 z?58hlQZT23P<0vmeGB@I_APFlrUPwkZ=@+T!4#1(aK&$BXmY6W;yZiIE>FSoi4oyd zsLz%f+MS4Zf-Pys8oaxR~ zt~+n-`WpwJ1Wy;3&ODtrh5&QT`_Q5OC}*NN70;+kO9=peB)Q|aT2Hxqd`}@l-q8z@*Zup~I z`c2&p>g&1a44*+DcE0Ha`9D9MCxbq_E!57IdOmS{9dQhrVy;afm<3k=68)2Ik!eX* zE3HVp_s$|lnI9B`V!@YlWaVm6ESv%Muy`R65HTTG=>S59kk%j23ksn>C_}db$$Ow% z-(xROkT3!meUT@u1boY{aaX}0$ykY=GJS)U1mL(Qk&WZtWO*bg9lV>UI2rPSW^6JM z2Ilz$??@ytg4~RV3y0k17l$AdOcMYQ8EIl))-b|{1xt_W5jN5>8HtWby7Nt~KOA3l z;v1M5>m%sMqbeNY0vhLzIb3r6%QLrc!jyvbzzX3whjePhUodfCZ1+KFpu0@(2mTbi z=u%U^M`zRa;_`I*2J=mM@3prc+*=N|JcvO0YpLWLyKs(HZVM0qk}q_f7Pu&*zUH#bQ4I2ICx!wEpxU=p z_)9JRB7e1FV#(~6ro%1r7M5!IkGy%hbYm~*6CSt!s`a4KBD!z6eGQ}|9&w249lydyYj!)6QHhr$!>ZtK@9)Q*5XD6u<^5pkYM?LK>>L zf5p)B6_@$j{}u!4k7gnmeF_B5u(ffft(nwk88MGLK2W=BpPYQWZCZKL22c8}KR*9G zVduy_->n;wulG2;<(IdPuUhe>>and+;WT&P{T{bUG8WBR6qH3%zvAI?a>nLFsr#*l zM=KwwuD|uin&X=jq^>&`eKj&DV(|Sjzb7V2tI6S2E1%RoP?wv9*HhspDV|60`m#=T zm+TnN-qp*m1|4+tR2H{6==dD-l>Ae_++7UFm3C!%=Az$!Lkv@W>SvaI$;j%@ps71J zkz3U@Nu_y5za?|C1Lh{tF*ikpdUnpp@h4kw`tYN=&LoU|vSsRT^_?Hx6OY{W>7q+N zo`Ff*X-cO)hvV!lC$@Z41KDo%OTv@QAlMOepHcU(k2Q(<7@ko--Fu6u5WQ&nPm755 zq`|_$_kDWe3z+G6P!3DW$L&2WHBPO6idwMc@k5xeg;nEg{4$+ikLb>}wq-JRx$Sm> ziCTU=V$tnupsDnN9ME2(nx!=~uk-150lZJaa72$hps54cRI(|l)_LS)PEsYP<#?^v z5SrKH^sDRzpqmc4Tr7~dx4Q)~kWvd67&(p6{#RJ-Hvz#LnSnRJ0hU}J$C?qMN9?^< zQW+ab-I>ms-2-T6Au&{(hB2qnPNC01nIe8a?j+EB)~+{7K7rGT<>2X0Va%K*m)1Df zIt!~g11{hUxZgQ7i~+=fqr=%9d2r2+pVmCcLYqV=@_Xb}#)`-9i(Mmc5qU(A?)G8_ zHNt^0e1bU(31lI%@C&YwW8p4S+*;PmwW>O)=Ym3xH>DCr<$~UyS_PLN^FuOVV8-7cfKZl*OAFGU z)=RusZ~zP;@qJu}fJ8WCn<(RbFxas|uWdzLt;HZa)~!}@%n&>P&nGP`u0`kxgr0VY zM<@m+4!$Sp7B1l6S)%`8;TlUbvK>D{I?hs2uV9+z`6b63Q+IK=&J2eorqBlC`lW$F z`9G43ynP&m2fbT#$q4Mx2HgBxbV-rZpcBY+!c;)>ychSaBi?WZEFiE<{oPRDU99VW z%kg#ihVL$!&`*G2zu+hYTKFRa2NVCgH@#P$o=i-Y7z3g?AM%n4%(dOW@jF2DLO@FTd^hi~}$Ft%ut6X&0Jxh)Zi zR13r>LSV%sp@1<3?5MA%3&W3b15f1w0+)URvOAph?ALHdfkw3i2iWls2>=I#?TzX| z=%N!6oX`;l=kTL2Xml9=4k6J?`Jkmke69cLTpR+ND{2ws0($?N=a1$aVwovOz!`B5 z+4A6RR)#In3G@unuT?2QvRz=n#M#56$JnDS=MH8=C#Yf*LHKyan6*8KCz!UU1iHHx zS|}uU8(t3MATwSSK!qP)!}WONExalfmBSBog*o?QW|l3*`nB`f+0~8??fhWcmUXE4 zzmLJ?KC}!dL|Oebh{+&dP+ll|eEY5A=uUV(fB+3J3%ntCTQdzgMTL$!U#tw!8<+#a|=eMHjo*o#Xld! z$_nY@hJHxXL}uXwEeihDdFB%D6XcS$2ltjpj0*n6+IjKO~Z$^K-ifjzHY)NVkamU=j$ zfd|g`Ym#Xjq5V!ZJxebZ>Uv0~F_+JvsjFg$p7AsoS`d^-KCzH<;nys>DV<6e!|XT0 zr4wqg&(p{~;UWkjvi}a!@Mgzpp+n#!LT5fyeg)Ugy&9fe5~%BA3Qs=9y^~I;XT!Z3 zxqlN)a07NM_`yk{1TK&+4%3Edcz4KI%?4;x1BS9P5$@;$F~^AY5^67uUKmV+AnQPa z%m)+&)03Luh(diw83@G=R^IXfWSH0@^!74SXzdE8+i_Z+)hwv#3Al|yD%82xUqn&W zU)%`S79ykbRaZeM?P_QW%n0t%CQp}B@2Grcm6|AR zX+up&ty6M9WP^t1A!%3z`3|8;TV~RlT{c#XJ{g`iSn4@s)UQ{kmH%yDh=1m)ZE@vI zdUp#fxm^wEkYK^^T1|6JZ?&zwvO4m&m@w2X}Dn{+g zoN5Ve-keO^UDB~XSrG=|AkD8JZlX#sL0T>79CdY+HvA^cn(E-jwF&SJI*GEW2L+Ro zi2-h@lpc~O{^z zgye`APp(%rq^3jbWWJk36sT*xg1048WQKH!%#Se$QE|SmJKm4vZxX;JX=OsZzygiP zV33Hm!yxKlw&9S`ZX|LP!byyZBOEWm zj*+_v@J=QN+{g*;6t--1*%qB=hN}gU>Mnn!&<9d!>Rd_{+>O661#l-e zS#my^APs~~5O7pJl-44-s=Xow6iSgClDcOuyE^TfVjng7WXBI}rmgSYIXwDgue4Rl zN-C_~bhlOk0sZ^6WP1B4U0}+(sZnF!zA~yNtRUWG&GH64tur}8Sireqw$v?osH9tRU>>e;DuXb}AWx^n zS}{pBk`${LY}L8AFrQ2sS~b}_5G@tH#7cPvG^5Tz^x)s$curzea|*qvu%36f?-5EOy)Ia;Mm0MpXS0z~FfU_QJd>`dVq z%*(Fey8*VsP&nlKSMP3+EiTrt0O$}bHi#@>=4vt|RsfU=U>*z+u)5X2iJ5U~1-~*t z%|ae(7U~JxmG92t!v?GzfE zeXXPs14Y0FuX8Lc{6YXa{%X5@NkuS4yRlY-kRqxAj8)3vT)0|Dt3U&3rCdy47Zy(j z9TPzl%+qoea97YqzRq3M4CfVtCWp%4a<_D_MP({|e5SdBTXusiQtjz$wWMc(hL$Nb z{P1MSL%^55#)edY6c=sjCT*#%Yu+8JWVw|JX-JN2NT%X`&Fo<_6~AZabxaFZ4;j_C zf5o;4n@>rma~Lex2&4Q1Akgk8nH}s5l4VN#{i%&96YadPu}imIYbuMCcAs)XUGaKs zeQ7$iBg$=Erq>nK=knOTVlA@ZpMU^^{sOyw4|$vnleEFWn8Dq(0Rc~{ADm7)k&=^!l3%1O@bwm)hU2t7z8j5R|DdZMFNVdQtyQ5Z$e5j ztjc1BX;86bDQh^jNh|)CQ#>A7^3c?vW+etLW{91f0!WikMk6$ZL(V}=gY9n>ejX7; z+EP$$8ZUsw8z)AD;52A88 zE(RGBdMYlfK=1>6Ymf>gcgoZnp4fvnx_bnOuOv%|_>n23E9>)(bI2;?4MtVVuZRL| z^DQ=rKgOT&1>TeeivLg$QTd3+9*AD80YdEI)E!)bF7}+fI%&sv4J$6Zm<4uhg~eEj z@hTMUfat+?#5$Bs3rj$1*Vl-Q4G9uwK(O8wfu6Tok;Moy0xNAS23LYsI5#q!kSR4o z+yv<@MzQ=)FFhCpuE&$16#$u5HiMv4yT|jmv7GeD(#xITZO#R z&9qMIo^Oaj?;1UH^qIk1CZ0C%S@5R8#r(t)2umdpm&IBOy4Vfjau46W;^q?JW|MHZ zdYMGMjEYpsyove%p|HTZ3bG{njP3?7`L=cq8dL&`2J}b7V=N;V7x7^FZ~#j){E3Bu z{Szj^p$A#XK|KkS7*S*J7YBZEWCC~vsuGz!0%F)3A34XSSPSg3R0RZsuv@?`atWde zsuP1OfF)3gtVGrY-A5D32Or1JV!jLRoR7B52fVSStO#ANgp|_SkZ3Wk367<;qdZEb zT5Y84=q}*eGSk}5hr*RO?;vTmFt3&uQ@ljF6z7nP3&)m&htVJt7^a27Xxq)n%r9hx zQ?oUC3|J8hMbJVVgjX9DW6Ex@l~tgO7-tjD`5#D2N96Z0*qQ(7C1tIHt7C7ilSD0$ z)k$%F@P+bq(ZG_k%nS=SS`8t5+|TTLi@~zPVQjW4nDPdRwTB1ty@Xr$+x8b+OluX8 z(+$=t7)v}ZWKEoSzz&hSEU^N;P;#)+o*=>Z;5PWk_{ofDfFUtGV}#!F*$QDE(qt<# zq5xz-t(uI?gn>hl04xT~mkgs@I+di-qk=GT5riR#^(b)#M?}FnPM=w70LEgNS6&q_ z{ut(Udn$O`_4VKcm6~RivTqokfp6d~#b-l^Le70%wJq?Ps&e$N8h-ck^wP{_Hhs6W z8LA;6wSGDIAdP#4HXv8PJq)EDcbOrNcbs&)Rp$*NWkl&{sg=P9L(5Qysy zK|3SZf)T7dKH0e;!8|FIKBx)&1YIWK#v-lEuj`W3@6Z(#ax92aJwG)QwnAizzCjeus^jfWY0@I<1%L@on&1Jn(02F~ChMWw0* zM$p1h(A0>T;R!x7fY@H)gUKf7uz!$rUmBlfppF-{@F z-qAxzh7gYfi$vx-=2(qH1?~^=?bO=FCI|yTIL8lW?ApI!C{D|WJ1(Q``{gZ^K7KOl zd%PATVy=sU2O(g+f}a_+j7^N!5aR$qV<1|^_=FCR3J2xz_vIoBAw=X7kT1Ry#=?X$ zlrmpX@C^bGsX%hKObwAQn1B}ZEzUOrK`_Xb(q(WVKa=YPzQc@xME?H*6aegE9WN2= z1aI=97h|ptG~?nG?ah|^ZdUNnNO8tnze*~CebMu1b-3Y_K@EHvCjqZGR}UQ!!0=nk z5IzeafK?|wY-O#OL2JgnVi{8~<#2_VUt4gUWqshIgX}^7>_+SZf|k@QsDghF7J|Es z#Di}Te*{KS2Q#G*iyKK#foJ1!R0IJbhyc29X9F99<)tDwq0XgZq~&n5_D)qucNK5)BL0-0c z-9Y&#*=et+gQ=v-pvFX1uwTBL7}yx?2$W-D09+lhtC@mM-6^ke>e3*t-Y6A_O7BC= znCA~5V*e}NRWMxLy$EsxK4OixQEdZ+AlPp~WpyU9GjfKEhCsNAkoe26;1gE@xB^dv zF<8I^!VbvPl5h*lP1r?@Tbro>(f~{>iKcMW!5L#w4a@?M`hq@&V_kQZ2{{jewK5Qt zGrbg4gv4xdDMfJ00DD7A&O@7X(SWf9pz#Hu#b>MP`x;&3v4BH|$xXpqs0C_NuL$j& zt}W|pZ*X`50&5?!c#IrlboymLhp9+ay&K2_^~lJ$R)ho*D1jN`Lxic;59f~D4sS`K zHk{tWH3))6R4*Onq)T=ARn93l%&CiGm0)EnekkWz83@&;_f zf%cJ!u7KQ8_S`E&pC~;H8vZ;<VWzAgiRjqTdqB7}j zM}_kYrD^T9@cIoo$|@MUAyh_pItRXmoPQ2v+5?2eu=1Y81wg|(*%e4s)Me@V%&F)E zrBw18;XOFMvg;k&I9*?lL0Y>#$01Z>X}+$T#0X(!)x1)ltPrIBR!I{D@o1ne`4#a& z0X7*GUtAjqd_)q;NdDmrh#cnNEXSm+HfbCl0jQwD7f}ICn$ofv z7ve>sc7h>-4OyZ^RA4^qM8cs~&X5@6#c(xQ2S76XZjx^C5REyA7&#*#DzBEWPL48V zEBNZ!7U#`l8H7=9rG!a=bwoBlX$!7Z);0%Gx|WL*$M8;#y+7fV*eAcLzk_u*ibau3fl?9a2s#va&6=MHaZlb3^D; zUQ$U@utXx@aQ*JE+Ng+X-O?TcLoIO?NAnPt0mi|CaytrUbfc;OA;h6fJP}q|5Q~5= z&VvX7m?;yn62O`SYX^I2Avi<`CMfJ+TBVn|2%=!Bt(c-yQ+!y|_WcV~wR0Z6=wL)cdHUS2H8ljmZ`YP1;qn zluyjlt^5Jt9VP9*R~o(q5=twU$x||+#3iIAPL^tz;+Em-pQ}Dvx)z}w<{|?Wc!I)7 zRP`FY0T3H)nA1E(RGk#TCTU-$mu-Col~Nhi*il_U$@c|~Enjby+o`J%EFk$Y1bKLN zv9p}`1C(4_riR`%Xp;jd8o0HJFSE%;SBlJUuT)ixt_-Sm+Lv2^%6o z2m(JaMd-EwNXyx!7-A_oVK)N^b>EGk9I08$9E4ka5T zdO*w-&<^dp3tIGYE9AgMBti{O^O>^MwlXUf09|l$s^6FrEw;Nrco&pQFN3>mPVS>^ zB)Kh~Ok|>-PUSiX|M5*hGoF(o8B_#KeEikSTHImP<=7J^1Ns{?8{JYu3MtXPr?m=+B#WPRt}hvo;wJH3 z>aW!FNoAj>XY>^}*3}cxDb{&wbBfZp9}AgyhdQs^#rw^if4S2-<)!I2rQfd!T7B1b z)AS`{zq)VE&<&VB{(gBvhW4>?oq5Wrf>ck{eBGRqAI%-+&&i1z{!4vMpl(Oeo03B& z_Co z+A;r6cLn_!H1O}W&Vtb7s`wP`UbD~>K4>ipr&$SxxnfZlBXSOiKGo1A6;ym>*xmAR zrI4YDYP_ZxoVi)UAA1Er2QbUr0aK8~N)I92y;<;EyST9;K|sQ-xUY5h>h3ctx8Q0H|~ltHa(m57UmDVv6KC(=2{9_%14oeOh*7u zX95gh6^l7T{5S8&aN5U-+xcfkQj7AOrv(!&t?HxIc1TZ`G7!nx;fwCfG(-G?1lT-?A(uKWi-xp!bat&@bK%C{j63=yQ^B$UTRQ}9E&8i}hrlle zy`Q^unZ$ix^t~`)(9+$biz<^X18wEsD+Eg-OHOS2#-!+!T^n{}Q)>2GlE#?mOfFYuC^#Dv#3_YMsycmdqRI7UZk_kSFalpNhPe zbf(Cd8&leof5+@P`&!J!t+!pv%+rP|_}7I+d5zm$`)CKuT&b5jg;fV_uPMs;q<{C} zjRR`^$M2G0iz%U z;y(kkh6M1yUQ9d~j)%ZxuJsMvNE{skQy8Mg;u6OD!BR#})tN#f=m|q``Wk_Ifmf=8 zAQSert0T%3Qrcj%6BicBCfHIi#yN;|i%Cg$7M1a&^q&xZi4&iOqV%4YvbD()M_s^S znxPSX%Q$8UJu*8>U>$LI7{ps4JN1n1ut4%4gw^n*95g&(G%>p z!~5axBWMNAX#cH>(EMZ=VMhbzzpI4q#IP1l?6LLdM96t9Yu$LO&j;I?H}Vl>JyU(s zo=_1q$MA_C229phQawSGE#u)v%E1+`2;4lies7N`{PkGPy(A7R5jR-L;?x59Tw|@@ z#Tbb}c&ET%BB3gRhFU;AJCGLu8lQj|5f52sS`8m4>uNjfe&aY~CpyO9P8MKRFypq# zpmGB4fpnMXVI5)`))intih(LzP!Qrd%!4)%Ntr|)9)t^FfW<&u1d*^}X7mk;Q7+LJ zr2SDTSlXp5P&erNpz6IDf~mh|uFNk_7%ATxeeL^AJGV{UB|Y}`p0LnYtBVJk-V+~K zkWBR|uht$IlR5X7EaYCbY1^IpCRF+UeU5o=bTXB&H0G+-dSbFc_RA6dxUSv%TSI)^ zewgPq?Uji)#!ZRO=)Jin{^p)P#qRpGF=tht$ z%DY2Catp>y{?d7|EY$OFP4>-Wyyj1dU%oG4t~Fywk#?fDt6AG0+-`lgn@Z?r5) zTADBF=ylphD=hO>$J0C>y}@R9q;{za>0jkk5!vust^e3-YU}G0D<^CCS++uR`VJ#6 zXmwCb(nNTSwv*KYiU?|*B*mN+laA*5$kj5o-pSWTYlqTHipl@W*NMvWt#nR&rEmEu z$m7kkLb{twa|Q&7oC9Fugb=TR*@t+rP;6&;T__ZpX4m?%4ks`WQuG5^UBWaN7z96* zltN;qfDodiwVEy)pc3XPY>JbBrADkz29=L2V9L=jx*aIlCLbo&<&UhdAV-bH3M{r-Y0xEA)9IsGL) zAi_%NBetKb*Ma;P9L7#hZY4n&ImrS#NGsuDtwCFPD&LqzRv3^T1Q=mn9QkVkjKR19 z@eB1#E`@GTmaD(-u zNTuCpQrNA3=5F&j`r)e|Bu6C-^;tg5EQl6=Q{QZyC3USYiXzX0ER3YLgnwwx*yN*M zGuGQSZb)<{Z%JdrZv8K{f-27=e`u>ei0i<&D?bGnXI{Ku(@|UgSnu7&DP_v|+(wquXZjbuYL;A+XEAjDn z&2LxB7bn0DO-l>vH|^N9$xRyfQTD^Mue%2f9yoA^)z9zphu_^QH|*+j;(^+8VqDml zAEiwlcJ|~7{vBst^7!v}q``g+@APc8E?PS?`(*z1_cq-+IcoZ=v!zQGibpR^6B>P} zEk6XV&l=@1DA_6SN9`;{PT6)J=MucO_(m6a;&G9_k?lwQ$O_{>*^Pz z%|F^WTU{}##Po>_5|bFVvr;MhV2vr0=P8`Z(Wc}@=wCzf_bsA6qhAZ2l{GW^x z3R>ou4uNPbW*GqR9aR>H1{7IOU<@%}g8aTlW0@7A3SfC%>R|Oj#3Lap%xj`IoZ(=D5S!vT!mw7M|?6BreajCn!8q4nsP&jJc)V=LZ_ z3dz0*y0pVPqGnBa8+Lgh#k9cYo=1ADUBw&Y9D&ZXMP`*Ais&hSJyr(epjvf-Or6Rg zz{CN|{tmVjJP?mt45WkrCqpN(X{3q(m>e=P<>e$kYyiu{hFqoB@NSz$13FcvT)pgj z_O`Us-o4`+Z`Vwz88$a?qok&PR(Wy=AL=9np$fYgpS4R={&B!2bh(K2b4M~^2bH)9 zqDdckgiGE(PycdJH=x6B>lcg`?>A6uy)SLu9=h`nLE7zE^_#4lvLw7&rTt5qH(u&~ zzkKYdTfdmH^HV!kQ6mSK=1T8NT)r+yaS5-YLJFe=y~bxf2|8G{C37x+)8Ih;TFZhH ze9Nm_U4A@yGez`+IOnI-LFfN6yZFRx8ui_Z((7wa1XkSGo%_L0_q=-j>c8Od!uc(5VKq0Qm_yP)vwNh3KRJbmQfqxtD5gv(- zEm4EKDmd28Hr48rUDHY`{qlq5m0cURbFMY3drXBoswk#a!40+e>v2&I6$pWCMxaiU z{Dr)AR1nA|K}AaLfkYJgVg%h1Vi*oU7Qm;}z@&--5RC=G{_4W~9IM{5XlgJB-n zsbw&59`Q~mW3qy>jVtyFs`Z!I1aXzVPy^(LPeDyagoHOzSN43+kgG05q~dj!%hmO) ztIcI{h}}VG5$nfDmDggFDp}cZq2pfMa)a3MT-bs8(QyM2EGAJcp}f}`Z2PV>0+*5f zU*V8Qrl8v#)^SlB7T;K$gAtGpAYXb>kdQeIkfH?+f&6hSt>qw|fTj1(Mez72CwR4K zf^CjS2!ob_$T_7A%OUOqeCRF4D{8Jt9{gcnO5 z1IRC|YYo{|@)LvyNvIUz8)JtkeWr+v#b-|X_o zX9pMaKK*3sp!t$Szq!YT^pxy}f~=(aT3&)FG)_caixclx|0&`}o9ma=#Fx8XsY$T& zVDFe6hS&qPo?p973zx#4#Z@Ajstr#WUp-%PJ*{s4i6b#m4_EJpjoY0<&Yn%Py}NeP z?jfOT$NWN5M{iI)=Thp`b+@)XmHhDbAJYQYZYrV`VWaBI4{Cfj_3fW}xY57QkykT+ zaj|{x?naGvpD(dK;{CHO_~7|WH{B~o=O3l1OB+6?E?rSZ+`G`&ESWg#pA89pmihXh zPL)(2e3Gf^V_W47W%7gUU(fyrf3+aw;)a}WdF3^c7Ylm7UF+0YaOqKbxPS2lPkkgnCA5yIqB5)e?rzsy>V0ZL*qZeEk8_kY zek+6i@Oq=b>!ayj-OA|0$H$ii5BmpQmo(Z|Zy_8o_HuDvEx!(&2`1=fkzmX*uik{D z46DcDAsvlXG0%D2SR_~B1dj&^+G&E(u0n;IL__ou@)lQPPZ1=6xEi|OFzo@Li3po~ zWejE)Fyw}ODpm$b&(^fE4$4;~T!U_g+= zM(&;9Dj}N8ZJCNOUpIh8lC;1MfeF9(MUaU>NCNxt^m41x6TAgeHk6@2KOS0PQz?*= zr3CWIF1B&{u0Yn+bec}VZ_cZ4&Z~9u2x+JZE$2a&3YaSae8%ZV(^GjfStV#`G0FyL z-VrPNxSu0_h@BMz^))ij3zGY`t&hWwk#RFbhgo?zfwcb7!BkrwMk0Rghh^%IpcF*| zd_I|N1A0fm;orRz<8FN)?sL0Z5yrdb4wAKtB(X^xm&L6%{? zG(N0q%m-4Q%xVdygK<&Auqq6skCxfv09_I2JwPu+K}j?03E-E5&mM|xCc+FO#SEa+ zdW3)Kl5kDx{?%SjanFl81C2)y7bHd>Sa+=E^_mcRQ@v^$?j7*wH`7ewM|-NM2uMWN zjPy1;|C~CuCVW_ApP;w)hh~JEACdfUQE!MhM|`AytwCq*pswp!BycI&m(-i5 zucAgo@^2khKd#+0FmP*dQRyb#?igw3VN=$RTsBP6Nwa@qlxeLp{+F76Dndh&%al9d6qp4qh4NB8(`(x59MQY&9b@OQI*xzRDzoNJyF|QkOQq+I=9ka_^ z)eV2kRM<)RM-4x^j28HpHGQOlZJ~MF;wxTBxOdj4SQw=F>y&=uG2f|Mt7YmhKbRNz z+1>QmpksYAJ@yQlwe9Pyi{*t&z5E*@uWPSKmm5TM4siuza9+@!0j;#IW+6a@N#lJ8 zy>MS1Cg!2$rczX12q8;(WzZe?Fwy9~;04IT6uT1uB^wWrY8gbMLHVkDH$FdbQ(ch_nmDGwuoMF0#?7cR3%bkNp?rNvgrH)}k@3`R(IK}1aKF_Lbm*ZE7T%PK?5`QZu7W9K9| z>UeT8mNR}_U~pa-W_z%ON=g(O2X$mt6WF+c@E+)Z0~ye%pN!JP7tE>$84-?(yf{Mp zA{0ez;Q!x-Q8(_vOa=liB=SI_uW$f6%HRyi+_K$nGbSrM|wBxKIRk<{;Bw2P+1Chvna#9A6Y9y-MI4LXmJO zM{5D4QI|;2kvxYAWmO{YJy^L#pqL3kTm!xjtOm5(u4G6w!B8e&?M7mij-Vxnzoqf# z=mM$hfR)|@Ob5T&8y-Ku+4{}b(+4i`7MkB)EBod` zQP3L$##i?+Y`a~wqH4gb;md=ioo_!lTeolHzJv>+zIO-AT%4_NPU!jGFKJ(ggaqyH z)q|Ry@*pAXJ08#}?){OJJwp2;=70k;m#k=)IF z>eWS6=KI$;bJpd?oE7JepIZ8!TchWpwR>mE5_Wvm&8-nT-QZ5a|7D`*>pt)E!L-;DvOmYO6BkofKlsJi@W8^^{+);!vMr62@ zF{h9z1SAzR7$zX_-b>b@qE(mCX5~@!nC7%|CIs&RSX#`C$uJjovl&V+RsR()qvEvU z!%)YGkL>ECu)~mk6+j%}Fr?NGFeF^+7L-=LEXnAyIWrxi#L|wym$l~6Gm?h%p97D& z^p`Zvf^M=^itS2)$emfb%4E+;`~n4x(M}TdAkrpzloqPXfGUm!r@(;_EfCE@3y02R z_myS{d({^})RTCaB(){LbS`FTyTM%?iH{e7Q|n_d%w)GkSA?Gb7-2!QxO(88KMu5m zb}$RVy1lJt4ANBKP-C)$y^MQ@%U|r|JHxsKI*7$?T)5K?==ofzlX%fFC|rCoWU-av zV;F+OQVC-64KMJ1AZ!8;WwDJH!t+0biW=cz2b2s!Rv40x$Yj=ROHe}z)K#cj@Hs~7aH9? z{x-EL&UesnjmtA4D-L;7mx`uCu#62cI|(}f9vGeExjk5 z9NN2KuSZAki<@^3S9ZBrw%Y%#f!5ay2M_Y&61Kg!H{+dAmo(BI6>5NJ7nO(D%a^v! zH#@I7dab^q|0|wj4;nt9KbPq0^`=+7-d!XAx_9021wOm{^U9k))qNKB%}}dT@Ha8b z`($p~x$c^3%uMB%`^Fc%y<)Aj=ZsNBnbTJaw^nFVap$qH+dJ%`kapR%S4qc+Dw%?p|{zLr-6 zi0KWajECqM8uC&|8*%kTpsMCOV8$4Ia*crx2#rp77DOl^@5IJS;Som^nfm+`vAu#wMWHhud&baMH@WtxhaF_O)Y)5ZYTSNEc!-g80L@fUsU3dk*vg zx}`ito=jwv))%S}fEhzxDHQkyT96%p8G^O2CCd;$FZhckK619;yd9K=dh{um>}l4! zRR(31FOx4fQJXWBiV8VA54hY41CH%wjnm$M?G|zywD{~G>(OJzA8*r=b2hROMYgn( zr0a5_S%z$<<(=Y;U%Fy}td8?2?| zx4DM9_PKZ1o{8gU9DLzBaxOyM8e2}5h83Im+sh4;<=8&z4o@nWz-WU0+k@wR8dfTWT^tiiM1qcQMClN zi9>)R-D7j!EuI2aZt~T(P*NMjc=T=zQEtyV?8We?EZG?+6hs8i`WWq$dbxZ?6qzFk z12DA#uvo!Es_^_E{?RQvMIPNJV9U{O@fRU!ftxpt?e$Yiy?~lNQU3Pq8g<#;VZ%yc zMKyT@gmhmoX(D?+mcr&CDHi%V@Qy@y8imqp536_T;~*ZYm&Nzs7xb?6;}@hVf70^K z(|5dS_X%$|TYJwGe{zJsX_(n{jO6Zv`K5#TdAp-E{3)oxU<-ES&|Wqud+g$muU zvIn1(_b5>5O)~$q%)A{=hE?-y^t8OgiXStNe|EBZ*wzs7d0|1&!B^)?Zugt4@-IA_ z_Py&(^#}ES@r&dF;>2 zD~7#pDhoS%(H0(In7ul)`^wz4<0AgZ-VnP-zgnDEH_;{Jljy4lkLX{|sN@YD>K4>< zW+Z)V=f=qI(*}&Sb=%aKwCCc|gNI{grBp?~E(vSi@M%bZyoA42An&fHGDfV+5Wl`P zvOIjX$KdtV<8;Aa<@B@&*6$np`n(@KQohJBci44dwld^||E5dz8~Rm-OzF7pT1o=X zvi3u_PseXH=l?Tv?$z=y7KgR?x{lsx8J z(?orKx8$0*^nxtkfSTxJX_;xAh(BjY)V*z&GqWsVQ*DJ?C$0;-W1ezEo*q+Q$UN=N zMhPIV4%?eCRt?2cKvMU>bFyNOLHKN*63hyoLx;nXyLcW$tVlr;lc9})XfKd05(AKW zIG!s3x@L|^E>n48aZ>`j_Xt8RHs%0n2+@+H8SJOTpexuO z5d!0d<&R`GUKl67+WJ}Ny`0&YMKv)j2IowfsLvrPD#Q&+!QnAk>RWf{MIC?#dhD*h`33)+1EB2M|olqKU z9d)e9-sH15e!wHDX)`^>Kx?i{Z-Wk%dx~?cF>(H&p$xTJslfSCDfqo7{lLHH z(eeZ5-%)#dZ@9Q*IsKD=|Fz1+@KDdp*HXmit_BXXIM@0wxbt55hpO(S{tJG)vk@@( zy;A2#wxMaSmH#STky!tJ`EJ#FX4elJK1}<_79QWx?PS=Hv9ao{&AMRmc9+a;KD+wK zV_$`bB!g(%dh3faTi}}4Z-9qn@!3#}9gt02n1~?1E9T)b?2C_1f-QH2umJ1K6Sj-| z7hu2%EfjypsDLw#obU=_665pZ14U42NW5qGDApN4&HSM36iOACAS+(UzSvkWw-f+} z0mpDM+=D~_eHjlCd(zG>;khyr5ml2b!Gds$HSq2*n6L+)YxWU_G<23D;`&PtseE9^ zTO2LyG18f-)nQ_@lStyv%fB%z$jv#aGNimV4gA%-2Ac=p5B6CeS6&m+Uy@_2_lYI< zXU)D%!4%235WG`lD`Q~mROpL02`j@5_k)o1V8SgMUJw%pd9d$JD2LW#`q*;aFphLG zc=(w7Cl}+nd|mG-yaI;$SeQ%cD4D=6(a6Qo-@(WS5xi&ACyILpQ}yYh#@sb8k32Ew7&LEB-)3 z@luopG19Qiw^P$)il8|Y)|y@qI&^+bWJSk$*;J7Cs=|p;RWS`9I ziu;nnduKm5Ie!$dDskEh&!FbAiGqGItIOXi{60?6F3l(nqPEq4wfc^rdPMX)BYxk1 zR9I@7{6<6K*ZU+Nzq{;KjZ`X5yKu5R_=n|@+xpL3-1U$(UGwFj%pW2ijr%0zm!HST z3%`%wX50G@zGdD%*&B=dEZu1v{aQ}EZSUOB-A@tqj$z#sd{Zi zNvH8U7HsRf)a`U3m3Hc*t*W@7f^f|?q07{3sII~o5By}h;B*3%x@ zKW?T>PEBqwtxGj+xuKpr>4JK|=L3fSG0}eJ+hOOHP4M+G#6LLNyut8UnU^c|B=cuC z{XaVR>phnpKYLPJ()2;p*aPubd(KLVn>*j!m_Nqe_=d$PQJo08dex*YJ@Uu>k}R2N zXutG~fL{~7ErD(Nd4sMdpX_^esKP(FQNJy^(OfH7n)yYJ&6>Vi7hIj$xFE=ql^GQ) zTWb3UY_uN5Fy9Vx)MZ6D;N~E)BkclzU|f7HA)eSYC?6NIbtaE8l8OR2!4af|#FC@r z3ad|fsFQ3ND$?f8Q2AevSPf+a*c#&~v(B-Yd9sIn0;|1owyscR;~4<>@JM+Vh_dQ| zI7^JgZs5>!#=O`7A(V_y437;;3TEeZk20Gk_aP^BvJoE(H<8raXIvwQ+Uf2E9gvKjjt1Cy#LZF^NI^VqAv`9N)rZ(qNk%uBsI^s{S^u+bKe>Sfa)FcFc z5Y=ou=AXMhFJtF>L&msE-!+v^H%&PvxgNXLHY?k5W6kaI9)Hg%Jbv(vqvzMC-}EY7 z-`ls_vAgDR?{1ww=?~syLsL#;96W5ECV%IL@MRa*eIK7Q?dMyo?T0@uFny6HDVrO5 zbKLn#?`ErUtieCdrTNoOBfTKYpREe3=NBC2ZTm+TVV_r$$|_|8qz2*af>Bf6o%`vd z4a*g;Y~8!NX6lLe=B`Z+OAqZ6GJCan<%S=}$oKkB>F=e~t8b(SP8pE2DmQA5?VrZC zXJ+Py#;$GHW7~bhudqBP;po{0?}l7`^+ydJp`MZDCx-MleQa_beTe?`v@QN<`Q}+k zu>rhg>Zu2|Pu(W&(cfS1wPDuSrizelU%2~ZObLh0ElU01Z-GFG)@)e-{9O|({R?b# zusNtPOBFt@#4OwMXzZS=ma*?e#l9K<&l%7F$WHy546nfy4yDh5j=;7zE%DV-NUG1e zl1#$Cew>#^9ws4)D<;khMXuS*reK7a61c#Ho$Q-V0GZ2j>0Ox}#aeALA&JL>hUT6kjClOcr84KI_hLko6iOf&(AoPPwGdwzg##3Si zViSUBN~hUQL1}E3xua`+sh1m+5qGu}FG(6bL+P__TYsr%WyGe&CWaO>D_GlD1nD#} z`VLmukQsTu{$t#&6pkg~&=^__sDKUyHxBNds{(e&Mho0OP31!hj|5_!PtFFOj*gJ^b%o?&k+|3h8z%(j zudj%7ixvDI?%o8dsjKZ54`C1xR5U0cS^+@M0u65U4>#k); z%y7;=`|SODrr+~Crq+UZ>D0^|>IjuOR2ZmhRcpeWLKB#2Ep`aYeM@^7B0qLuhA3nz zDa$?4Hgs@b!m1p8S4MtSaa`d($GT?`F~Yg`UWR`olb;RwxxMD$jlolGr?sbNNBzl~ z_2t{9@MG*<5z)bhVaXkT(RM%bv6C1c{KjF=V25L+rLe0o^x*71#y1H{mTDi?E zG-Su=DO=rVZ~bYp!&CdL>pxSTJ4Q(+9GNXE8#=vNULL8NbDy|N-um!lWYy{OW)y+>ep~N%)yhV{#Eks;pipmUb5CEC zda#AWFq)lNm>DtEompV?Cr2=usx{s)cj*f5spk8#Pp?pPyPc+)ny~tRJt)Z9U{&q0 zJ=xJ~$jB&jb>FgAk4%FDGC6d&!ZUk~f;M)4)!`^oIqjQ0V6(f?FSWs+&G61t*eBkp zE1NHCO|82UE0o7vdTyWW#X0@lbmp`6>dPF~(o2P*%Ds1jVo%G+GkU`2Ta-+?AX|E$ z(m`(*PK;%{)*a-=*c5~Z@}DF-Kx?1*=WM+y-a8nvdBrN_do{vCXLY`8S$J$isM36c zSJ)F}gJdO8$o|F0-X{co@X$LdXAkWr$XIH!0cfN|1zMyVAe9Wf3*ZREWtV93LHvrK z*5F$C#0LPIa41f50FfXCyB>Qi^ngnk5|5A;RcKRtd7-2Sc07#Yv#qU@?FV}A-}GhJ ztojJn+Od_)sYUw@1$Sp7ni3x&%kAW1h2;W4sUT|{USOET; zMvib%j+WQh2dJS0U@~=qZi@RXO%_A}K&wG>toVw_0L&HXNdCcvjn^ZZ8tg_vKILdw zZjiG0~dgFrAhgV3VPx6mQyc zA(PX%vVl5t9J4}rqoeH3Ye5XRXoqZg&jNn*X-d@G^#_s{asKKSC%4o%b$YKRFRU7B zKJ?4V<{q}Bap6Noe1+?Z{La+_2l6{t%AURM7M!&VvHx+|J%{*4ihESPVjQ0&E-cy= z8o|CBcgZsRH<>EcG|4)|UVGu@3Ul|w>Cv~tMXja|b6QBQHs@=z7e3}VC<_HTu2aRg zeBagzN%5AUr%DXn1im&&oxW3qCr@u%EX|HLUmNgSC%e#Pg=xcS_lWK1bhX>|r|ECm z-n1#?ltYK$NljE-^YLT14LWId$+-cZ?&EVU1wCi>nxAjHX%KwQacT5y+5=00!QI7= z#SfAKCK%7xo9e)49I`Q13eUUL9qf)bSv1GoFX;95?<`e2yQ;4m|GM`+%h#21Va2*d z771jF^l;uX)Ag#!`P6+iJnAEvNBN#kTQ9_6Xa(w6NkgC@xku-6M0LFsYs@3yzGzJ2 zFDc16V4$q!W`p9lez&@&gdvJh;TTy}h6MnY6ZnZiEkM0R2++kF9&L1-_&~H0!g*d6 zNKMlKG{m2X|HA%2E{bv#rVGFjqmSz(Anyl@68lv{ zFTfhQ3o7D`jzMsI(Z(Ls9Va6b;UWw~h+?mUEBNYBK;ghqLt-R=$fz@e7nHMs#(e&K zzfd7*Kh#MT^K;(-MxvBk%rdse98D{z7G!S*wXJ9*!@_h`jCida2xuD~hsc7f1*Ekq z3*^kmtG5Bq63;02ZB5@nZUT@&G=(|gcOQB)K(@tTPmT{3M~Fm1h!^C{K(@pvvw+A0 zM#wtmfl!6u&%z6=A!-;F?_mv%wM2t)Y772<7{b#8Jnbe7H5reHDhA-kq&2t;px5Im27Ma7 zBnBF?gvdK1a2ylSh|1+SNe$!%axB5LU|hyi$tw6UXvwk49}PrCXHP$P&~Gr^-AT2l zt^Q)UE=|Zm@f0eL4$Hjct=ecr>P!{z;0!@F$&H|75RWlSE10soavY7an8DPY%@bBS zQz-@h-}F4Yrn@k|qpn~YpP@JY>+l(ylH0rdMTH8#+Lf<|Js-7fzBJPne8UC%r23s) z!6*EcN|}GDUPsJ4{`RQwjLMK$vGojjHs7}uQY9JO!!`h+bQpo z*d4AcEgSO&gPB44r5mP^GjtAWOPI|9N}S|;C_O`TiJTo}5yHDMyT^`MEr@0Zd2=#E zeo#8cQ)!_$$E@brWl3l;Tlc)kFDa8{eUwKvilY`eM~JP<-x-pw9CdD~@Lt8%m$glE zXU4ZZreE{_>xbAWg740-;?A!Ab6xkQ(sLQob^KztBG+$!{H1ngd{UMm-o45tx>YLi zOyo_Z36eVJGaQ3%t+-}9PDskl<(R2*s%?Zi3*0S_IItb4OP;wUM(o#Tq0k%|!Sa(5Fy89E>egkpz?cQJz09qXbl$3(aTp^=q&9wkq#b9 zl;*5Cn~F8f*jR|CG?D{^=h>vJp?@MK>xx# zU*tv}!`wxc3a~F(0!spB2(TQcDtzZxZ{up`n6e-K6&0dKc^ANU5Z-@L9Kz8Ga_M^v zq)=Opd4P)kfo1}7AXu9M>M$tq7pYVuE)bekM!L)c3fF0NjrI?;0!s?LH+6cxt<`FA zSg_fUQP_Rcvq6|9E&T41DR*$Ha2-jJ`b$}r+4)QEPNk;O*{-^z7}>=V74z!`L5@N> z^MGP?p2L|5{0FH<|0+vTR-BT}yIoUq-&17Rm}RPSK(XqJT$Qn6)^zh>FpiV5gw50A zGUk{of^xM&^Z9F4Mn|Va^dyBZHEF+`gC0sRhs7BPk2)x#$~7j+-CpW^*j>fnR$Wz&NaMm+wstc>tJpx__= ze)h66CF@ih1);+Ali5=QS|WjtU5Y=I-sx#1AjREv_~wFnL&f&`ZBZO@MOsbI3L0m& zM!bVZpPXSgil7fb>nx2}Qi^m>)L%=dVLYl>pz8#8XoQ+E2)*M#SgZM5zPJM?=$6JL zTRp&6%`veZ4cZ~V9#FX;bqtpru7YnEL!gxii=qD^)=&-V?ST4jibcK{B5;`Nyapo) zq(#NF43Lx0!VesLR#k$;c&$Jr+&^f_jov^*80GfsVETfF31NkkLE*gUWG@Ks9vLxo z?Zpy`{uT*kcS#74C(AN`9}AiF>Tb8AY!CZM7eZSC_#UG5L4?;8lM6~v z=PW~_0FrMHM57S@lBB!*6RtpVP)43Rhg%s2{e>tUfS&-Y{{7?AQ14c`)^rZ_8B6Rk z%Bes;+7Gx4NWft!fVVZ4E-q?yyauO@jp{^1o;A9%j~;{&(O?z91R33Epe?DX!eizF zeTsH8(&P5msV7u(U;pX(A2_RsVfwc^1EEUX{xgFKmC_&*IurfLgjUc%b`ZIgViw_}0528u;;~ zK%=_9aftE3!JPyRH8KAKeg;Y~QJZu(2E@OoQ-Jbes0Q$Oq*tf|k)w!-(kY0r>X^~b zA_8gvqF~XWm4~w);9Mnm;ADitP_;Q821F>TNX$4BEkI396o&*&jdb4}w!mnr8*4)P zPceSsBb`?g3;JVOx(XWfh9<5R^`4$W5g{uIT68QIkiaqV7`F?D?28WrpJm6Ec6bNs9R4TjC1kTVZO7OwC%6nIE8^)gM@M+qA5n;vt>4X5f_6>O6h z3l5xPmS{yAvy3DC1O*nP6FD;fWCwx!s=SvhMn)2IW|b2?n#E{+pjf%gwDNdX#8ei< z#IF39W3Pw^9GAl0aUfqlDcFS<6gJIjwYk*$X9y{rDVfA?V2w)(HDB;$AN|UM395F7 zGt6!m-tfAeO0BEB)|%(F0zaGRAa6lowo9sL!z$&;=hnA#S~ggQZ)8bD6Ko<*br%aB z#!ZjPTAN+Z9rlLZipbd^ZOs*7 z?$7aBuALk2{wnj1J9+$&jO@naEfR7G)dw@tq-KeF)$0{uu*7XkAuc z25Q!plEb=r`@Vz;A#DnC0uvs5tqBkjonN5QEHl7e8W0cr;Nu7w;Y6kHA+pKTu4ES= z->SDfOb&LKyvGj$OK|jSv{4$uQkZ9ubwu=xfX=bP&z=$roi0$|iJO2Dv53ye?|ttW zn#RJE`ZZRj#)30?jpftWHvyY5OAXCE&^KV-?4bn>q^kshwAJY1;E9f>9Fa0+LH9X( z5>%B&b-0NemZ>}DXk(qP=(4f*6GP!x;;k+$nZIKw2FQQLQKZZ5LF+6Y@C@D_1nx}YB!~qEkcW2$3IYQ!7JxJp*e#%g5PjJ-(MC{RJV76=r0Sw9 z2SS5}6btl%*oAW72tWo1!c?T$BAYBQnm{_gWmObhwvB zJFt{p`7-j&*)J}iHTCiTwJE>-6K62moozdZz7qP{YYVedS$Ujsosny)H^tpfj{Wq7`me7KE_L_ zx1?J0g(dFck&jI6zu=f&S?DQt9+#fcP5QH+yQzcqu&pLYVd63)Xx^ajE612$A+U`NB@iTjmqg&;-9L^URFDf)(W@Xntn2_?e*5$ z-?AN|3U)=@e-dDPA-&@j@ACGp^KVV_Pd;DBKdtk)HKmXh*Q%QI^Mv?6*+)xB`R0*} zcKlM~wfS+ESoip;XOI6-HID?gIwrkXmHYd%zzbUWvBQ%I zZfw`64g9vX#w*S~tVt8arQNm~@{ZJM;+EgpXZqO9F6=tBX)rM_pIWtE?)}rZW?8|M z(}k4M`ZIy2vsT*(nH~3K0eK-g-!s#N>)m|soN!%wIZL>E+~82tm8_ZZWc}YwgWvAr zM8tgIK|LMVM%~wLBlLCPO)=tC#GA}8b?)|n#wJpa(1$%bsS{e}9HQrESe7PwHSLe6 z=IX3M`f#;rg05AuB)G*8&_|;Kk5Z3Nkr?QAG3vM@qdEg%>Jw%mLT*HZBK~+_;D!Ie z+J;g%ki|>)u!64Z|%^gdaH44*6-D1Mp#r0`{7XdE`w*>ZfVg!@p&7v_y z$DA8nq3ItMf*}$-#_nZYm0w$ma^7PANz^rZ4DJdRK^s#qz$ykXjtyHT z3+OiyyXGOd#$4qOl=TneU`#0Miv6lM>t9ojk%M3ehx7nYh{@DUxcV6f(KNP$2DGpi ztoaXm9yL^8hW!jtTz^stM8t2Uu<6e>lPDTpF|a18MO$e=EZDcDVWE zEG1OlMV?I;%81WENysb_@*>qBj}g(v;71fEl7q(9f$U~<=i%{z+WWC*RULMMv5X*m zQ$MDnv;D&Zf%pem5t@mEm5Nnf5LxO3GR-ByVcs7XA3qc&?-Ey!HD9&wnCM;2R-z)F zHPDqyG6VSyzyhGOf;9zQg=vGtt!_t3ueTvr*QR0@vTl*eTR^0)2WPwpS5Tu z1z4AVpD+LA^RfokyO3!n&wXdMO#kfVz{y7?yZPdx$|P2q!l1=T^m-NN?qwT};mJzj z`ts25m_*l_#NjG8%eOU)EDjp&sIeZrB4023tcA?4QKk#G-cA+y@4jl5Catcde&=3d zDO*ALwyMzo%{2XV;i>$8w{Oy0@ZHeyTbG4?9#!AOU(%~x{Yzql@QPpO%G{&v8MLA; zcfwTNhadjS;kdSor9f^s{v9o&EkWL-|6RkE&8wPkTYY-ReJeG~Xrr(wJJFsSFhG_T(8jE9-*m1pCv8^2O(#+7bz*}<9VXz--@rR^zYuw#Bw7Pn)#u0-i9O$s=|znZqn;cBWD-!+2Wa490909z&mR0CB7={XuQ6B*T%QP=xN z@8|St)(ETRin5RYMSg8U2FOU~d-EZ_ubPA06XvslS zD@ee_tO#3MFK~3=It+-;1)G)7PsZ*|tc#28vZ_|SvMkiA()1y!6nFMMr1dMN`{g~kP?GOQ&j@2X^}3p zjtkMo5xp1~jy~>BEk?c_1j_$6v}xSl78ZpXlXW4WHFmyWP%;wvhE&v-54 z_!{DtpmyW-CpEI?+)VS`?v4S*x!n8BJMvvS~mBeaxI})nSoZ(5}Zt zL*w#A6ksAUlfd!E?p+Q|S%If+Psw2V1w-<;p6Y<|uwc6=_5$bhSKR!ff-7V{L3%g8 z&hDs1`SZ{6@8#DtO0J*V(4fj}Ru%?UnB0CogMT$8Hg{ldqK{?&4asA6Z&l&h1ZVyq zOxKkWuh;dz5grrP@P4o4WDQH_wl$bo3jF4hdEq&{hSzMbN%myl+ZIiavX-}Ivb+T` z_T7g!_s4~}MGl*BYq|qP5wjBanuhlZn-?~I`9 z(yxkM@m?MBHr%u7GW8KHYOZ(SCw0=vOBZ)q$3O4)?5X){a(wI4#WTL-KXudX+<0W> zt0j@Ni_<(V{o4HX%vRAx2IafGOyw!k*LH22R^RnEPUNo9v#~2!`ID)SCFhrQsr3WO zAD`1lztkt&b9EL->}gk(!+;rf@nGHt`p1)@tabm= zhS0YnM%PGpfBYZ)VoFYMsmrs_Sx_hEAZ8$f&z8^uM)_fCNQeQw85eiNA2UJaASk(r zmkS@M(_|5j;1C%80}38>ANnehWud9OjWI(5%GyC5m6#g?P*QRq^5|H#eG|FFLH@~S ziFHChsAG?{W-Jzv&HmqL&8{klg{Tt`kh2)=#4219T}uU>4vgKWgw<5>fu%su3^t&6 zBSC}!`SM;EK$LApz`1x6ueZ_x5fmiTdIPM9zDXDBDr8K)-4o#Tx~TF(g?<{W9d&Jg zs4Z(0-bo)hi9t45X^>a*9oWmG);v}SaY7V)b5!lq6crqOLjyCToK>X!LIg-a0=4b` zKi2}q^2eSPzy6zt7}}lUa>FFRVUVo=vH(Vg@sEQ=x(ZO`psI4QAn6HCpUEYedclKp3IOiDxorVqLYoU~l1aNyYRFnemF%GzaodFaBv>;JV2hx6E zHt`%2y!K8peDWY|NTYGYZtpphpnQf0H~}cm2{!?$ugd(zxch zJ?tr5kH*078oQ=BJ96*36|&Ho0E!`++^$16Hd3 zx@vPv!_1XhmqkDLO`APj|J||X&nL}}HI?d+?h77F>oA!9Ma$MtO*3<2A2YHJ&nQ|| zJe!oM+Z?pvzj4N15BSO-u3lt-w;t>n0l=FU%`@Hz{e&;>zDc z6^?n;n{#t}s)hN}j2vzUxP|#mojqLZC=SU;eEj&$|Z`?`GJt>Tfdj=D69VD zMTPdc^rrd>*{#QG6Y7>7$eH;z$+a%!dFh%QW8U(q2KC9yxRy`%d-b;-G^@|b;#}NO zquX(>PeDK4$vi=(YEhn(clM9_LU`J%$StfqrhJd2X7~_|^t4x)7_T5LqP5k;K@W@gT2n`Kc-I|GEH``|5>&sFJ7y=MlwElsdP_j4nx2S`)duY#-2Qv&FO}MW%jVA zN$j{X=r>0mE{4U55LBP)=7Sov0I2BDU`L_-gL9yX%r~qeg6|tCj#hvQwrb5Ay?>r* zFD8gsOe88GvlF^H6S_Ib5yGQV2F-JfJw~}X2eof@Rcd=hFvTbhxxGnP+d{D)bXHx$jRKjq)P1~BRCiy>kEqMD%7u%>4kALG z#J+biErg65DAH-*Ke1&w=t@<6lm77YkB9Q|A*Z?Js-S@k8$?GYq{4ew2J;_yYiOn@ z`hJ0^k~@MJBp_X2R)M&!ZkG7JE)_IFK=TZT!XW*TehwiG1CR_zNhUu2@1BwT{0N;< z%}8JsjFCl{IgHlBXhl16e#uCDEz!H3S7U(+!y`q&(u>pSj(QP+(dGjQY?(QzFAzHZ zEDd@SzGDq6qi0~dU|^=CNyHF39L4y5ven3F5SR%D$NWc$fTGJdQmghk6Q}6|FdO(U7EZ65eNe> zq0RSI+KecsJ)9+hxV)TY3TvyUyiJoe=$cb&QB49hMmc z{J!3-=0w8;D#d@gAmm1ksYCoVdvY4Tz@pwl5+R=7J#&-)jp37%`u~jhJ$t#c-JKJ! z9OLe6H!a?D&5?g2wJxr!ucgbn#aGX#Gek}gkd@FUXKWa^SKIwa@{vu+p`9Co58Gw9 zJf9ZtUbyIsi>F`MbPFFnpVS~UTxu@famwlvmsfWu<+4bBW!Xa7#qv3ARm-$9*{cT) z7SehSGkNPb%--VYFckM-{q^PB_>L5C7)c#B`)$+x3uhQ*k{FAnx_dcC zH|1z=dd(e-IG$7!z`fJDIIO?H@_F#^z_DWz*{jA0TbqC4Z|j@aSh(Rp%DlA>`>xM? z{>MV+Y5c67NC_{`=$C*SM*7FZJabyI#kPIx^Fu9v=e$@G-tx!oy5Dm)&FkRKp0adf zCb|35vv=Fhj{jX&YtlAL+PPu&=LS*NuQzrqI52<0bXm~4-^ucQ`G0*sd;O4f^)0g> z%!WigH(KoEKibuCkCP(y#8M_*v5`2@W>V=jv#hJr3sjr!&Av^R6^4!ydol@TY8oBD6C9H?F7A8N)g^O~`XKSky`B_qlA zfkk>U^+&4K^j9rRM#FQL7GI8H{wiHN9^@HK13}VDpP;a(b#D=kca7LXuJhq;eWJ{m z=j-rpd7-lYd`tD@RMEJLNBg7MWwZwU5~GYu&qC#CJFnExQZv4fe;GP|==e8j<1am< zQM~bSW1*C(NJt@K9#OcI+zYu_D^zj0RiS`gkIACm`m4u?|M4&?A8ud_Fp0EKx zqi19AB8v0)jf*I(BOY6~Iq4wwi#9(5E7S*Dx5Gu1HxBuVV z#u{=8L67?%2^l>s!4MvQXnCZ87wAqr*$$_ih+Pp-qfi7PWDw?Iq-wE#TQ|ah7Va`| z(yOkpmX<)<54w^26!i1=QB@?1)d-5*V*iZ`hyo;-%xtxUL^%4M=KZmGfiM~4r<%$# z>aEtCMgzm|o%EXA5}Z&N4`zoDM&$#?`J4P0`yS45`kNRU^(JZL5;#eSpn-e@3PGS7 za0wAc1AKd7V*6N&dBf$Pqo9D~IKwm=r_M~C1D6N0j(-J8z^JGrRPnJx(1CYdY{9I#0H2sI9BUFcooSdscORqWEMy{wRfJd zcL-)4{0h4eU9d-y;Es}B0)rHivr5gS#{N7~$vvtZdM85lA?Wj^)qW(1GD4!(px!b_ z@8LrAIMS$uA!Hc^wSGbt7b?F20azd?4uZom?~e$NW2%9^pB*QLeCjGPx-d|#x<4RP zb$FnDMJqo4^+|;5tAVIbzKCf2EH&)J(5IA`BMj?{CIP&Qps(efuRnez{E&Beq$kaA zXwfsvu!YV$j$Px02F(wQaDH=r?RM$)=Y7oA!f`R;Eb_4#Ns)I<4%NDcb+~mdD(crFtXwzr z^@hmP1zBstUe_IbXx_N`DYIhh7kBckm2uk(qhinbkN6xrmbOrv^1wF7EJSCWs-BnI z3Yj52x+U&gZ+_h_JH%`P(md1sr2l`B<{#xh6In61LYwxsu9@v!DIu&yE;kwtW7hy`S-m z&hV?}4tMhgCT>d=P~HxoTNbou^)2hv^YsJiGtQM?-MQ>s{El@-dR6zqyf}89+*a|- zVnK?-&v!OeCEUDqx8j$$=&qq-Czm$HJnr+hxxA~t{wvqZl-IRnKZnnU2gzRv;@<5o+w8OxhuKbLH=xZLsVZPOq3|HFyY zmL2@TJ&dt9w>Zbt_rbb;(VxK%(AGju;|Zb>bW@C*se4PU}sQIggKX{6&+hIGMrGlMk$}+ za8feMzEHrEKH%=@u_EGma?BP$TaW67_In^bTMT;!^K0zIB9f3ttEr%CA-Q89gg11> z3N@4kqAnt%c{2z(L73Qsq`{E_1lxG`!9Ijs1EPI1+Ks5sG0@Wcf=EP0&d2DYA(4}? zdkBFhvbH+ZXf#U$F!*^(O<%V542+#7H6AgC^t12K^ z)fAX97x6zcW01S(pUjt0sr#`RgCq!qW6@n2`v6}Rn#-}AN6oU6!)hU9VE|MgH)6sD z5RGFC5+KRxXI=_R;T#W&64x(4586gCAxU5FsPQtUJvlU3;ctwpf8aD?g6#vz9fm;s zjwhHzDCB|4^rPSfVo4(N8hl+D5EIGFE%>4(tM1E@pH{AqaIT)ZgPh?SWLK{};<(sg?HifZTXFb-yxA)shl@=rUxp8^<$IJI%_%QmKH0KAV&8<_66Y1% zSj(Q}vr_fSrCFSzJo)9X?WOlE`zR2a6U+1d@5#{*~)ynX!GGVQvB=c1HSjy00qu zv82zu*7QO54`1~6|NGFv@&WG@P{rRk?`^HRx7jCUc4eY_HKj4~Xn*17))Pvll>M0k z=jHR}DRwV&k8@Xq3&K71j$fR=gcIOb=Jl_V#gO8(!_@25LUG{4GHQ2=NAF;9B9&BL zbDD{IUoPbxR32scUl7DHc>hJ}Z`qQ5?>~P=?)dbNQ%%#NJ~Cn{?)sooNX{M)ngP9fs zL-HTj>TX;OmuL7hwd6pkY!#!fntS2^{k)SEw;Ud%#w#K7`!90$mu1+W>EFiv=1KSw z-sFv4t?p~C>mBcEiC~CzIe{ypS)y1t7m;~c1zufS0Il6cu$K`H8Vn#5&<%z3NQ8BS z7L3E(7@KNl0Yj50w2ZkwM9HT2L2G2jK$m8ytFJ?p(}ya}G!jT4AZrTK*-9w(QG#Qw zLf9>%t_B1ju+cH@0^41kzyt6&N(@HVb1#B-CH-pa?m!)M&7N1 zTt9OG6U?Fab@;JZfK3azMOD?(Ng>v$(t%^lBfNcSdXQzMlMp(y#VyPUxwm+=?ldLt zQH`bwVZ1(oy@o=mZu11v5e;${N^QluI2u0s?jB^1VOTFhMZ?EJC1}mr=!hRn49#iK z&(QWzTZZIjdC*?aFxw=U8GLqM*MbT13-7=j$BsIxe<8)Q4;9i7LPf2ViDYBCy zw}>%a^0RDp$}7R;wN>(G`C%XsqN*~Nv*T%kyzJ#c))|f`9`xx;yK0^(NdCI!wNGi; z_Tvq+ge76KOxQ*}Y+JpfGmnw_&eM!1n@=}2PZ4h6EdTlsw%Ij#g4CCDibHPW<{o$F z9TM!xkGH#E%Ii#Gde*ux%FfYZRtu?-GOOy86`r|~e(RHUQwBcWV))Zse^OmsugAI( zN49olMA$e9-=~K?-3hvqd~!BGK6w#kz)VV&(Ywr3!kXrGWW3~#gFG1RLv3HD%^}w< zZl1x|nm4)iK{)lz!;}JQ?gK^hV(Gu5bzZeK?GExh?h|loqcWrI`y)%--g!!bXj5Jl zIUbw3wx723Kz_;YVWSAP|4w1gDgU)Cbxs-g3_~~j+zTQ5zWmuTynoMELc4bkXF7J) zHJB6~@f2J8EUKN*;=q#i^l9DswN2E%=Rp$vfK{DcRmd{)TBTN_s-1V03cz!T^2a6) zS^Qt5E!U^Wj$iA)b#;kf)UMMr18XuXzr5rZ=J#6A+Zr|5(dpuBnaTG~isfd)WoELt z(~cR8$e!r=lae}9lev0@+v|!Kh~?uw$+y<0M;Z*d{Z-k1&Z%nB&EjWk*BlT0cgVkF zUh9WE=KH{UIpx_wQv-t*{RyH(e0J*D>a9l_CSR|UorN5f=@EAt*fUEHCGK-~a*?TUhuQ))9`A`cjxsOoUQ`&;}zdN^wCM_3yVI71cbUb%0mJ z7;W{Oj7iF^5*p$u!Qo~>#h)4ka5(D}Q$T6Rf3Mc@5i;Zu_y(ppW>CX6Ml>QQ_NoV) z1e7%$E27)hC_3&&=nL&f&|ya(sQdd%W#)lVogdKAim*5sKoY5fKtEX!W5M?Tzkn(S zrGIj(AQ*w$fFpn+yeACg%7)RII)sHo2iH_-C%x2DG!Axqd5*j99ex}HKW16;Hw5(0 zK-zmjhk6?R7oAGPa7XJZA&7xF`nX2{dJ_@}H28|1@qD1Ks~@Atj%KzOHA#_xE;)?` zLtg=J&3b=#{tlF5KEoI^Rv-QV5D|yn7|s|FjV2n6di)@C_U$CuIzgu(eI|+CXNNn^{3UDV}O|s>O(2jr(l-~E+_ozGUg2V%;|C$^5w@3qWEMf0YJ8|1br2u*C z|J5=w6D7r~WeCFT_g*PjU7E%&_!J+Yxg6#VkvCygLl-re5ItL9GsBRGBg7PyWd-PT z!rU7|k}HZ?5Q#6yZcswONh09@8JbA8;=`jOqPPWA9)#*^siBt$#Dw zasDs2W)(QD6&=y0QnaoFw7Q!&TrW&W30uJhYFm%oJ7W8jNZCZb*--p(CYSLz*xjx= z9zyk$jG#8rGPahmltVMNZW9z=wmU9fC@s6qZsVCZeEy6*{&Rlc>ZIUTjA@@x%bors z9Wv~h^e6jwO4B0qzgD!&D~-(ctf5rhZ;reoc8p*GK6Lc1WenASJ>2xQBBCI~(ZrBL zx9n^7v2hLdd2I-P6x!}jCz2u=@ms3e&f0V}<@EgQU@i|j5L?Xs#Pr3pB~nc6uM5Z&hMKXX?Uh7CF`8OW&WFsVBOT^`Y8*R zzgRwO=+|r>wN)G{Nbz(xPmFI}xu(ONmFqC2&)s^f$YPF8I%n@<>Fydvm#C23x9`4S z3wMi|Ser8Q%~SSKV~ReH(!Nld1jH}08;id)Y@>wav*3<<_eewA;2G(z$4#*uvlM#1 z$6JrCO%AEQe`k7DI%iSCytrSJI@S*mvr)ewCTxucv6t3E3+HymXGkYph{ml8hmyg#nT<_R?lCC_QV>QAEpk6F$S-89C z&^f?Kxa*V&@(`~cz3k)sTk@YACtsH(a#Jb#H&6ZK$ez%D`176!F?6<_Hq-z-q=Nn~ zh;x^)3z=1iC`P%0TtTHEnxfzMB*d%#6gPVS>Epb~$IpcCm|S3f!YRDak-4CA*|W7=orYYLo&o^R^tTr5UcJBZ2=yUSVx_(dQq4F z-NHynK4E0iFkg0ygMo)6*YdJB!q>%gQ26aY-D(+&KNVVl^7En6N)Jo{8|&owUcptM zOGVCat89d=cmj}ajFJ-&;t-Y$Km$76DC7X#yzjw128Uuk033BwuYW}C_en|!H>M0D zIuOy=ifKOyiU10s-hmqwxc#_;i{=)RiK>Am05^d`Hvk?x2>n*mLg}3fZ6O>>1&k5s z^T5O(#VfR;**-`HjCp9z9|y3G4w(kog7`g8cAu^jR69(dke(|0v?L06jaU{R{>7uf z7kIOpb{lwj(`f8CDB6oJrn9P;ra7*97SQ0y=tMd&xM~_nUR5EC0`?YqUl(43Lr(Sn z0BAO#BM$gQLbOjZJOrdKDYx+$(N9OHZ7?V&rv-41PZ*lNZEzTL)*1L6^tTOFW`wDo zP*q?-%p%g|b9=O<7bI@9dpy$1K{I2gvns>5ig z*~AH#vtY!rk9}(5;HQ63VL)0i*3e<%e`vPKFIXRfYO+QZznu|wCS|QL{aM#In)!`T z&-WbvS?%5WO#@Bb|BRRo@s2Z%wI!x{nG7d0QEZuPslyq=lQXP~=1N$eb?jQQxZi5T zZN;sC-QrH|uPb<4l|yBjH}cAkZD^Pswpl)*Gp1#_Q*e6~-(W)=sD_-jI`bS}L6?3f z<$h58?5McPs)De3D%pu!q~DaIIH8-;vt?6qXRGPlfjK^L$#mJ1)b;`6Lw8Mk=}TCu zQY!_NZU8!C=nB1Z>Ychtjcwvv%gXB)iYGg!iu51Q8DVMK)=dHJ4S`t-UF(JoL9*i^8QebL8gyUB6Y8f z$#4<)PBE`ea5Dc6BBCy`<%8wz8MBfEdsMFsGn8b5bc^A)-ihu?D1N2jR~yq0U;CjZ z#_q&3D<_w(7ptd5U2LyX9-HXd^&%{&WzmX>jD2ONohdOV<#q4A*5$4Em2Vu{@!NOJ zG5&m2wf=cL*@0=+ zH=YffdqL=#j2S_8#c2!D(9RkOQo%Yr3+)^xh-;pU;fcmcfmG=NvM}9>?a{pgE;UXe;$m4Bvz`oDoD#v|M*FvY6Oc{ z0FC_vtN=H%ZmnS=@+J$o>u6`|wBB&Gp-T_oEr6C4u=CNtUossQtL~|^Y@OWFrc3v+ z`5qFx@*tAy8I-Jbv5J;M6EabY29SkNlq;^p5^8%Osv!XH?xXT<2opc%I6-xFj)^VB z(3s?k>8_}w84Y!pC;{29UxR#V-5qmkKj58y;J_i=C}aKhz)&J`3XotO^8iKjy;lGC zKIL$3LaD$CD&k1OK#+o|Wf!k-qs3)Tj?3*sV&Z`^u0qJ_(SE&L7zbecDJ||Wf-VKV z+uA4Jzso?MRWN|`7FPkkyD343$@oO*hHz;dEDLnB->cQ!Xg3;kFnS$`XmZ*!gl>3A zz!}lfxe2%pfVT^e0}gp*N zoOzH1`JF_u3_<>De;!Nd{IX27jd?a;t|Zja94YG}fc3=;w0K65Ay-!oXvTDFv{ zFauvwt}364#=-O{ROtS!QK5HtsYS&*<#09JF9SD!zBdTZ*?PIEoziU~=!_Kr0+-SI z@O2PqXcMV&1(0l(4FAJXE=g${GHH5kXC^(at55@@;3JKx|2YnU!zK_gXKm#1iik{DWK5- zI`o%6k*8M|u6%XeuI15#h|Nz1GD~*$U-|4@_=>v?5kI6C>+p`>k#xFy8EzNzvnZ_+ zN>!m>LHM4!g0OBX<$#ImNysve%qEN_e;Jy!LT~rpp(41pg7d1)ysI}%wa<|)o`p+_ z$z_2_!eie5*-;y_+90lO{DZf3Ygv6DYHuzX+2TIwF}toN&_OlgSr-zXZOJyDy=GI) z zzy`;g=9I!!{~Fx9S7)fD>5%>Qrbtdi-miv5LwOR_ws}q~re)-A6g-&v+asO+uH#!= zh2xuh4ll}L@$?+$FZy?S!8QGRJ-VIAJNwJ?0~&8UG)-K{aB+{FH6)X4SmyLuP6)qn zZqGppOE)s_y9Uxr*5$ltRwr`><=Pv4y8QZDhtQy1B^moTL7p#JP67H49)};~em&eU zMPXI7&n&hxBeJwmk$yV&)BB3OEjyCd1|_VB&Hr}?gW)Zk zzR7R0cj$`2_*?Xx&=t4*2T2?3YZD~n6{OXt?+wV##+-;YbFVhINX2IB%34%+Er<+_+%4W9|TAo|j5IFCguEhbTMT*9U6?eaS6r%gWny(8a8^kxUGqRpKC7Cm%6~h4G1*cPI#nE3cO^sz z7JX=9b=s`icLIe6p{E>L`PH!UGG;@MjS49&V6@vnN@_Xt+(Hv+(CrAsBmU+g_&U7c zWjd?Z{$!A!p|H*IK|$>4R7l-IpI4`mXTyd+m^Qr*L6<^Rfu7TroheR(O^^pYJc2Ib z#a?{}c0rIT4(}`VKHLSn1p&vep~5&G>_@v|T044H*o`FE#cM$;CJutcTXh)8G(d@f z7G(G?-h15D{1<)%gSZOv^*wSUa3Q_x6&^%*4nTrGMbEt&s6zM^N}rr4xf2Sw0Z>_Wq#bR^HmK02z#gb7CDPhf;%@jMze z9~;a-*PsI+ULw~A!A7${wh*>!kXIgX9*i=3(0wS-hgV&?9V=OC)5n>R9lgl@f>r`Z z9+*QsT>GRC7$PpnID=TZ9nUBYR41rfSxqb9OhSJ{Kpj)Uhe9`qmf(Zn58>^y3J9`; z(j-xkM^}X)5n>5yD+s9vaWJo=XG2cFIEcGNeKZKH@x?13dsw}g;K(Ge+&(Z8;{*wC zJab5!7^fEPu(Xh92T|w&i}oa;v^^!r@@mZjmM&K1V7;mJjS7$gmym2nYgD5YfCh+_ zP4t8h1a_rF0S1{yB%}0b!rFz|<-)K78D#Ji61MO(D8P+&38BP*Yy-0tsVLCxbX2oZ z4-VutNL=zTVTEBK4)p#Yh|(jgI`e=qst>D0!1RZaz@cE?(PjYyEdXmh%Z2MkFU=WF zqzC#+R9^}M&lq@*Vcy{HnM1U0d(cD9RKt;xK9Y|GISt9?V%o;L<;I{yJ(nn4ETFd9 z`bxnu&xTiD4oaMKs~7K8L?rz6bDiZE{j|mLCgQJB!**BY{_GISj^Aszy+#nFq)wIv zx2!Q{O{5%Plt_K*>wlB_v1yD9-aVC}>SvYF4qE$-V-;jkQ54(0^ubU=ptHlY%JhC- zqNjI{UHi`5jlye12^aUTpBvYr`jRez{7IG>y9vy@s);Gt96^(%h=YYbsHF zAvm`E9@gJGz+bxvB1I@?;0yHisSV zvYneRc}Ezx&!5TAy{*ju^==a*?dV%YW9wr6?MSmrw3W`Oy}_;nvd#tPQbN~mRmA(H zDKn24r6dhB`7N2qy^!-oe#_Q{Up_AC?YZFjwqd{L+ooOST@Cv`J-4W!_xNy@{~zD4 zx+G(s)b{b1=NPxQE;hZSQea@6zDHQ(*GUi39_Jmt^$B}Q=xHw9y{$1QDBzmo<^g}n zuLhzC{+x>Qi=U(q^yEaEX3cr`ubMA6wKVVZUVf0t3Sb=W^-YO5Hak}s==9PqDR_Ky zP+jC!~WICRlzOY=?)jWJvb(x zZLsS;`LOq4kM5TxI?`WuvY(1{a-nV?UpFf3GJRTT{q5w`#y9TK76l4QS);h6{5WTB zxyabcebKeXmrnDJyr55eH{v~V?9oo8`MICSza9y43!mq8&?BR5gQSkj+;ce;0v4r5 zt2Wl%9Pt3zc(lqX$cbfFFu^HE;UZ-9Y5gt+NpcuA^lsi^NGk}kLw!wdib1tKAEiXK zwb2H2L*IA+A}JowZZoL;x$W8$GAnXLJ;GR{j49X$mN{AOMF!oyLN&Ko~dTt0IZ}cy0kfB+wmjh8n^w6kV7gABlewEQ%4J za#^*uE`7?(sgrA=x>I;INW$d|S0jG*8Pzi-s*`6vn&mMjBV8@vVn8OL&^7Mer9*?b zzfVBAQaMvj1cAodk;o@IMuKS)D2XVKraJX#g^W?M_o!G@`#0ZbRU?Nb>N#R85!Y(b zA>L0-qF;n1R43AbWD;~TF%U7Tpor!N!6G6PaC9u0qbGp4i7O^mL7gEjDmI&8y8cEO zSKHHw!)4kILf07;%Z_4#H+#Imi;aY#S~93#grf4waYTp)3iI^>&jgf*(fE!W2|{#B z#_+U1jQ*hOEMX57Q%IpukCX^s0=EyhLm^e=Lg9TN1GkEFYS}S}Mi8U1($!flMAGA^ z1K0=AgLNdxmPdVhOQ5X;Bub(3{X~D2aojXJ9F&ucrrWfN*5Nbw0u?};C?gG>1<9!4 zcL2Ezy7WTkV+=Tzp%b1 zVNb0LYJT(^^t=v`vlClpuVJ_!qWpBGPut(Q)TPe)&lWFL+1_73`M3?zMk-bHZ*aHg zGpe91a72W(R62Ek7ALbVQ*uXOINK>aPQl_<738MSqpG3essH5$x7N#@9B*O#HCC=N z<|xBGFSKI1@btJl^{a+!1$0lE!L)5jfx^Alw9nCQ%N$LU&wHzSFAJ)iC;cj&9K4-o z>=t3ZL#DqHxM}m;s}mLk^SSN^7RXlWnf{w+=c-8Yc-Av~$|>$57$RN)kItLU8{Fiu z@S>M8FeU$uNv!{BVbkePJW^I}6P}=pPaeG1ykdXfE{ZZX;DvE_;XwUum(rxDhc&0> zbSA9sS|4&z=tupY*S5m++KAWIrv;AkWO2;NCRrAl4z{89-Qj83-&1Z351m=FJoxrR z)2mVEe_l9r!0=&7Nq!1<>aRd{-hFarDWCdvdT&fgzR=*06mH1(m65KWC-n&jcJC_n zps{`s@Y^igOb<}nEnudZ_FNajrOm$Sn&-bHLq4twIHmoTO4c9>4j z_uxgDWD#qzqi!(6Vn?Hb@#)r0tZ?IHGgndc?+@>38M;4k*4QR>o#P*ojy9XW@q>Jr zHoMvD7O`XfD{lo3)nD7%9;8)yH2$kkHg5idJ@@rzle#vXi(YxwF?OZ-zyBrMxVdTH z(=9889G_NiSaIUe#+FbJJ{5eBMHAk;wmZlFNVBMW!VZbiXihz>(31lX&PzZeJ9B_ex7j#^vm@eC9MCxCX5 z!iGHtgr5*A^hXP1LI}ASbU-G_-cGt13zRn1sK4~ zAq1_q9$c~Z5210)PC&^GIpCuu*@9WHyFjUo$=uH{1H?jb3V;ocy(vDBr)eN`gJP^| zty_e;X?~!MgP;NU8ZHv(slS40&O!ie1bO3`R0wup)o0!7Uk}0baTO7lj=tj`6n;%WsKcl&e>HrglbcEm;=(3P?1kG?|PF7Fo*q#|1 zh5`s24eukDG?F8u(m}(Fd(Vlok^qz=9Mn8U!4jp^js2|=I}_>&LP;qox|v`{y&(o) zJ4zBquw{mduow_Eo$>6#eM6n45v0OrLjnT~ggAn31+W>SmBnzP2=JThG(BvFazpGE zK#ByEtQy1(sh|ur;2Qi0Hy)t=5kJud91Z_1%)JYx}@ce+3I1`t+CIL**Z zOWrD}hCCYe{Ai(BgSysm(ZsKR3(}3>}hIouSjatyyghx*&YQUcoMSYXOBg zn&z0DP9!^f7$bauCQ1ut<|zmm7FYTKS z^x$qh1<7MBa9sg{DK?3zi{93+To=A@hkLrutA45$D1DPlZX4z=FNKU>zZoyLRZez!1Si&S^10|O{$GNyy2a<4f4uqQZdCNBSmJ>$}p@S0-T!iV)K zfvljyw%fctz>6E8UA<+|uidivcKD9z?h&@l5Co`FSVj9-c6__R{z8~}Z9_~(n{&ID zMS4Kjm(t7;#)S~tm88o@8S$&H>~^%8U(>{xV-A`38v{T0JU`sx^jynE_;6NRG}q%~ zbF_kySG-7HGMy3mP3z#Ed0g4&Hm0Vo`&_TE$5+ap$*)hI+%Y7|S{7H3v?lE9i{nV) z8-$*pRdNC%=kn6SLNdM|IOW}+s~bgfG&$a1?D}t;uj>wlzV@2AQ?PZ0;_)2PMtAKs zM#-DZK5MRA`Y5ZXPkeYF;!FBJDSu1|}2z0tIq z(sFI}iawz#C(*&ed04rGuL@cf5-9v8ce}{gey}#8KIB`*Dh{h``I)0N)nqYOKepbZ zwm8VU$0|z}H!|mi;aSJ0!zbo#yn9JVI^X)~W5>5mXIX_a?<{Lc+Lowu?|Klpwyd?i zmC;jK2XM- zF**Ns{p$EzY+#3XFiz#QI1hUC=q!rA#Yw16AfY$x&>K798_y);=_Uyh_-Zl5=pZFV z3=2_bnZT8uP)*UANp(f<<7e2}a18x8w4g(@p_#>R5Fds091I-}J3YPthK9{+RCNUX?Zg|9XrVI*SPS5QQIG&e^$`#Op%WpB2?*K+l#-_*3^;F8DBbv3 zU@#DFpaM~vEeY!Jp(-v|7Q^m`BsSRl8nlOJO|l=7n5Z@_YU>B-a!_%ahvNQ7TFyx? zp*E_aln}?S~Qh z1@0WE`PE2?5V%6X4<`iLi(R0^%P`2L)@oW@xgM38;*a|<+joLw0y&7~9ukNE&Pb#o zr3Bv!g!*=HPC!?pY9L6aJ)O>DCARvt5fZ0nF<=#u)ejwHpwOZJ46I5dUR(ooL?BAw z&|qzWjKH7{au|s_jR}G-24qerTtQ3*KBU%1-^69LHM1_@>e!lr93w$RtN)LOu{DA zB`9d^AQcxBD{B>(A}SWF*1heto$q_j=SdLTneT6|ce&m_AV9*if9`Xi`)uT?34({& zgEDr+{eU#1GPo{lxsdftSte^RYjK6yVIHuElJvXMp?QI!B={$|sKIzDbZ#O(hS10# zmI7F5uxe8wafo)9HGsAYZxRhQBsX#9N}))M`ZE|aV*TOsVE_J(CJ#QEIu+UnV9PBX z?RFS3J;Y*xXbb$L4A*6>qyo$q+~GPX1BL7L0~*%25@Ir8o0=vgF!S?OVV=`st0jAr zl-FJJA|5T#?GN{y{JuB;A89qQZjnYxx-tsVeIw;tzkeFhv0s?VYiqL=*!z_kh09Z! zR$09T-V>AjWadtmezGq?L7P5QuwR2M^Wv8;mk0mco)YjF= zLj3m3k=d+Wc+q(uXSz~6kmB_;s<+BVGf8A~dSj)-lBCeJ`zwWCc=p#Nt{74MLYY5P zJB@xi;Y5%X<6uc;V(2g)&)Z8Bl>c(;htD1_*w)m0WxV{tykAdktTJ22ckRyY*j2jb zhX5D>7JGl#v-0H!uFTq~buA3I{HWIcrN#c^rrv^fJEbzf_gYHWiF?Oxv^yPF$WxkT z*z8!HP+gP~+5NqZjP4cMeZS26*czXt*t5DK-DwD)qqub#LWQlMa<#NuK`W`i*Iq?JAW`g#mkyhb#~WKImxF zWv^Z_z20McVpCv%OZKJb0WR>U#@Wby+VO!|Ly;#RikNwU83Hgcebe@&tQR zh&&SpNP-pmCybg0djwrS?ZL{-lFy-7o;b{7Tev()-Ne9;!rH-qN5fPHYXdC?93B{^ z+hR#SSUC-Kz_IRzWhX+`2@97CKT{?StxO=vfw8m7m4oT=0jM&`3k(GpL!m$@ zD!?tF!$2|wfwi|0`~~+8RTD!4MbK=hZXcL_3u(kLeqA8HFdz+=IbEu&;*WNMKK&7o zgYz+W7I>-mxKv2shQ0tk;QOci!NK_gOQ)u3SmjfYBmC*}_jlhTg{c|#G{01pup+R~Rmtc<|vNP@78 zIE6XXR6>)OBAkvuP!;WfhPt}s_^IMX%2TBbFVJ^X!H6pbrb+LMxS^PZcV7gqis1+N zAe68hjmExrjbKW^sH1S!WDPq2!%~Vig@cZb!Ge%i#@jP9xReH=44aIBQNRb5;6C_4 zo*2#+641j-j2e_SE@cVO-Y$F6}BMEONvxQLNN z?+Ber>ryaMbUCs2=qkZ~z;%F~(!e^!3_4UICS1R8Ih2H=Q3AK#X)0Hm1bN@U6o@IK zwl89$+6LL+bx;8WQ{1VKgAFrrv6*z+a=uVtRKGLD#-_JC{=G#%C#@L4TzdJ%B|p{F z#9M!ylSGWU$Z<(4j2WYpN@vIgW`3)~!-Y52ad&&J?#oyD9_Gn|&6@mOdm?haVT+`m z(nEne&rIfZeJ-2Om+NsSB0Khr<~x-qZ1;ZaZPvGc+Gw5%B6$A&rYf-#Ewh^SoZmFO z$>p`m7E;I_OXgklUi=<=8Pj3!<(Sy@9d|BK#Leu-zqk_|>iE09VDw-&5zIaGA{Fl4MpR-=OJ$$yAz1Y#TXo7U$0b`!O zaMTnrd*8tlih%QvlkwLek$B!~WFuDEbV#qXAWyn0q{on7{I3;QDBx8o$s zhuNjsR*s+Y;OCZD&zkFxio?b_TW))Q@00;!r+G0|7PoFzUUAROk~$aKy>Y(dC~=GGTF&k9^6xS>vH!keVvpsja}zznWH%Go4mB5CzFM`_rTMv^MTzv+ z%6rd4Hotix-BEHQ?!;PW7>QXJ_{(j}l~?RGUP~O=cc8^T`sX7en@TNblJQEfFo92A<2S zHV$1Osdx{R31^fl5@D^3lLMBkOnMK@*ZvHTn-kX>R2Vf(v4duausC*?qMu>XiVE;Z zg#yfiCFQOtw&B0&g#{$EglY?cS(&urujkc3 z+`$e2powu=ispEM>54<1DI{Zk!B>G29_RoM3s^6#sZ5mq*ehEU+M&Z!0gg-8iF>h= z7^p<7ECuB1aQ;EF!tV$jnDB{YNA!q^C-mmq{8)5n~rUjC(0I))oR%b!^eE?&~$KjFD8y&nIgXRVAHQsiZ zR7Q;{q9iM#T|t9$8b)Q#>yM4f$W5285#}fQf(z)!wqVz)r8W0HFfElH66R~4&7;IkO)e^gS;x?e;82VW| zg9Pvho&&#UDu-dSz%};agDnZ4RIh(p&)!~Px1fbFF7bP3mpdyL`S?~H`bFL15Ev-5 zwq3$yIm>J|*;mGg*WFf!xx2mS%l^GKLKHvK@&5G=?TVY{Sj-E}znWCcj5r=r+`FGO z^IG)tkWtS5Igo^Za7CmwJK~^h$M}oh6K3)bCDIS11nK&;5jGWb-0am0%!FZ`M>D(j zU9nMps<9szvDHD=BVhF~$J%Cu&5<@*bQZ{y+v;Pt`bm{_Zs`-ac^`Hci?}|CrE}ig zpWyPri7k;m1C0d_pFoS6u``@!1e%{~>Ej|5fK z2zjkm%dG+nGK1EtE=T^HRaKbxNC`%w*SvtmY0xxrRaiskfvOFnns!smsKp;4ehH&d2XCz!K;)uT8Ss2bRD#Nds@v=*G$Pb`-3eqKy;G04dJT4|j{5R$vyX05^_D zii;EQ0BY!9&4WVlIH<{q15LsT3s}%FSFM5fUqhJXOObJKiA!p^L<7-+iP$(0+7_c$ zC8K4xTh3~?wBJq63~?2{L_?YbfKo*O_`mcJhB_AvpuXT>5X?pSiim6_{Wzw99x_|n z(cXn<3%Euhn*MZ(P-=sTrr_aVkjx)itcl5JT@FKr+K%;j5p)7YR92Xv&{`fEuM))yYRYXw35kpm#;TN4L>~-*e4FAg@NpXZR549B; zNFW-)Vo;9~SRy+(%^}{*Uw@{PUR|ZsmDZmWrV=PHjh!mCCEAbmVYpV{rNY23$_3Lq zNrZdz2{ebJH9(P$3c7=vtRIv&HZ*pYMy%6FEds|Iw4MEfjgWnmmGP!_I~wk>I) zZSm$goi&|vo~_+prwXtL$y{LcJYq!TrHZ}398&CGd9_b%6&c*#S;0;|mlV*mx!k&M zZ|IEcGjo(qdpAFR9I^b=`dJfQ-aK`gyt<81IrYQWK7rBeYQ5w&%_$K4I%k(<7U3U9&G1KVS`$hB8!nxnZYK8>UQMiC&8a)GzGGh44nEMN+ zSKI>|>{|QZyw*SY7nezH1(>Z* zN8e^H)|UUee`ZRP=jM%j(n??KP09=|n4gqAsx!ntx8C9KqTQFjjoJC&+a;EaaWe~l zir7~6xnI9{%*<=D?5pu9!G#w;4xQjTtIOiYprxIcwUfOA;;+ftyb7%MzofBiWkul& zM_OERO{Q;W3oqwqgq9ZW2SbBR0bshLkyW;&{CA9%@6rCxp=A34YGP z23-nRKN)6$qC{030-XY?o`{SgLbQT7Iy&w7uny6p2vBM=6L5(?1bcRXwPolFYrqKt za)`v15c=0iD(Y0r;EX&5sWtQuK|`dbWF2EQPmA;P06=hOlnHSk*=C#SWC!zDVOWXy zI@DBy13~}|X5`ocYwdK{t`uV+AG!E z)>l=gveOX*=w5`dbm#5MyVcI6@ffZTl`VU>#}^1khU6>mU|8k9i`J{DN>anWza3}< z*OtUUCa1-~JVF|{7OA|~VogH?aW> zA71>yFT+Ct6yD%*IFVBalk#s~p^cbc%c5)zJau>)jN%z$I=EqOjKT8+-vG>+-ynT} zb-{o(`K=fuqRt&a8xK~mN|DXt=D?%XJ0D4A%v0kNC0um@vYH#A{p?qfSSf6|G*j;Krxg<4?a1 z{;l0;#3!${O549DYZf_1nBTt6+%DTB+aBVp^snd0q{)leHfj8IkdF2DRC|X*ccuPe zGxJlm?q!(}!8LYDmd(x42v2mWBhEEwlGM`@#vEGT^6{t#5y=A4!{|%-vuwY}T@@-! z>8`kJHcaC=WxiD2UVHDc!=$5K+jTdR{B3z9{*kx5yGtv^7&Ba2juhYbwrI6YVCQ?9 zR(9oWmgGhC{;lwkv0IZv-k0?L1@5LtF0Hu8PE8n>){&jFX@T8+<%!jEGQxs;YEtGV zFEtNNczEVjwXd4FxhBo#=vmpT1(kJI%tyJ6{(4MLac6no#hClvYt{-=z1@A6PY^`^ z4{h7=QPx-Iea^kHp}t)c(iKe+2qqT$5P5K>mJ6xYFTk4 zf9`Ju*&9onI0s9rmh9mDZ2!e1`OAR%{9&8Jp9OlyEsoiIXw;dfM#sGOrHp@(UEcGC z9cV6W>6rTH?7qI4yFYGyHE-{>oU@y&(h_#G2`36Z59NEV`fBtV4q#mxt}6>3#OZ>C(FjKFFk*9%8zq?V()kSIzFyi_x%}{&iof^ ziZUYO7ELNSSsc0YQ+eV2K5yGO(Y+NmKG7!@U-hjnzrYmNp1heELraPf-b3o;2%5b? z9NuckpQJ*`D8JZx2y9f#gwl{{JX3up5@bhNx|yWgVq-!x>{o~buASVil!6(#XfcS8 z1V#e1evo>ky?-Z00{Xr{^F}3FkXC-y8+F-4C@Vj&)-!u!Lh21@L;Mi3(iOxPxlVRL z@>5`K0!IU*0`WW&|3j|;ya<|MODK>B^0zr)+8Iup<)$03pAJokg*YKLV>fT20j-)pK%@RV6M1*WIN=W=XR`tp(g>d0IjV*Mi~ANniLii`*#)Q zzNSpZ&*j4MTU>g2c#bW;~hnYmkx0w=7~V#X@bkn z7=yl+RNKBA`kKK$nAxc0Llza}H|Qe*5Uh5iq<_lifX*izn8UFZFUOz0@Z1Iyjtxgk zy}H@se>YP9Kr|}5QrI(f^&t^6;}BXWkY5J95rbEPuW_WJ$gn$O*u9#xRx#}3ugT4z z4WVm*_Y&fD0NDnMgO(E0AxP#GMuu^#V38xZUP-cEZhJ5SL(&$=c&R*xxv#THOQTdktUO&v2x4oiG zd)C_8^`D)AuM4-N_cyTRv#$QpKEM0XFCk+l-0WEN_}Iv4&t&($d(v?4L~OFZ$Af^s zxsxXZB<0D(ob-uK$$ZN&U%Rj}qN6%k3%~7y@!7kDI zyv(cWtrX_zi&@tw~X4zCHtKl=9g01SR9&W0L&Y6oOU5R{o>h)JmL+MKb^8G=@#xd&p0cz2tM5(d z-*_L|g8Hp)T^74KnN=`h_v}X(*~Z@_UaGwC@d4+`^CzSCXL(w#y!QQ9CYqO?H@{^^ z-P_e{s`Zb*k@X;QSK^D#$YI`PiNj)+AMa++dbLXwnaXuqAw25ugzzl%xPw6CL^CPC zY#Xr@d(GggCX$FH3tE1)`oW)+Q2&M^xPqo2eOzooM~|a~9aPjsQ1mA<@qx4;5+XKA zBtZZii%blH32u$J7R0M~3;23~!?(bWz}B;1=}>8?0wO&Dwi}?5U?&7Sc*Ic|CxEs! z%k=L-hcb?uSOUr{WwwzBN}FvGOW%ZyOlbizZGakbYJh`5`pm;qYwya0Nr-R9w^C4m z!jz#WEg8`u+Vl5hodm)OBDMi_k#)}5WOdZsXu|CuXslWFI=es zi2?n=i{Rsl!52AP{HNj6`Cvs2)x{A7oOkfJPG4=I7j6u$2qb!$^Ra=>;I99gcoC9c&2wCG?a03@suCH_eJuj7Wq{QO8Sck)f`|(1Q*9 z9O!F?rUgw7xCI7~2Op&bofxnt;Kie>q0_q=_z4Ycvk@!^JxRaQK{E&ILQ~HMn_?Pk5%u9=+S2F|t5#&R_$7^a(88EL zOk5qPZ2juwImf6ZYhNv+BmH^s6KEa7YA-wxBwSu8Yb-X`T1IF0WfaEG+`V}F{Fo|6 zdP-bRnRmH5_d4A$w zmv-b`p!MZ;<}G$>FwtaP^;#aO$xf(lj2srqaoum3b#-E={c+j@dHwjR3(Qd3vP)3w z;dxb5oZ%<_Mt(j2PIOYZsZVR%+KB8=Z&bvNtS{sFE!rTP?kuiTE%5qsM8tO=luz|q z?0xG~ZT(q(p|QDX0xLZwa)Ie3z5bixA|7ark z{}M6-2r}|}YNr$g=21MBN8G zVZG&|E)=B~(FOJzR@Pr4%7X|cOcR4U1o3hRi~xUBAw7oTKn#O0P-EDdj~&twptTc$ zg9Zv{G5SjaCJ|v(Z;$jUy8fYngQ2e1+gCU^{ZRm=gg2=OHU-Rj35JO3)D6|)8wr3= zi6kZuULfUJDBxkl2l79BnL+1aA)ujwi2vtTjs6h1Ao5#4gae@uDF(=Fj8?uzO~SF( z2E4UT{L>jhLw|A*Xh|q!mWwETjI^|*Xgs7Cf&Pbq1)~0-Y6IaB4F3X8$Lsbd5)J_g zr0a3wRuQa}Mm!5*E(&w8&7|S@RD;!x9`i)tG+=riG$##(PcVt#hP6>Xc?x#Z%r*8r z(@S4x+B6&qymupIactgZ)q^O(of^MOT9b*JA&+E_&AhTO4>(9g7;TvMtVq+cs-YL&Q%3Qenm{50R%IEXbKGSUvb(!)sCSv9i&EdH%CezuGW;wBMs*>*I zSGIgJO1S)L&kJt&7u8Wwm--I;L)r@iXDz(sCnkCA!XGwimOh#m#lL9Nr=4fM*}G-j z?IVWNS$Lz?3>Wx$#xAuUGaR%422AkGT)?t^EB-A#-7G`oqxoWXlUg z(>|CW`)X}No9@h_FWI9n8LjNvKW*~L_jh~59!@CzzOGGpGluc-^4j6^tcUZL#<~5QYu!P=pTS7ladjj2D zZV13B06J9N>INX)bC5gRCi2wcF!&TG9BQ79;);k6P5==!BATFe0AU5-T+X2Y9C$qJ zbO>x2z#G%r6$IL;O#mr{ry?t3r(V+`4Y3d+O@L*?X(`~-WGFI825JXk?=()&$^gw%2e~JSB+|K80!2F0lC*!ZXGE@lEYW~*>S&O5 zoSzi2Mr^yg9OgwqUg!jxlZo7Nuxl66>2^pEiInlM5^h+WVaMqjiaUi%$; zt!cZ>wZQTZ+g};=Qz0Da;UC!+zP^b8*N_wCapvLoO!&9*4}Xf zm#vkeU$h|?%Z!|h9`*Ge9uXaWjWf!_&dJeZ|Hp~t9S@JL3cYY+pJfh*(NcYs?_2eF zqAX$ujr$8D!(zC69o$wsT&j@!oo-nC_^JRR$G1mtG=^U0z_cN|2Bz5^k;{FCL9_F;S~2AiAg_Ob6;s- z7&WXH6G%JJD-1xhg>mFGaZHVW8<{;W34!7S5b<%^1~oHKZ>jLnhkPDbl>{as0d%4c zWg7|(5C#SY(_wV9t?038mgp3v>CQ`P0j0fRB&QJ5UW&GPkF(9Ibmq}6l=nXg2IT`# z8sU{}831`u-5w%;{IALTZ{3XsK_jq?-i~1NFWDL?*Z+U=aYU;bcgg zbZerR7_wdM0HVRsfU6HpKmf;e3QYLI{_*%9AP^NXXMlG|<2EX}jj$cRApUTe?Gi&v zMK8(CcsUrc9ZNjt&2}9ZaeTyq>lX664wc`I=}9z_r*dxa_EuEHuxB>(9Kb1v4d(S&INqq78G1YM44Oz(|=Zb*jn^j5odHVwkSv{SXLS?E8E{VnFS*!O; zzg`wrwA0(uy&~OMJf-4jM%#W}U(M>vz5AN2RU0@8$ByT>3OXXLjBSzBjM>}x&7|7! z%Ua>x@C&|Gj`_ZoKaENeG~3U;``ekwc3I6~x$J@i-{tl&N29h~TV0$Ae|)~xWs^nW zPg$|@ulN(VkFBBS#MGH11-4fv7paW;CBmz!bf%x$sJm?5xU;s$ zO05=NIX&z3HOnQx#Wnk_=$@aJ`+?W73(`|G&y}+)%72tRd}O4FQZ8_7=y{{E_^oHd zC}rQ5tV513YFih7{o3xCAZ4G}s6IsOd4Ri|?LFLTg!Iy8hoY9cC)J~Nt+=(}k8iR! zXtnHQ>2vycV`a5t5u>Ok$jc<>#L1r~d$0cbVQ=MHm%a=$+f?haUsw8`)0m%s%1*vL zW~u3Al}SuA@AwGI^<%p3ik9ZAS~RDP?(djcJGG3KHNQ~%PsRO1b8_2SBNBY%B$`h& z0SE;SQW}_50M$1b4ul4#B+CT`&0&^{zbLKEmh_qfs!ZaFK!5ck&2YXb&AwFbAIyZ& zUTcVyP5fxU77!g`#TZD$Xd+~v=m%I}y^m*M%YYEe+A%pQPr>4Xd!l?~WKm;UX z;1vqMV*^>EUg6k>N+pmeNj`>!OIaCaphRH*hr#+JKDgMQV}=K&XCWc3breWll5JODNl88%^Z873)(4N2wXqs3gy#)w123VoS4{U`8Jh%{)M_C`_ z6ufotEJ3hR4$bGAN+HvQ4F2Gz1O^4eAo(`s3>yc$Qcf>Ht$o<+^(z^ghOzim}1 zCU$yzTuh@~_T$%zgW4;=*sXcx!P1iL$%_N#-=FY!|LNq|u97F`xJ~g_Z$9c$ z1|-MRwdLKvsxRH;E^J!3YswX0kL~P*MG>LDPLPIWEeUa-SX|&In5^{6xKySZrsRhh zZ~0fu54_!+uPPTNyM7dSY;&Ily=mh=*A&U^^TN5&ZKLYry6EGVEl{!d9toK7-JBiF z6$3}t&PiHWR-rz8=*mHzqd5_9bwDg4-O|jaW+?KN#miSecdPofE3({CfF%}={s;h$UK}K|G$Ln zRDkSI0@2}WknSOksXrCq$oGUm zjG-P1a;2~f1=TPK>%@Dh@)RfmgYwrOAzcP4im~4hmXt$Ptr4I&bWDMED6vrD$c3Wz zvOMG#a+q_9?xHDV&JscnWqGhx(+_Jn{X*_M5e-T{ae-^Q-1T;q&e3E zxEw~vjdNuM7w(0Nv${=xOr#Ioei>!IHYt)`5%lM* zYCdIwM+zk8OV`!Cu8iYvRn#YvQ?ZSpr7h$YKyQPDsVwo&;ddWLBtpz*Rml)ObTln{aM!H|y`kRd_N z7q%@yjKBzGI-;R9q_)6mQghu6@QAXIijfJ&Vj#qN0|K)Akm65tTmKW^_Drp4Q{|NT z^LGl|c#?`8{^7Glh~3H}>3^q`p!4#d(+Kqhn-W-%m=9Vpyn^@zM?NTR8|=Z*SMt03V&9UjSVwN^mVal(AUjj|zu9 z(Cv3~9RWpA&S+u~gTw@h z6~L%y98cO<%;ZU$a9%l|3nMJ)p$vsNX>WjVjy_%zkOc-75Q31xqQQaGL3$x+b>o5U z`H}*-Ffj+~?Rau&nR1PhtrUK-A08xxPow3a5WG~eUJaZ)9m191G!TmdylMs+N}3KG zY2nNh!4`7MG>{VP_4K>U=o5mvgRaRxrLS_Y2?<)^xcu8S+j}CdG@+KC2=}u^iL)!T z&e_j}vUzi~haJlE=n%kcpV^k9qc1-h5teV!x$gY?vUR>HGiC5em$`o{tQ_4kGl`LL z`$S)RaIMJzAJW^8JC^NCkyo8z6j!$x)vphcM;*}4>7qdK70&8uX;a2Jjo6}G6}-UV(95$fA9$q{Uw2iG zdGXB8`=g?&QAZT}Rz+q`jZMln*r2uw7^q^ ziPjQnh?5-=CU`{}7)j9J5u&db1$6@$20X)%@`sR2V3wc;;+qpm;Xu9%d}01~X^=OWYE)D{x`p0mP%K(B1fLAdoE(MzXp8~W z|5sEtM6*C$fNP+&#Q}Y|(f<;Z4H7H-Gj4!L6e0i$>SEyq8~hp~5mV|%98^_e#_~HF z4bVcE=ky;8f^Kj(uq%Kd6hIhy*eB!k-tKMap5bSPoQbe0jFTfx-U{7WsH*}{3xXY} zc6jtN;G)46s9y$?0#Uudoe`q|PZgp%7NAnb0kZX2<}{c{9Yb5!57Y0+DuNx7|0*T3 zJ|)``h9~fI}XUHjs*`NZ*ZULEm9dwBXBSR|#9zqM_3IK{peHU?4m}+!Jc#`Oz zFy^=medV^b>u<^`Wj3YO0kY|nE>%>Rwiq4TKkLKJfVr-VzO?ohZTUPc(%C;?u}#H6 zD;I~lO1to_oMEiHNx~4?%(Jt!V7<`X!u#RV)yO7ogce4&5e#bywEJ3ziZS_D$eu_vE^k^@S(r&=}?a?3ivFFm||``>#v$h`Ldp%l&rMVX~GJ@u`SJ1whCzFKmvI72))=n}{n0{zCTbn-PRn6bT zt=m+&?yD+ib%+DTuRk=?tjX)jCvHzy7eBBnRecfW*rR+E)Z#f^krlQor}KB;);^($ z=_B?L*~aN*qFZK`*Oz)lWt!NqTP`}D{OKg$Z(?o5C#kv9t2cV`q`J;r_udjx4iVR8 zc!os)9tZq*2$tYxqXGd!L;L)grR0&M5~2aOjEMXIngS&=P?9oHE(l3B$=3rPajQYZkACDM*=Slv8I$0Z*pIIX}zlV<1>gVJWmrX_wc28BeRkP~*?$Dq|a�ZbiUJW7`0+*<~!)iZPu94je0j^;+eFO@s<#RN9u^MCtOA~ zRv{$Vo(RzJaT6BOeljQzx-$;O3H8n_$!C!i9te#w7VY)yFzYIdiGy=$h4JZGq*?O3 zYFa=?^A-u$0GxxSAx@7RZ=1Wo!X6ox7Vg&%vJJ%bhjMpvOa^Rl8U(esFk^~(bR!%e z<3D{BQhYK4LCTWX4hhnbDgo6qr{UthLmgq?|LJ#vJ8+FDVhTPE0c-t|Hm)%zffN0@>`sI!)PY@0Aad$tR7Mc z&_W3fr+)~j?)4))iVXfZ~iSlYB!VfRJfyszQhIM^KtUa~?KA0X@R;fkhgO zz1D$ZR4ZciJa&<7`2qtOz+Ct0^Cv`G4 z)I3cx!7yDC%u~0g@EbZ34u*I_%?kv({cy)5GJ*8Ahzj`v@nAu22fPIm2C)IKmgQh5 z5aZ>LdF3YX+8(oT`KUwAC!Ff=Gj#ALdZdXc?71 z;;X2gW8+Nna)82Bfqdxz&Bfyz69p5qpa60M~VYuC&Et{P?JcH90#flUihp zZn|)0%KrGubz!}2fYBN^hN_|3amP}NaEmQb)fbARR(IMj+ao_^+q>VkCt`pzCD)?8 zK^@*S%iNeIDTmIu(6F4>2*a9!9sUkYKw5=R;I!8+;F6`dUbm3-%yk~?!V-?r_O4ee zR;-LXH{r3u;#s}qplgS>@5TJv8@}}IWVu~gy0N&w-~CDExhn?Gi| z*V(03|Cn;%Ygx{#?{&^;UE>y?susM_POctf)4DU_74OGAF^}$_UUs*-Y10;Elu?Az zE3&uP_pzkyD6B*!NUBg!;Do#mD#{TfL!2->UY|^d+&C^A3pPkmAA!rPOca3;psCjM z_|%HvKZXSAfKfOh7bTHFXv+nO)CLhFt1xqjrh!QpG}b^OPLzdJFkH!nM;Qi?z|<4W z&4mLDMp;_91~RpY)PaCUK^a0TrCN$8pOi5=5Q39b)da=qIanhO719vv^WbVr0=q$Y z5Wry>vCte4-2)Cjjua2JQR%PH{GNAKjbX@5^w4~Pk z2{igG=58(F8{yDQnO9f(BpDUw!z~&ZogV3NTkjHF&TP&Qf=lGj!N{orxBe}`<$-sy z?5#tN;=mUgnhj3HvQIJ0(88%!y6*6{j9r9!g4tq_!PFt>Jd~zSX$2M<0}K2=GEc+t zO6h$W6Jv9skcD7~K5C{|$Ea?;&xf_N_?OKw<}&GBgLvC@q0S z?f`gT6n&)t73~Z{DMN3unD7UfW5kbuSvhFrq;R8}{Ps?5-^ zAdTZalVVGEai-9WUX~ed6lzjrWdxH$VTLh`glqSLsg7i9m)QEpwmz5x;fUisasJhi zahIAFyZ4oIr1j7mlT7c~uVOmq%Pq0-rdnU#C}O(9Pi}VfPo*A^x)(y=QfKUpt(6PCIE$O<1bKp;ZsUHnUs4R-J!cKJodUG}?Dx{p0%= znc)V~=Q~)A^RvTTS!i*EdbG-QLqQ*10KGl3p?E^g7Yf zdEEt4Z{Iz3o@zGkoIS4KtVZFIZMSQLN{}6^ed|UF8q5t1IR4&Vb931)3SaM8XnBW+??zrCeGZ zu*58P=owhSIcg^N4=vX$RTT)*FLgPa4PXA9a=_}>eQtYV20rzxSl2wRqQ>LamV!m? zJ6oa_8^eq`UfZe^`jOwSm+`*oQu{TzJBbP$BUYU&>&v~|TU_xtW?gJ%cGpx{^g~xs z!zT}>ImoYNJGk%%F&VxVTp0C)figAm-I7;Fdv0->fHKzPz& zoJbIk-osp>u{;F~zQS2}`S5*zDSlT#Gy>Q)YR-y3K`=s-pMV94Z|$&WQnxk)fl-at zDYC#KKpJ3f+d=)=V4XbX00K%Qw(I9Q=~*<%DF&D%uxoe)xh9NGo$W&{)Q($y5Tl52 z?SkS<7|B|%HQylJ5!-I*=96yy{}1JbSdDr%(NErT%flKcPCvx)0b5c60Xnjj(hEw0 zwDbgA)No3Wodw-ehBDB9`?C(5aN^dY!6ni@UJgo3AlC`kz6V6O^jB>-2t|}_fDXil zPJ!reK!bLQf&OJTb+Ba*bcFOaC$~|b`-^w?-;2F1RHqK|xMH}i0iEs&;QXUEMpu^bj()6hdX-iHncq%`CVzqCzdU{Nul->a;9_kM8N5IXr z#F*(NZTK3}i7~%k`Mw#A5E)q4n%5b0}b$H%TV&0%U#N^QTDalJ|Cch^K6> zV5Epk#dXnff+#BwQHA7!;sAGU^u39DlQ|WWfS6iE+^!wf>aRS-;?3Q=#wst$cBg!A z0QVmEmej&eTv7g9G=gh!w3e4p#jR-P(WREr{!NkolBzJKZkA-=^fb1e%=jbTwzdR_ z=Cf?QgiO_|!}1u~GIiOg;K?itGo$Kdk7wlHYHXP19Usvl?w<70;=2;rKTq5VZXWlr zDtqnKM>8Et{%Eb+pR=h&qEbq|+xF%JJT-1N)9{M4_Ow22cyPFw4*4xW1@TVHX=%sU zE>{H$09~1#^^?wu_O0wXkrGrVkFwG2qo3(Gm(d>`JghVW%1gPB2w=v@rHpy7HtZi0Cdq&W zO0dZm2r^)o{b3eRPRLX^s8Co~8l+Gfg>+o-!^px>tC(u_VV-FWPP`nGFZbdzofAtl zvJzWh_kgd6c;pMkNq}rf(B0OmgfN#0;JkvCkliZqFd0gDK=I^Si2Oxh2_T`Gr@-oP z;Lx!68KCF+Q|p}w0SdhhIREa_GOQ}tElJ&g(XxxO00~eXCrla_vOdLDFENi4j}8if%4%}GF@XaSiP#wmgM$5r{4xkk z1fZ}FHh2vg+o?}Ng-btFxS;xD=wJsR2bYY|mkzBPV6Z($Wzq*u2UzWJ{usDEd_XTF zOp29~gbD(R2#o*ci%=q}j^r)288D7$0q^|Vx_*I?hSFsXwDMhM4B;WT{*-b9K8gWy zb#U|`sDhIOb{FEQaM=*wDcl4sBeax&A$5N!4O_npE*ytnC;?8Q&Eg4zIPfk|y8-Gk z2sVRq3%Nho0s|$7M92X%)PEqh;xz(e3HG8FlG2Eb4^6+!B|}ErgVG(XXF5`HE#q%59{a8L!oHQWWnO&xbRZtVd|X>j87HBmn@O|zqjTp> zYL7g;1WfhB98BO~o0LleVWOpVy|z`DI8p)B$3i56UWU+R2Qq`e=s`&&_V#zTW%{~t z*e=yH53xF;>`1DMCTK!dX=KrNtWHshRh}^4qiNAjnQ=_*$lkofTC2|UOI&o3$sa1x z*TpQ>J>xiJg47A|p`%kAkc8eP#8TpbzF(JZA)$H{ZO7f8fh)!c^mN{`SM z#u%8h4g*A)z7oGfcOsKRXcl~%Ba0CXBJL^HaCtUQqE*E(vH$ao|N_ zvj`I~TN1lOv;mzZ<#zq9lSQ2IkNCVPN!R0lx%)n{xG+yiKcgT<43zR4I<(|C&eZB(y6 zLRhIa`4_ANzj9=cj4$(;&mtr<2Uu9W+BrWLGL}V98iqJL6!u6$F8qPYs@p` zB{W=deEm1@NU#lVFg(!Eb(YD3oL-o+#HolU*AAW|cnW5BwiQC_{A1M$54E19L)~2n{`g!`xwfYm+tLH^6)tb@US}OW8Hzyv@7i6^f`vj*#?RnmI z;4o-2fM`I=K`M1^!*wGVUsABfxtq3@301a$RizrgkZ_5eI_w#DK=&ZFL$1hj(7>D> zT@T0lRaH`?pr^BZx5KVuooeq;pDLl$;wbkInK$oQA|0zwX)uK^bU2?8Q57x zB6gcw>(DleQ)U!mQld0=vuW}!<~-;<{^jdUYjS_T&ue41*wnPKop$ur@BaAwPTQja zHb*8bIKRipOI-JY?8S75RnQ}U0cuW&NRT%@L z{;`#B>Nz7~rx*8euu9I4&H1sT@!3k{m43g;UAZlJ$<=p@9VK^<8E11F?`1p|HLQ8c zTIpT5ZySvE*JzpwP!3NRqzgbgEk;-saFgtLqp=JOatUCi z7~`Kz6JhCEI)P;*XmXz7ph!evRTv`pP!k6a67l9;V*f(-@K1XlfVqtHeIn^9A6Fc_&eup*QKK$*Z;3a3mC z(S4(HWeNL(_~s#SLg=VdXMm{iPS_s)K(=ncJY!aSSIPfP9*I{RP-zgtp`nNLJz(}g zE%9#r#wDCFn9hGDB|uQ&1lJm=Qs~q zRPdr<0CrsZzIx5{FsPQ7qfniAQofar&jB!2G2aKVHMoDrS@ zJ=d{hnVtl=Se5}JN_N-UvCbsgjA;(p`(bz3*4$AJ9Ru7s=4t0a%RT>9M1y3MuwrkB zeL5@dAgk?(D&8bTVt-B>2h!HAq}rH(N{+0tmN}L^utKKY9W}Cz0jb1tX)h2#A=4wR z1G|zmgmD`)XcbH^yVT0>Y`6Ig1i6ntbEnuM3pwpQj{WWCs=>yF8uuO$?_9XP~4V~FF($}+pPhO ztY`$In8`Q@3Cn5+brVxcXFJR0K!hv(YO8WXv(I(tB6bUA&uLnmSU_ut&R35BOWPn# z2`SBe4>amlK7LOC{QR*^41zVIw_vT*E9i9k$QrKSvLMt>E_!jR^6spge&nCY#ge18 zk6jMF>gma;RX9#MD=gUilVmAxr?#)stwnSqvuhXKN6OtQnB9Nti|R4fI&lJea)^p# zA%CG^AU(vaQjD9!1Qp^rNasPWqJNUXi)JSuIN#psz6Ps?1 zfimRg95t9dffI{LS%XW5xBkyxxRb%UGVw%(4q`-3dQ|xHXQ|>3*oE5=L@}gg{S{B~ zG!3UZh+_JDqqm>t^aqM&fTE_q8+aB5BLW=B97|d$3^lwO-X)g?jvjFiA+Evjfx%Zu zLBa?TWl;3!I00^p9ra&S^gOP05X#-4T-dOC4Ll>bRYG3jHH^+Lh3NuNPX!00FwosN zFmZ(AQK2YuXhWn)xS5p6W$EaUAvYEJHo!b&z(`=sgn+`iCNi*@86;6dzl}r!u9&=< z1WyRbKt(+vbOIN$S{XR`5`Zlm3m7op7CPiZDjtrC6Gnd^2_9xXZ(XKy)#VL453ZW= zIP6jb&ven_YoWjsxf^D+wRq&62pf&wvNOq%AWsHOr(a&=J{jyE%LrDN=hbP<_hQlXPcK`e4!bJOn14$&CVixNp+}3mTZnaE?wC=srMepz*}g1`gzh=M6&_Ft z!S65%T^9a`m0l8~jjEL2?r83?r~`oPWUY=YdQke!RHOMORXdp`>z5p1x-zfGWlII| zfmMqdrzb7k_qmJQea`q!&h34?oNA3=d|u?_F1IHgPm`L~P5XTELGhdd`9pfgWObF5`sP6^yop55ivlI?da|!WukJN(C;4kTCJH4N-9XQxWi{C_04vQMZK_T4Xze^ox=(-UV8`EJP>NUo>bs3r5EPyMxglurB`{+eE(+ zFAn8?8c&Ad52=ByOOeN!}>Qt#1Edtb%vii^6O8(`0}CyvcF5r%v@M7U|f*7ux&>{xI^TPt zIzbB=RHhrG;CVN|hyff(LwtEFIhXJa1-V~%&lH9y7bSW!nv1r2y*F>8#TV6nB92kO z% zm+@11N5sl$=^A#fvT~z0r!)TBm?_PY0L@gF^iGG1F(WF*R!#?p@%Tywdg25H2EuMGY3Bb_9hcSR9dH zk?Kqk!zF-10$5YqDk@E4b);UZcG?Xps3}@5Q6XL$Aqh8WYYkRAb+)xet8H2c;8fbq zwzQ=cd$YCeba&?b&-1SD`vTa`*>m>(owI+x#xD)YcUkLQ&wAc__j2u`8HfHbKDea7 zc5Bi-^-Wjq&71N-)dRhcRi_NjsH=T=|38dvgB#}l-1AfB56w6K__s^9f*w6};q0;{ z;eUwvy(@c7{o+E!PySyn>bx1+bSr4j-&3WxT_uk6`44*5{n&J6=itevLl@pj8NKwo zHLr~Sw{gcSyDW3_X0AQ3`m9C!-Q4CeCk749q9eOcpJ`ga<&g#!K#baY)Gch`K?E;0A(I?Dq;S)8<*gM7PgRjS8?>E~tXS+7hze$*i6sT&-!G zKk%U99rzMIO#2uK)pwVmq7a?TYp6~4fD=?aykn6oS}RKSVGWHPKEs;m-o7{?(GxD<&)X9awf^c|41N`P29EX9!NK|I{OLsA3c3@VMVOIl8V5OllEWH{28om`V zs>nIx;EdoMPuc$T2?mUjIe&|+QBjH)hL3wz_t5?#tXSdbEW!>!byLV4N#Rj2QnCx&tJlalPKzZb1)+Kf-*>;_vJ>H{B@OwDaMmPN&Fu#^K@rzXxwS#RJSaWTG1Y zlQke63ad6LFUL)H)Hs)h?GP+)JsMIRq2*O}`ZnWcn1f=9($QTmij&cKxZ7-0&=}=q zC0|4%0N3`O@ejuay>q4~`TEsWf2q9GJ}dFD9??5!MA{tx_0lhA+`p|@KW@cW_0Q>c zo>}Lp0XeSNk(m*2)Y)hcxBHrwL+!#!AihAzvhXtVMtQL`7uD<3;*URf>_}1g zU&>=tf;NtZgNp4)&tNP-rZ@wND0=u(kLRo3IqBWSO5jxK8AY)v7s!MTT0XJ?qyd-K z+uYQ_ZbCcP^UIE;hFq9MD7M3i39+aXGGC>bn%de$qi=%ni_Nm8wbqpzc4z{^9kaW) zN_^%=&zj>80s5lYqA@4V+@QQzRAaMU)Q4UHX^aP4gg4X$z^n${KH!i=hY&PDSglMR zvOBkFG~tML0KlQlI5-Q{QwgP9zJQrD!8F}|5e>Cx!5JV2t_E^Xle7#_ATh!&i_6Fb zsJD*#${s?ced-eDJ{lH?5aBUdaoN!U%>hlLZ2sSz`_cYp>w?sq+Ucd9mnKZev?Ra^ zumTP^uTBaa&a~i+?)%xyatYGhOu2m7~uxwHc88g)& ztsFn$IAUoKT!;)mCmzXx2H@dM_ri%dZBT%f6b?}YFAwYyRQLQUTJk~kqCrf2OQ;eQ z)sbO#3=XY_aX6UOXxa#dy=beL%A*^SULeAP$y1C##QHRY=Ful;6rOXtN1uQ%I3JG;lB@!4OGYTnX!7Jl#F- z4C4j@O@@YdnBYztS&pjAMLdh-rj*BOnmY#R4d|}{f*7AAgv_Ftc>`+ziCoT3F@;4^ zsox^f(p|oc2J5o9bgNn5?mtn2NbHm~={JCLn|6 za#ol{tYMkZ@hbGQ&M4Bb_QY%BlBpjD;sLV#>}Y~|+7?NL0Vu;XP5vG{vu5oHjMCaT z{D!L}K<%_m2^b}Z({vVLLc7m?E_Nl_#$0XAY&O~BZcVQ;ycZJj^#ZWaCHI$~+)-dZ z{oB8`oroz5`gY=cgBcN@WI$l$iF}zXG7}TCd6_lLY(a(w@_eh}z?-ZToDe;eeo@Kf z$s#gDSSz(HA*f1#=82)29T588Rd%P?tt(wg`O7^!ly|`+g4BZX2QZmxos9Vn2ANhagMw7_yniXszGYeeThp|9H0K-(- zua4b1Cg}kPncUV;iJBACZxCKO@;PI=A*OgVf_%2;QdtrDWb|Ln0Fd*<9Ef2NF0hw( zM_0=?07uAOnwt|XyQv&&FP!SpJZX!1`-$vTdSLA& zy-!`k^ClQ_sna9QW#Y5;>BJ!}h3RPtm7(abU5g3d+&8B)-b}wO3${0{rFvfIGWR+n z{Ihms?O=^ogKGfJo%~t2RVnq6jVYiVqUn^sA51y46FhX zb-sfvLh+C1oK^r6^p#|vWn*b$S>puEv`6MW?~BvI`RP*2Ttgc-ZQuy&LdX+ln-t!< zF|-CS$3)|>-CRJ3gv};~uMaI(#lc;c_%wMXP$7;Wxd?*7?u*s)*v=Bc(Fl+h=sFpC z&!RqPzZn?9aUf4j z$*sYNV>>~(4X62+LHwPp@)kAc

F^9*`A$pM;f~n&LF{A$<{T5%ER3WlFsS?xM*8 zEgKgX+UX<%B;4N1axPS&^YtkN;g@>bmIcKuWHI?1ztk59j*t5^jN055oGA-WT zU-dFdTj07EkKBDWC3>*Azcg;VfBb-Ia81mbZ!=$S{lk_^XGM=+-qz-6d8_%Ji!UGj z$kKS1B@GQ|7T{FolPw(&` zxjslvf|((j7Fe_d*XE@X?dZr7>q6Z(>PgS7z&W*CM|!#ZZxG)r48Z%AVWFNXovPvrBrlq{r7l3Q$J7|?Vf35#}6}4=Rc*}yH~mA z{-FE27ZyKvU#IVDSLR*u%l|0U8t`>Y?ew08_{BjovFNzrW<<*=Y@%t?#7EJ`+q8Tp zAeGoKCF=6=6(&$JN94sCQ+6lCyHeIcJ-Do0+o(AQZ z-(fL_SSDsLaT*y;h47aY*AOX$2AO6!LlNIZ?tLsIn1~0nFa>A|Q(Q2zF?x4EP3aUg zcf3U^gjyE*eNtQA994dpB4f?E)f=@L4wg7Z=z6H2rtBM*L1Lc5;Xw-}zWg3#@ z1SZi`7l*Bmz-&zsZ8o&CxiF2v*{n7c_k%q4vpfC;K=t!&qq#yn>{>;o5vF*CUi=*t z%TN@aeM(uz!hNj`CtzkLz+Gme>KZZ!Y(n5E9&F7#pcQ+@Qu$r zWy9|R0iW!6D=ilH&0P|yXmyR8^az)f|IG&RGHr(BasL8>u(+ZnU>^)Exn1fGYaqA$ z%?Jbq*fF;R^zc;b1jnh#3|%nc!xUdN?Q4Ri(`%sw&J0;SM}Q71vDhHkDhW<0mH=Ll zJmvVJ%KSt6_p@u}Y5Jc(g*5GZWO(*?Z+)Y9JaV2gSbUHbKUznvm61Z^qSeK5B*-2Q zv4lxjkf=;I3YA51?(-%&{PH>j+cwhlqJ~;%6=KxSK(5yyr70rD6Wb!b`vIyykAX5l zIbp@5x&n(DYT$S@>F5jm{}2BIbPY%_|C4&0kSUy4L+oR~04uyAF>By& zM4z2ew+=}5gY2 zC2qwoF$7JQ;Ttiyzd3yHnV@D7mtC>pH)u2TvM(dvXKzeUYzO7CW}# zc5kUt7y178!DVN-T8*zLk%##ozBCrnTg$3u&FMPzJhmjir&KfhY9&Q=!DF&cJ)flvzLxcp zw%O|Uo{akRia{!{fFk3w$U}y02er>9spadrv{bN>gObsEK*Ox}G15X*fqxNGp(P?h z2}M<8g#oR$awBh}hwr&=ku3^o*;GCbC3J0H!)y~!!+A;r$qf+9^i^IzrE}DhWMQf; z>g+BgFi@-|OnD-d!XW!2eF%**hRrpmjMXGNU+Ix9(2HWKz|Q@Wv{xTDXs`Q zWp-DFn3G<7pDicS2(vp$>Y;cuDaH*K3o=gM5B623xGGgod+r%j12{S79v3_4GCzs&I^`s)FsgBCUeRNKw|$7 zcM{qLaNyBxyqdY@)B+BopEB=dAC^>>8y>zp^ZU=4a=t zm0^FFnK|8e=|I}Mx{Z6!oN0E?y8GU%2P|aSME?BfObZkd;HnuJ>qLK8TnWF=3P>TU zBLfDfVDoI4aEof8ks9J8Wjc;L`COX{_?5cZfZL})5M5#d$Q7`MAe_bpV-3i5QE9NI z{{Z?%x?lhxJwt9FiZc^{1N9NV=QJ!Vp>t$}2xcQhch{APlm{-@3JZb7=;QBfRQ12y z?5jpwu&?<3#**#H%U%HOr%xWd*+9Zbkuy6KLTNJcimCxA;)w^5)5oX+JXd^T8bHj+ z1_BW|TGxAX$w~x7sR-Mg)LaOZVB>Ok=2d=NK)|cHToBy;S0E%S_Q50GuV0IScH|7 zP!z*WX@~5QHy|mCW2pGr7hP;q)eNxR+8u*{XUdhOA825SB>uoQM=ppdA+t8KRyo!! z`2e7J4bFb%8#+Vh@sRk_-s+115(mk+yK5+WmJ#MkCDs$~S?*<`RPd<=B9O%=ZUiNKl-y`JT*Vyo!VXw-Uh|_l7s@ zb0`ai#w<;b)zyWo`*YZ>K$otRS?ng6#bOTnHk2hZ6b&^^$ySBfcB{CCv!b#rBGQLTRI||X6ZWpGOoUd@NFq`^8&r>xd7xzhMg$!xjL2&4Vdsrbtc_;`#p}<0mX+r+LAI%}b;v;{dUN&PW+q)DD=`0m+=4 z*@xs;@VOz2AC3a4yu^}?h_0$GgX-{Nb_6uUL&y00pZIb ztcKcbmaa4C#j2L7?+UKkwHafSo`D9f>j6b65EsZ~iv-FL_AKK#KxP^Qo7}U(@&rg- z9+5NVnZXVF3x>&T50&6$1iP|}JacMY007v_%kU$Ij|IPn2L+o32hPN$c%2EN$H)Zq z0c31*;z_7Bv@mI1Ghd1dh)cgh^|_PeSK;gc6tNAA=J}$UXcEc`gQj5h{32S#YobES zSulIplhh1mP%2a-4PfhE3MvfXqH79(Wfswj8;)k6t0N<$c?hh4wXq1J!K4M1(fKps z$B`s3fT2*fcx{+l?jm{0w>E0>va1Q5kf^Q!>E#@xvOY4my~A+Pk+LG&l3o2)hHKvsXZDgR4DR3+tiZ>iF8k12gl)L7)E%N-cC9fmJjJ%{52ud)U90#Rjs0* zx}GAwMPtHN&5=OHO&Z8aDdy~vSPqP_(kXE|JN}1K2bmgofDabO*qdP_!#}XW;BpKu z5`^*bO`QDlTFDPdVQKw~dd|H#lQqx1C7OIDM=oBOQvK<}*$2ZF zZxqHXGM))H9z$YW#rM^Bwkqpm&|+C z1xUiQp}M$DagG(B*?s^;pnaQJgeSIsCfSs+!pbT_iZL}XZ^F8pWuIO7JHZ<0z zSaWBq!lpXn=4nMShyJ?1#UJ-atAXm8jgOlrE$VI-7Z{&0h9$~wXwEIej%f3-=;Cqr z!@wdsdKk;d#MCpVDm{T33=+_dlw}$X#A9ef9W_*QSXf8gk)o%eo#P3y`Ag*?qL`cq zh?ce*~A`#r^3mKyxxZqI3V+}X|&Ig3V>NuN+93NGXW)cP+p3EotQ?9 z5B$C;&St$321|}^E?OYXROue$N9f~Y>D{f zksqZ`UpVxfZij+tQbB-8A8Oo-Eg$=OGu?rY!|`l;y5?eXuHuoM8`}d9mgQT+?148l z?(2IR)rZqv(~FMFniVtV0g0Bxf0VOk_O1M{mi+jbNuQ%%o_)l)W8a!bKHYNsOj?L; z?zK?(z#U398W<5kWj49{VDchLOXg0>Q>`c+Z`R}{clx%Vadj{(SI{1V$asdqJ`L9e zip_*}Up=(gHAz1vxG5;N;_&#g_tgF{r;3@y7S2~LR-Hf^h+Ra=C~RMB3t&rx7!!NL zG!$BvkXsbQD0@~F4UiIKjiO*!-a@p@)=)OK0ogwBDRv6KftP^a@G#!PHcjtOE4b+k zrO07m6Lt))Y74j(v&Bf9(1}V@*}{hgv9bkSrTXi}O7^PdFDio-9z%}edz)Cg-qF2F z?WcAQE~ISyC|l5n?~w4cX49!(b!il`*ULaoX)Z4 z{L10$h~9H<4h}{c9uOAL4M*dRFKf*FU*Qvp18$!RD`7#v1)!6K)ZDVT`cr1oU%mk2)w?@ygMC&5vZ2 znO=F|3-zVbRVTIM#>~-~)=EUT$ZzJtriDI(r~))_NV&5KsY49B3wG~{vQ9DfOtVB} zoLx6VJ5}e%$_MuU zv?iwHp@UhkAAhLKt*?J8vAD3o6#F*K_6oKvS>O=u$I$;ooKd>U(~ra_jxC}Uc6%~a zao*1HLMG#tsM$5`xtQwA;Cy#>)ngq?O~L#2Z=Uh^4=s;(Dc_xVX^QOU_}lFr&NpwU zAOA=5fL!_6QSk%$(eV#`xAd0hXePhQ+H4>RS6hzl3?9FG2x9zgU0niA&GjIDGmnGPbXMc6`l>otTUg-1tP z*NS0Dx7^h*t!UY3;lnx)Wo0ssi6JwK@^Vl*RMcS8?KKwMP?kI9HubDkhM+vO;tl1w z{OyfXpcL+zU>~cWBCp(PEqbykXmy-;N={6rX9DQWw)_PS^~&i}AWCkMPLM>9d<0GBsW&ZFq&up z{=(ozfKQkwa;z(uYZohmZ7~1j$a{LgtHBfq5-C87-7)c+qC0*nxEVnYnYCZT>f_)& z?cWrH0j5rY@rX zwklL0M34xr1en>eg#D=^H%6Zsr9j_Ez`+wp`SC%CM*17*0rmwIs<(#N!KPS{hErtT zRko=4v07uN|D?;Wn%paEwI5`j*}Ft>AsU7NUsrNt0P;xnNW*jnQw`(J$g?-jA6hg8 zcL<~|xVa=UEHAnDy>#_~w@iwulM8oF1HqbHSeaV3^ki%FZr4)X_T-luY&~g{3k>OD zYs;6^{n25WFni`-Y9Bl}lOYN zFc#sY@<9-=)w4N7aI23@0+aqOI2lXjJ!~`sNvC}&PuYUbiQ?48Wxa0}{or_GiasU$ z&6X!lIH#4|Ne|acw8^I|I`OmSOku+GQ~uiu zSNFa+%561zVJX>1V-ZNwKe@;2me`5OJn*4W`>QmFV$&Czi z#Wep1$TS6c1;dU62=NvRsGJyOdIV1J!LvaZm@H7>MVBSucP1rv1k|q~d4x%h)M^p{ zCRu`nFyjZKVgOY*3gH5U+bV=&=^M&8y^>DA_+57n<3_N<5$jJ460A;ZlQ!BEW1XV! zTSlpizHdQR*p0ji?u<*O-Ds&bXMc-R^y%cD*4V@r)Dc}QZn)P{{c3(1$0`Hrj~X%^ zXh{)es!vz%^;gSFS?z-Dw4Er`6~}$6^LwuMnjbeQe>Vc?s*p_U!JV-YVygu21@)0s zUW9VN{mOHzXS4s85{@VFYMi5ycJ(oaLB@7o3@}*Qij+GnaxJH;X9B2GTOoR zfm&5Ez!$qyA=4Ow%`gu5N0GcTTokK~-79r!q22&%DKSC{=N!K2x3Y{%;zzXIpRhLk z_*UC}7wanGFFTO_;1hE1HWXZ!XkTx>alSNKr_I(bfW}ecz z^5e<9e@jnX6zP7nbYpXI)o;%y7w&wxwKD3(^uCkH4bSHFmN!pp8gc}`_DXO@B2*rr}?U+c}srtOnhW}(T03gtgPYGTov1I z*jB^1CaTv#x{!K65MhT%K+qeU< zfXen@2B!l3MyMHf=8jTxrhh&hfQsPp<{Yxek;aq38dC*RH3UD&SZ8rnu3y_#$COyf zP3>57@XDSv+cO;z4qs_iuKp2cX|*vg8^(^p#3`tK2Pc4izPzF|=!t!Xjwq9#di(Pk zuLX^E%tC;WPKgU3ZppE?RrUlvqN zAOm;}!pA?EH&a&7J(XDD6cfQ!lfmy83$XFA3cn5rk;t##rs2MM=YWw~%8=k+0yvJ% z5m@XXYa~yznEu3>_Ms$2^AE)ykY{5QBFs42uQ@A6DyOvehXCu>*c_D}AT5{;3HCQ2 zEw2pzGe{FY{}pKC!OFFO@%>opme!^5v#8Zxz>Ldgxc( zi)`>GCZyOTK2esZ7dmY4Bd*bwM_L2-&fS))5|>Ue$o7&$md;NIU z>sOt7%5y3W>54AT^Rou(`<&?sh?`I)ZFI~CkbRk@no6CuBo_gB66a1BIlz#1wI_9l z%z{-yjB->*{ZY+}^zL9-|03L?=(*{N(){MvJY(+K`^0(Yij;x|_1U1F%p;O6V{}{R zKI8GCcGuE58*R(>y>Q9XlD4I-#8UHYp5>K@{+7tLybj;{f=rKxLa)oalH}O)*za3@ z8*uvU!aolDX>Izk7iMUG9_ZNf?vj;r4wz=FWR@qMO3T6#=%?X}s*gfQR1gRk)&YE_ zRR0ho2Fsp{uPrgW9kWsa^88(vMUUnLXf*~{Q8PO>h!qR>Ve&%;IyNs*Wrx3kyrMfF zRR+zCr1hXyQ|oM8mVF5*B2A@!DEDB{0GxrS0cV$h)9q64+1Ar&7p48fn6P+V$E4hN z2YMeIx$&k*|9X5EKw8dbp$^f;$+@^3A2z& z4rQ;sjj-Ii-UVA5gT_0{8*o@5V8+lfvYa(bAkbwP4Nzjg=V5jtCSo}!COq5>{q&!m zl6}s~IuQvIC?=mh!AR5pA{f%coyXG-`3g`VlefzGH{7}3OU$IpVr*DEl%U;G`FoN$ zR2vhhACR1P=@R0G+$(nSE{sk_oc+j`ffj+x?@U# z`lVf_+Eovw7B(iT6AG(5;=Yat_qNtpPEN|5{q6mdn&-_u`oC+`|3@Tu>?xjU`#{>? zF|9siQ=j>;^y3%zeU^B9?6%`}m89?zC+gfPhb&M#6*57wT0F`Z0Mck|H+1VM%4JJiceST4kfvGHb5yC0*Ud<|XY}ePz|Z z*X{0k?kaGDs&J(zPPG8gC!Qb%zCtz%t7u@jvW9N#3HY$>TRW= zv3;5}f1osOp|Pakp(>mdW_tMw@QkO-JAzZ@5cm6w=ClTD~h%Qy}lu1B9FeG@P zyfl_hAIE}PO{6k%P8aT?Tny`w0i6UKMVNv4Lo^NMICcgVj6mg|Xvb zp8V?{dRHbw5a{ty6g3M$}b*z*1oi|K>upNY1hKN%NIH9TK7Wdt-h1RpF4Mjt(%sA zp#6=$>eYXHH|%~`^bT*d`5O+L`0Xy;f7F#c_?Nd{Z`)(Ox+^j1r@bjlW=B^Mpnn`v3Z#Z)#Ql{icOq~S-r)TF?G}G(o?4oO1T1H9nWxGW zFv?Oru`apib9u=4iyi9{n~QX(ca$x?d?a*F)J*?H+Z!L*A9eMy1q-hjS0##j-pa5o zU9^97NWr50D?+B;*V5A2iz?@x{^o6_{$+COJ+bzl=j8*I$dmWx9bBLI*29y}w7>sT z@?Yl9eXFo*ZtHAmf+||S;XMFWmhwRe#By%^!l6A{iQp2#J2;?G4mlDrW+_Ah1~(-r zteuhl6cv_Y9fd>=Sm4d5y~!LHMf`RSKQN{{3A8sEn^dmeN3iDxq1^(hH!g{j8SIBP zYUb#rb|gOXb1XZX>Tn}Q+o0!QleH>g1}{G=dZh2++`A|(>K7-{Xm`P7ul4=5C(5RE z`9?j{F(o{7UeIg`f3z&vVnb+& z=w7WhEjBT1_E?!%mDJMHyWUuUnziB9jdmOCT?%t4;Zn9>lReLHhm@z&q)Ms0{`+UUi{oO^cy+HFZG7CaHc#v^zj{tFzWRU`b|EA-5XfjA%mEcv4t z?q4#T2pkU0lj&;gN~DA_&n1hmU4dT+{c*nWhpSR?R7$SkTdjLe(SY;wQv2<@agpFSuR~l_QIB4<5F90+hUvez%fh1ro?sk^k&{Aej&9} zb6=dN5+@RQxhvaHiv^_vab zx(!R(BTbE#4rx!unZ$L8wzsyQxi70QKflCOSC~Cm`hCm9I(fgXcYI4rS?i!N*AOx> zx3|u%O)dP`=D)usrMd9vT@ul^?}Z(C^0%Wb9Wy>!BmH4rhyF_Tl|R=`zq)W&%nJ`^ z9!OkxwJUx4hwlNh@_0uDS3}5$9FKu&V&LmpneYlUh=TL7Z>z%+5MxMjSPHD8I^_s* zL5e7>v6S9#`BG0@_c> zKZt>r%P%5u)?(lzu&Db6 z1^i?b37F^4dRYMA@Cgz{eKJ}71vsrZ9ttwWQzpBMw=7X15Wtiqn5^^^GD4nKxG$*2 zXr?8E;EPxBzn}oQVE|EurekF$nWEHFtK3Pe<4!POcnUlNrt)@o*i;ZW;w=Nk2sABg z{^fPFVmD9>8NG(pQ7-4dEZ8uK^-!~_5wwSXJwkQ%;v(1=4Xu)H3Wf}3-W9=sLimT9 zWcEU4-=Rl2iF5xNh$0-2B#K9=|MEyfNSDKlt8L7j1;Rta_H-u?WZ!neyoS5=IyLBq z$XhB506$^^a&h$;+#?ie{-W%3f>DsLlYt*B7Eq9|E>hH9BI(_S1V0JEA|4cy!re22 zLv~+OznxV(bK6SP z@4Ea>rIh^kuVK5Mnz=!D|Ljf2XIxIOcx2}vKQ`mjy$cM>zRPK^zH*+)7pD_oes<7G zlkZkz50XeXp^QJaC3qYZ-7Su)5CxDMn&~o6kSzQg0~ND|nM+;@Q6g(NNC^FmZR5F! z78sS65d!6y=uQQ6V{lxE2A2*noW*!Ji;@^U+cPr|Scu$)?vvTbk9&anIA*7|&^ZY) zPTQLL51Ev6s!W@reNhiYQdAaVmsCItFNctu)_<;#81wYt`pKSg-)VLz$2w}WER_~f zh>RMy2FZImE?oBh4A{z($!Tg+zkOlJVoqp#$ zJu6zyb^7jhRA-?noNtLz@`ZMLXMAo!^QEw$IK+G{ExC91`q=)lv9D#l*;d`2@@i_A z;b+5}9Wi|~3w8(PKi;8lPrI41I!*G<_X(!wB&FLn0uMV1wgq+ErBmJg(aj|v-^gEa zb4jDkukLlntNEp?a^IL?TK<@O%Ib$zulK()ec6tsKgNHe%|5X2k->QrmU_mn+5FYa zj^}1CxAfijY0IS9{!JR69a;HGjZ{7wv9quQ`egf~KU#)AnpR@FVFQWsPfCeMcDZd_ zd;G-qwEg#;ZqV-%AAeE3PjRlL^f+vj0HTo0hI>Nd63_}-ei!-@HLEg0A`h%gG)UnJ z3EE;Ev*NU4-r-HnuUyX`fB7#K{hrxpKX~n)HRA`ePqh7I_V)uXuYSDtA=TvCRSQ4y z6q~z^Z|_2$sc0tci}XT5G5i_Gt5c(FC#Bd;G)pn9<{3ti-Ugk>ATbXbtYWl2^p&cjle#mIpTGV_ z_9M<)p0aVzpG`O31vnk;DBk~^1eVO8^|)5*$&4I{S@egF4JvMPqgQ0pZF=`wO7}qyB->o zaoKhEs0s6H!^Q+l&qnnw2}@fW>M2W`YnP07%}0R^g<0e@H=y-AJ1xpdb_$^NYsdd> z%p8fA|5ME5aFZ8XL45B#dUtAJFJqA;Zh$+)kIPD+5)yp2vDM=l>vXccZup6aFAAV6 zXufQB#RRjX8!VJ@y#8NI!3dH{WEE>YvK((%7^s+KIZ+@B@d&aH4tovq_e}dN!%b|u zaLRF)VodV9kWN04+J8c2k%?NDG>Llq-ah-sQ?hYKKg>~hYBRF~s<9ZWmHrXPV!kJ2 ze$IlBd5NMNRie(^>ANGpHKp~;fMaiSgkekwiEkemmVKi9Nph;ZSA}rwl+56 z<(L#rQQiKjE%zBF=g*9NJ#36g)Y{+ZTGZd55XstR^{yz4n&*)!BIotK_ioh5{&}Xe z<37>IPMwvPxTAXNZ2nvC4pVP>GtnN^n^|kpAK36?-e-x=Zn|f5|Eo_mpHAGh>26)$ zz4N*%(?S$IF9%C^_twRab_|#ny`~in8dZ|c*c9b<_NbV%fEYcvlTtn?w{Fn14xqi; zEaNB2o^keNd)LfO*b=q+5%l30CAmH$;aH?iGfq=vQTc|e!WI2;6ybNP&$!YWEhtws zKlb7Gns-SmZA6;I)>Wrzmf7^<_~MrM0o(p>a~7^ywITDvy_5m7 zT}6~p5DYc!ifkfDGAP6~>SBa*cyp@F4jLHYQG0!JPZpI%NQYLEUrM z6R5ZyxT#~H^d9+!?0|y`WMp)o%uztHKvHAmJw+(>)e&x=FB4C@T6I*uShrFtN_B%h z4pJS(Aiv~;s3(P{V>vM}g~DczkHYO}_CmMsg8kl!d|wvio3yFLUuD^5@yp(ye7p7Y ztt~w$np1DLc`H5+vo9qmp)u!h3LLESLp49h5=6U%H#kOEXXix~Kdt1dKD z{#rS@Uv;b0DnQ@!lssXFF18#ub*IUWltv!JkpMUR3lmU#fteiX(;>9Epjz6eCfKFjciaW(~SXvtWzNI=q zzWWJki*aIHTi5NdwD=_rp93AoRGCigFPypG;Ga5YA>^~DXS?l0eBSAnmP)ygCMEZ1 zM{!C)UtLI;;xWsvvl+WA)eX1P?T_xhy795HCC3z!M=L)I`@OMkT-!9eXj*Xlf}7Ra zeYY=8&Yd>%quC)v54-+i`n+jTNA0G2HWb#ItKVLhUXhsdx5Sg1?tWxOY<|0T?kA?t zJck?ho>mMt&O*Y;dGuzAMqf^uwG)dj&n?;|Ya91sq7=k8Ie*TwJcXgKpd@>5iKdx2 zBtpWm+S3}SNSX;G>X0!TT-ncfxaXMRBSVI3>a=ySuRUk_quXS^Z+}UewyNf>d|PeI zk!ih;ty`17&k#SUT>+C-vwOE8UOm!2&JwVIie|RI%6wGGP%+Pe5N2%igjQ5_8c;6G z!0J$)M;4vTr~k0rJcuBEpr_cZHNcpc)rUlX3JZ!v@4iL1$xTD}NK>mpp%}qVLx{;$ z9*IK_-?Ne?!({Z_{Nk@PS&CV|80Pz8Qp<*nkJq~c_e6YaFPIkKT$OFI$6oKPdpTLwuwBd* zzNtC9O{@R)_!nO1gBm=9BkVr0x<=3-Y>!^<z3yvdF;s{#qeEHBme8)qKc&J|*V=3~O zu4?CIU<7=Q3L*{S&WrGd5@t}C1@gj)^YG`TQ0ZSV_J4^w+DXW$H`0ioFLM0Y#C^g} z0&t)#IL0D5l?!6Z_ge>o5~uW^(ro=kCfHf6srw#cz2-lNYS)1O8wQ%A1 zs=GJ$po&9O)FJJcHu;yzamS|iw_h;&XXoDnDl~lYaO>@&<~RCYbmhAHbGd`n~6W6aAm$0e30SDBQBRRx>Fj41^lXvJf{kJ{uIw@6pr@JZSW3woa)*H$eJ zQcPQC^h`E2PFhvaY^jTeDtQwB={q z!nEVLVQvM=RNt*Iok=X7{)6q3BzZaltv!uaNrhTC0> zB=s4<%v>1H4qv!i_-jE;zX z@Pzh-jB1wzuW)qlox?N{%ER7-()_^974V@*MAr?fIfy>&uj*pWfVi$=O#P0geVG#_ z4a$ikhKB-oq9vu+ZJDbfK!&t^ZVxA@1@MrUwF;hOmJsAWk79dFDn<8=@La!*^>)P- z5lYb2ksof0!gec1S8_eqH%t4Z_QCEH>zKf6O=r&c6y6|wjrPpP{5q#PU7>uC!a@B44llZt3I+b}i(fe1B zlQ8y9^!(pMP7-(hFltFt3wmiu?r!$(K*E?o=k+>o0=${Lc%TuS_m@1EmjIFi_JXI} zz&0nAe?@ssoj$+HEq%{|vv8o!2&xdA$S5rP1g9od$;oyqCjuHoj*RzG7dOuwJ5AwJ z`^2>k1%sJjm#$gk50C;9jUvy~IP5eljHzr2!m`s})LW$iGyAIj6SuZh>-`po-dM7- z^@Qn6cysZsI8|SJT*0%}wk(aiA-hA?e83)HHzO`1NRwoX8tuPig==X^?yQ^2um^L` z8orR%Syy!(HGYuRA|E(@v8S%M)Xu+n|;ZapIXn%ne|oNu6czj*XhqpH$K~yc(CLThi~QAkKgl_ zdt&&l-17Ek53Q_q<>^Zh?LBY5>AI%pkZO{D-ZcMtF;8iC{;_xW$2P_8nt7HplKj7Y z^g~;<>&x70&Zvba^2pRju{U-K{Ch^NJJm0QMdt&vNUDvNZWjT@XSz^InW3@YG>L;FYZc&$| zaZWVlPzs537ceQ<;h77W8qq{nQhy1wu?}~1%^gyT_wg9 z>6OB)TRIf4LczuB-L10>*E{2l8H33+$)Y-H&RETnC{3;cOX(McMY;|8X?-M1xglyn zb)kv#2qPWLMN1<9JS78ls!|jcFi0TWy4+2P4!M8tE^Gb{y5hZsO z8@-$6XL=8Cz^&*`3$?rW})7gC)<@NWxE=f)s%rWo(0I)lB%wJ#h#{?hhx0ZFW=E! zAdM{Sg3dbPEV;>`0;D*v7|D1>q0}6kd!k-T|@nbl9uCxH}awZxU&M0+PZ} z5{9NpUK`4=aTVw(yE*|nON8d&mrg6#G-={}jC0d+d(4%yPg~O@Z;Y4jJN~fa+Su4r zhSD$cP#PD_b|l&-HS4&#pw+aFZnXl}Q_x6eS*^e~mSJ!}sbc2bwP!znFi+XZgm)wR1;FzHwe& z{iM3g^Ww|-znu~vQFAH3*mNp-UrW|+bjKb%e|pD@Vb90C(y?)Zs=DAFR9?uezap=T zdB4y6&|T`Qb3UFgpI3IH;O+D%S@h}KEg!!UabwB&yNpMs{$Tq=vt702Bj1kf6TSO) z_1V5X+xn0qqv>VKTT8MF#yDbgg31j$3o3M#?NcM0nj6>7SBd7TvbWbYG^J#$s(vw1 zK3Ow$;PBtIk>(e+1e{oB`{b(q{p-?mXKDu$k1yK#vhC9B;zs+*+Du&?i|L7mVkQ=> zKU5@utu{Y2+KIHT43Ri$s?Hy#nC-D&(uBuT4VtF*sjP9yPQ@FG6y2N{QAI-to+u>w zP!`JpL7zhPARrkDonSlJx+nxa0pH)E$gstJ^RCNE}kOu3C}NfG(WdZwA#1* z-fekTt0qLx+4jecZ&kZQ`S;c?R#$%CqA9(1a@>MTF}9Uuc~9cfCM9USxJ*qIu$(09 zh=&>!Wow&B(WYwpw2n}6Hak~bTxmfKc=056L{m{sS%u+xPtg(O1YgaiTzS7^U)h9x zW-W@Oe&Lw_&jK+8uU0I83LPnV+b{AalKh4hAY-J01*^!B6ei+WW#Y==nQ zq%EjaxH|dqhwHo}bX-KjMiVg|BBUICcv&k#PdJMvTlW5#2Bz5W+J;OWGe=e7q341w z1+eDT^dXE^4}*tc5ImHrNOI?;^Z2rF4mlkOEr!1+VcKhv7as4)O9|8P~=J4Qmbpm(=G2G^aABJWj%tKs-d>w@6x76wK*`VZ@bXA_%3U`M5mhbb^% zF4STgjAF#I(>Rcf3Otk(wO>GM5II>TUJ>+r?9t|nB}*M0zuo=*(lm4)OUQs-o)Wt7 zFE(+mkng<;6p^H^bh8(>l*2B{M4_OWv zpKa|;PSm8g$$Ct2zSWu+)!P%a?Ng_iR>mP@Sd_o9+4MtOWqtEfd9dTgsfMo$KiHi3 zV9STf%hM8{n3i8v)c!_BW7>oz*IPq3w+C*q`8y})Tk{QCSi+{6_8d%{8#XDZDE8HY zxhn^p%L)v4D=w_gnxgEwrJJ2@DXX2*@qIKuk%`rXO)6Xbo6c)V0ZqQ97w4W+bd8Vy`gze+zb_B8KlIbws1t`jZPOn< zxvIFv>?y2Jl79ngJs1SBGSY8~1i@ypj40{|pO7cK;b`nEV~VmI=IsM@V{#OO2Ag;@ z;ny3KfyUZIqoWRD<;OY)Q?4E;+l>>)aLntYMGn)l&iL~?GD0FKoZ5zDY?cQ%h!tIG zF2e4KYop3E7S94HZbbCZSE9gQvtunyS z1A7Qn2l8`AD9~T-z0oMO>XEVv(6ZDFy~1o_kq5Id>RhojvB#96mQ6<)#*kTvK0=b^ zgpzBv22}bYV!0i4bQw8h8QY8)jWAf^cA^Eggd+?m!|>62uL4-%wrGY4cIZVM?kdZK z_sKd6P+}lZ!;@Zo5iLBuRS~(=>lCj3v=@H_$-?pf6Ue+1LYa>|E&!R!!~eqBc+2oa zPCPQRBJyJb<0y|y-e;!ZpyAH*UswR>%-)g{Y)EJiU>f%@NEMzO7xG$j*&GE@00vDI zSEEo24S(d_()r~*$iQZvdQNiquMl=mG^J2727&C*j$OS^(m8GBKmPo9@kzJt?BkaU zD1{k|i`w5PtwsL!_ZQ(g-A=er;7WOJZ8*AeNknbhOIMHOY16M(bv+U5tZ2DtymC)$ zgmY<<;+x(;#Wx12V`*Aje1|5r5KLFU(0HXcY41a+8LQ4$<~skb34dD~u|Z32Yx(7S zjDLy=d300r6q)#?$-Q-|S&?pCxh%ry<8ema$nSq-|F+07$$1ig}8Wa z&a9}Cg2M%l=4G031qT}R-}SYJ*{&(CJX0`F^{)Em@#bi8)PsY{{ZrLjbhzcj15K{P z1En)&=ij&gans%dW#;9Fj z9L2{uieQ67y0zIoRg%BGAuOuXZ#%`1&tgO~Rpt&rR0~)q+~|?2c^jK=oa>D5(Nb># z_jjs7L01trk^!5fVt(wB zpZAadzU85>a{U8-Ge09)`_R|j%~a-7k5G&h7?H|UBu(Ukjdx3|T9L1X3T6AzN+Deb zC69raSxPH3Y%HRkn7z)z(#WqD7Y=NfkCFtRp$;Oi#TCkl5TwuLofC)8>jN(%0SDM}`Zmd6btR=SN{i1of)1p3lCBryb`yU4seaj&>xDI9Y!sNCZ?tx7+>C>^yLwz^UARXZx3Dp6>b%^@ia|M*<%>Y~>=C)#GXW8}d~`So1?q@YzVN1WEZ{_p>G z_Ms=&e*2e%=F59~OJ>*aed5~6f=l8u7Bcc0a_W zD18XpECG?t?s>PeW2!_=FY1;nzR`5Hy1~tK#&6*lN#SS(dmTB z3M1-*(iqTn60agg3nWscga$BIAYqFLM29}-m9#vIK%*EumZP$@NH(uaC7MMU1Rj}B z!lICDSAHIP?H_}>H3&*aH!DOVlp+5K4Zo@&qz!eVrk(DK zH~IY2vNMJ9t-tH(9v5c+M7mP>wCwZ~_2nfQ#+&ETjW68lf3~J8WTtGIq~4Li;)=`h zOg;vdr*tEW`0vULM-jpIu7&H3@7>5hxMRt*pPd!SCLMZ@I=QqvF;HX#@j`MFiqk^z zhU1G`Iit!Q`d$XZC6J+)qGm~^qc)>RiV)Zs{gmxgb<%^)A{<*w19cf>JtoagO=nh+ zT>cbXck!8rH3QohnB-j1E@vGE$q(d?Y!Os>oNt-=1>)dQ;l6}eB5i*hObCnoPN&c> zbUcZf0~Tf#KzWP^E5kKmKJk4oStR~5qJt7&QkFIXw$f>}4||=5k%g=@+>4+`suqBk zIu9xOmhr*5-Y-dV{aZ&?zXh}jzxjW65^dp%Yu@a9n))l?E*^w@_*27 zV~Soh-&p=#iRra9Gk^NCart9M9fxSqqvQ!DL=mt?V<>q zFZpQV(n97*MfExA5T(NwaO}Vy!?~X2-DNFtvsWdq2q{P{4DED}dnWem-NtwKwBF9i z)jxZJTHCg!ZEe$UX^~Z-+Rs5raHT2dObhj{ZS5#ZZ0|jX63vjVP+T>x(@Q1YQnSTyLwVt%{(Utb z!nY*ly7pG~I`e4rZyya>v%lxj>3>l*|IX)y!k@mq=-jjO+R+aU&X2FwoSM5+fszTK zSieJfIrGG)s^PWSYpm?qo0lkx?RH@lauF!4RVC#{>JjA=5@@J+ z9m zgL4i4XpLWttoQTvn#A=rx7_DY7RSnVuM~8 z->Y{$ygUEogAH41+NW=lEO;R;31xT}>PyYluNC`D~rk)`%|9e4MSF%zZ;t3|Q)>A(z=nqL6lfDVQCz!@LrYVdQ9f@xy!x95E zhi9Iggu+M4P?TvvcnZ|OouzH|xl43){`;%lYt-v2PTc$Wn})KGaO8|kkXWgl)ELh} zRAxXwoUd$D7>X+o^^D<3w7VaT;Em?;`P({YZ)!Fw3;&g1ICYl)la|9go`am!Pkf~GfRi^Zkm z3Xezi7-Kg|E~wiUyP~?AL${l|GIFp?NS#7_j9g(BA^fh`EJc?P7*f%}c(cLQou!R9 zsSI*w$5`Ciamq{aj;|cGa8*5HNCu_o4*x>q@y*IeZJj&Tr^ToF-lTxCguC+p=)0`F zd$oJxwnL94zWu@afm3_lowL9obB7&!CmOR3_<+R>dsQhfKc%P(hO+lTG8oOYyxzm@&zh_4YF((6GCMu zwinI+&{t$5i}$MA&dS=*~Ta4g_xyK>jK>?qsK43+ORd%$)S(w%@pHC315($V82 z+8XGB6B&ihAXj8EA_XK!eluJ0y;XnocO9E!?NgRL*O8UDXLIbCh}QK-V?QWH^1cT) zgaxsyT~X!lS9^|aIoo>9T4@qrT=mV4rT5SJ-nDUddiBy*W_@GqPNS5T#OzMwwHHP>6&K z8II#y`#z42e$Vr~&-+~8cYXi=`?;=k?tPDYuf6t~_geYR?iUqy4p_OxUz0~w!9`&` z@5A&WiPROqFrFE22I$U?NrJH zz7JiHg$xw{p+>K2fie^x~Tz%*Hw5=i* zz4`M3x5|o%tk*StP^)zX=V?aJr6!lg1Rh;#>P5_(0CWUM`}FdoP1gk&#@1ou^bEturrh{Z|(2b z(q8{>{Z7l=G-?ClF-_q{-^c#`whs+CJ|Mp1PeBs_EAO|k`AE0-?Bfd<9PEec6@Q(Q0qC7y*hsJWEgpm1<|oy+nxeFcNc;5$-|3YOwoPqQRq z&3#Fi4E0cM>09Tn?afyjO4sR^tmWWBj>ND^Ca}xYK2vAFUkCyD zNcIBr=G8i8v8OLha^lbO%i_=8;KVQ9EpO5hrGV3c0K_1Hqj-juY3aU@I^v{r{+l!D z8RcdVjzraLS9BYX{CVi=)vv)W&ZcL|ZZ#oaO^rP)^SdiC-5DkwW(dfsE{Orf#m?+t zW<%c5W$!b~RZwOnh zhO&|=2jq`mfoSJ60exctLTHtpywz$2EqTlE!mI_xTgKjV*O|<&y-ae5GSP8Xhfvqwf*2BKus@JJ=j*{z zT!;cm{Gfzns_C{4D1ak@k|9798e}R~aaz`Z(g2+=MdAPslI(GuU^s@L6Bh{j^mAZb z#*lQ65hYiVSY~$)YJ{x^c@!!b`9ea<{X2M?D!Fi~Q33~`P_)U)EavT8I$X>mj&P8k z#xvi>+*HOu^5zDKB@%@_79T7Mc=wdbFV)b5klR;?K%f*s^r4zScT5G4SO^u(r?cpw z-Ldr67TB@_0+=VJbj&#aXF}o;s$hS!4VkIs>pA|GH{eeP&vXV~D(t3VC$1Sf+}|)QiaF|EIS`Pzlx- zHB-RMpkeT+D>T6vDk%W{0=hbDNRR;(W%k5M15=A|BKi*&H5}~D&|yx+W7U|@jxtEf zz@Xm{5l@v*z^;R+I*6r!T$o82IYYP7Y5P#I%FOBaW=rhDSZcVSDovcmU&r`w?BafV z=s?e;h28BhH4Ix!1x%{J`UAzZAOHyq7G%nYly8;bdxtL}-cj3X$Yad(4Qc45P_WmS z5S3Id3DF=~nG9JAt8Z&&Hsn07(_A3+*2TEoji9$}mA1 zIl}$aaz-9faPTJh!{JH$@u`Cb;<2l45YAWMPG6#92b&NN087zBh6I)bE+#RS82l4L zNo;KDz^?jSKLxK)-pI4h2A0iYk!yQzZ>D9H_S#se*Bg+G%^YoJfwokG zK1@Rg^o!v&ivqspWqKu(BuG=GIL3tZE09_TXfy4)EWXB2xrPh!G!Z&toq)O@f$!ygCgdf9iw9pF5_Q3c3lC+)QrBy0uCb1|7B;BPCiyIF zoq+o{^#y5&M(e@O>Lg~KlpHaMB&W+*hJ% z3f0NmYnjDlq3L{iZ@AT3U;V4K>NP$|+w|i64%n1he>sz{*SaB{B(Bq$VWXg7<(krC zZe(32SPeOwWj1IccQjBEs6U;4v38$%_+&SZgp6|77y%X<^jX4Kcm!;WH5v8;&Pqcr zJcz{#41!3JJf8EVvatDS<gh0o`uI$zPQb%C639~rJtMe*3g+uT3aJrfJQ-lJH%Mty2PMZz*W#WKp z5ZLxf6d!<5MM<#BpebERB~6f|3APEz>(1D!sU+b-P!*6|OrnHeRQfQ2j*_9Bxip&q zm8!ym->=Ctvpp80bAPD%*p2fLIKZs?4@{gG;hd(@VB7BGK=M5>Zsj7f_TIp|n&P0>OKmc!OJfIdtT?Ncw;npw^r&d5LQ^S zSDSY`G5%CkSFn>ezvh~p1HqpJLz(r4YIv~c%5P0#Yly8KNml@hV`P0SJt2?HQ4BWO zfx|_|Szi5OLoA`!gk-<0_Sp{S{a^$KY!{T$%q-P`66n=2_;c|F{DS_RJ4wF%hYr~L zDO7aJJ^vNIl(v?k~q*h@WTXHOfW$- z5;DmVp!#}p94c}>?HdOz05%A%O2T2ejW9l~M^ksuuRNp?fuOJRrclj_fj-hK78DQ1iNOM`w-u9ztoB+0J!JDk70G*UgjaG@Y+lhiiJfXFBqOF@Rht z?zktz`c>He>z-dXaM?GBa}k#A$6ErwxC=%_lyL+~<^og5$Y@SCi|I3-Hw1d?3>A%!15Qfd{q2-Z&B_(SA^r&r9q56Xbi!;-$U=c?;_W}i ziWR{X1!WrwR{%~T=L)FD00(QhqW(Nh5C|4ZEXV}XA4=pd?$o1RiZl)Me|0qj;YnMZ z{#KoML!BIp+S+Ww{99bA*UW%8Eh@D-W@#jH?bv&gyrU$uw%JQ5j%2im*-+5DrFgqe9iXnsIKak#NFP#3JH3ZslW(ApZ z(8$X)Wk-A%Np6T_(EbqVK&B`161!2y(5_!7t;(*B zNu6NN@T~??AEIiuOa7*)!p%cU-_APYvS6RJK4~BDV-$G74q0BilGp|`)xdGPTwfo^ zFq0EpLh2yM9_pafu$ubnK-vhis2X`a$pv3i6*eR|-x;|MX#_Q+!|Gxivb8Nuuf;vT z4+?lgCb)c#ks`PKRM?SYUX^{^mB4UP@fnkq>D_h}DYCYY4yOcREHkRs8(A16l2{bh zXLW!>`2xwaCKw+u5AA!$S4sy{z2l4WwVbsw%h%msZhm|u{Z#U^Lepv=JxS&#=7GqE zVdI`wz7fB~VEM|z!;&`-nMCHeHmmZ~ug}ztaBd4a@HS(!y-7k|M~`97j*|dKETRO> z$jl#IN^lfw%7X*J8&dCAUok?pSr$*rm_8xakXon|_6LG+1jG?MKJ$aB1&;9maZHYm z!1lVMPz7Y8Nsj#!0TF-n`2ae6(n%ycF+SBtrWU8<0Cu3M#_Q!F6-p(;jun!5sAy^= z{&yDCG@d4KnwX)8%%piqL&?6?-Wmih8L;WvRM*IZc~r^Quv`YG5Wyb$2nPpS z6e{fiA&`aXOHs|xQXrd<>TY44UF0UH_q-e9M9WZ|C1(IY8%ui$`nj(9GpqtM0zQ~R zN(GJA;V(ewIx|xak1<4VIi)gnp^c#H$I^I(2pYz0QD(KZyVB-WZizOpy_TJwual!A zYm^|h5g0@nY1QRaUYp_TMiz~I0x61$fV}fCSuYk=DAgAZ&KV}4shC!~5UFBVF(S0| zrwQyorl94ZHPRC(Sobud0KZ~5NkB?F7$Vg=Nd27^R|EtJ$r!&W6OBSKVha7A{vjkT z-JK2)rTfqfB?9pfVJJrNrVp^MBQXjSwZOhSD?}eV2`{A?H!co;Kl7C74@lvlPx3#pNdX+4C~G$}1Q$C&5*-tLpC0um z0@f~23y0Mjc5wu2Y1sl(4w=grijI0Z_g9AL>^Al&U?Dy&&ICzEt;`3)fCF zi-mRP@eC(dhe>^x*)<>p!e^P3)?PDH-%=~HaMP?dZ_x^#ACUX30!lrAjY`LfA*#u2 zKsSKho-KV{)@_6R>KED4Nnru|)8`qQkixU&)fXw?Ph`Mfl3gWaD`9$nRq{F%3(5;R zD(Nh_n}d*a;KU#~gY>n}cI_cgiUE^Nd5vSw&Xk11H_IaNOLQ$AlmeXa-P<@I9tVd~ zR3Jk(wgGO|tO3Z`Jk7Y2YY%{N!U^27fGjMG36jZIC6)+EAXD^GKvEwrkyvL_Df#z4 zL6FRikUH(11MHzPcOHmWLbgYkyg&c;BNhtxdC#3b5(#RgtP$cU8|l zmFH)w*)hw5`ytsetPQ0oFrDgb|5lx7@&at&?7*K*eRP}*Q1DM7m?rI_V|a`$yutPT z4R~`2c-}PBD?w&5MfHEnP^cD3Q`Zpf_q9t5IEn&nWgB7~nVlFHOQxqwn}i!^5G0?Q z+MU}8a;U*J$o>laDh&dPAqR~jZ2Mql1{It^7g;UUQMmVq7}bv9WD##;T_T1tJyr zy9r?%6Rmf65eRH}+|?>gOgl&H5)ryEEF=J73y8r;(4qS~5C(M+4aMll2*Lq0%k+P< z1V`{V(d+)S_krh$j`aLZn5DfAd(}S*T0#kr{v^%xWcu&N%+OQJkMw8oI*b&fu83L9 zB2a157ic#~q|pXWOYWQogZNYIn8~j(L+GZ7-t2`G9f9RRpxcYZ4M~rxy^$k~G3q$g z`os!^A1eWh;E^}2^3OK7g7X(>DCG1<%M2PjrDd71|7$#Vh!OH#E>RVzrYqjuPSCW`*fo%E6MRP!gwwVRGxMt3mWv8gH$1_A2V>b&5O@VZ z9~E1c9N%zmd#P5Pj8Cy>yE(bPn;D55==DaxlZrIR;D$ z$wEP@n*_jR;RG?*Xfh$r5YR+(B^Amb$I_wkm)%1XJFT1(TLa8k3HZ zkcvq!H+kR1l2W7n&(n4Fo)+o$5>a|J$1LBP*Q~I3b=$_wPycJx9q(JW zYrCcJHs;-%QkOzBhiThYX7Lw18+r2bOz)?iPZfy{VBIh2W{5u!#trUC$gYIDpB#ol zJ`7XvDE=chqay(4#48fuqI1sY!%^#P`4T*SPMo|~&9_@z;S2B%uyZ&b)g~#i>Jput z#q5^~BC7W-EX&@dtx44f!WbZ4BB~4#HyPOJhOEe@0*DGQA5G(d?eoi#oCEAs^E`+y zn&;X8@f+kd{f69PGB2VAu)<85C?h4Y}Xp3mDhgL=73m43J260Z~VfsxeGcR~m0Z z*#@Iv7J%jgI|u0+nY=)N1Q`HMne$1DCy#Ik~}ITzzE(L14sao)sdlL5jB)mS?ws&AP{fFYr(Z6 zBOuykskIhtRNtLHuA1bwSj_UM!x3uu2Llmt4JJm$}Z-~XQfIu z;u%g@r?OmM1?d+V0Xj~=65<=;cJ!*BBRvCgadW!vBFHdtHAvu6Dbx1(QV{jD-(+2q z!*;XvrUD0`!-(PEf0CMBw9lAH^emUdULd32??AQ3@x2f0wqLp{{=?vQ_Vz(D0|;>M zad5i_3uD7Jq?!BCa91f#3(-|KEdtvKo+gqW3q&u(X|AJ^Qdn@fyt1Ah$N;MjAf3*0 z3VC|LqEPMQ?+ICXAv1Y$*zvTad9Xt=HHl@B?hbqdn7yF*p&ke()Y>m#`TBejh3(_FZul2kw_++ThiEX)HGDOL9VOJ7z zM2f?z36KyF(A27v%xpd!g7L;sOb6|j+D}3z{iP9L7h%vr z`zBIxhGO%o55RsZ_`3Rnq#CDbi3D;y1_wmTu^8VDS=wTLDEDSpD0Km%|$}rAR;= zXWkz~1Cy6%+R1D;I?!Dd&kr;DHzKD&qdBzSS+DVh%z?BM5Muv#w-Dmg%p}Q#m*{JTJk=bFhOiNWM4T*9R( zoEHbqi>E?KZmcWuS7i$AD#MVOa#wL@GO=Y_==8ND)+&SF57DlA3v2$r{>owPcs}xw z(L|T&vhkS-qMd`xi{^W=NmH_&24uR3o|M|30B5A0>!wXY#^jLX{D^>1G$H8t%=Bz{ zizcTG+R>(t_R3jvV}hZ>lEI|4ssRVFuDzqqVLZeB>1jQC+@p$Ev7Ow-$z3i(MV}>u z)XDEu-x+0Ma*2i=CKwhip^PW7OJtHqF_bX(=%2#T@QfsHGB(o`I-@pN2k2=4Btg6L z84dqtuH*DIHD^vgK4S|vuUB7eVlsyCmRu?XX8^miaWL=-3{Qu(6Zy!)MibrGxZ7ad zSz}c+#D-CmXaJ((a~8s9b$ybQ9Ke?zmJ*ECQfjKx=cun9l0Y!0KL>FBZ=ACZMDIb* z3}XGlmQr-|wV9Ub3|ul9t@T1H{L$SoAZ|3EE`%6or@{66Tyk_W-@<9&W=?EZ8+@+@ zV;4(Ko<8m}^~&ZR>*Yf|b=Ig(o-h|4Y?hn&ZtR)_uo)n>*a4v6<~5Yv+NeXyFk{)U zh|w!%ItFKn)U)EO0VlT3Oi1f7TG#UO`5IOsV54`aRtdF?puJdEIRaC({ljs1&k%YmUUKb>hTHmJdx~9Iwv{u-;fY!a3 zUgPQSQ7_QCGV2Kw$-fTg5P6-%1djVdnyj@lwX*Yw>z#IZiU z$q}i;wi^^op!%(Y_%$b`rV`bgQYFqOHFCZdPm+0do_SdxM9K@Uz8z_KOZex6%M^}i zMRtDc`8d9rQcG-foWcoM+IMKZbur7jas2&pnD_OZMib=Xc1u3%8nv#~1b85ZOxWdD zmB}T0XwxBU+sS!3YJESaaKUmT&6lTeTcxqvHGQuLY|h|#OKGew?h>1R>X{8)<3n7E zR#b?TcRfCFoc$wU?l64&X#H`z)>=NzblxS$GZ)-XO&q(lYxbNB*_DZ>~s9dXV@J@&L_$Gy5<$=3`o794> zwd5UkII9i?i$nr$=yt66RX>=s=wdg*A1|Vk(R#Iq#ql`R=quQjv_Z3p9t=&#!ePRR zO3E)`y}=Di0Y|O~!=ov0jrwvs!Ny9xf4cbsM2jWxU={Wfh>$a!ZfZ{sBiuDp{s0Rn znR46%-ddqa?pg?()`2)!1s{Mb=mV+&M`Ha)e}4d9^WIkk_0V;Q@{tgASF4Ru|=m>Ja?fTYg0Y7-bOfb*vKutrBQ{UtY6?!i) za1EKAc}^K6cL5C1`o%u`41>HJygD@%&c7L}_PB%*l&m`8*hEZqE(~J-GYlO-^A`=UQN4;a1q^%GRKYI@rWZ(Q3`eYgE# z!HR>x(RlYC$su;O0sA(+RU<2-Cl%4sz&+;SPnqCdGvb$^*4`~VF)F$9dUCUyrnx{c zaQK)^V&CTsF0Bg6S!flXgB89%d-bdI729ZMQcm~mtzVaz60nx*+V}gJR|v%}_oi@( z7mNFD>l~2@)df-Iap{&N$C(L*sS|sEg(J>dRs{E8(nI{Yu1FqpwL`RaXnrFsICpqj z-G&r!{u63poLhIKkjA33QftM3FjZrx4L7cXJFr!;bcs_eiJ-vOAVL!)j$ntE5ZhfV z9nld1fFHtf5mPm>^$maIwRW&O?DV`lij`VS+0V@xW-#1fjI z!5m;>L8+e>9XM&738wH_r-0c-zd{4MbQ1|;7%RNORL;BT zzW=n$Vw9&bEeqVnMWD0u@W2CDJ;bm*! zG{f`i11TG6a$?rBkY_$WA|iz< zm}hcEfDle=`AH-QW72a{Tml#> zR2Z4d99ux7KH$KejP)Ifk{IZaaGA87DoJA}sk#qRS0p794$p9i&Sa2?fM=jHKvgkr za`yOhP>|w~?)ENJ5h=iki~RjWyfAsl_FY&x_2(6+L0!|#2&Ne^BZeW6uQo1?q_i&NERXI@gNy8{vAUhpYxe2X}6RR`kw-!7iu zsi0%gsp~;^rSf_h5xO7iKXyNj?a{7N-v`&}-UqW}HZ6<@{d?C}!1IV(QN?ofxqsFC zJKv-eXqb~{G3*8Z`iEeX_ipsI*bo3@onvs(D4!_qur|z6(Yd&{tDTC%EOtyuM9`{_w#kC)K22a9gKvA~CD$ zvhs|X?xwvx}9*`@Z7WA>sISx5$Kz z0^4-c_7w>(J4oW6l#DXj|0yuC$CUMjnaNomGv1G@brYg|CW|j`@II#g>Myl-k{i5= z^$rj=5O%Wu7@^p<>KA$v<@%xE7VG|xLWyt467xK{Ysiv$##COeo-q4epWL^^xF`ymgyI_ z(N_x35#dkpDTCx#?q(IWm{F65*f&ko|7b2|-Pe!nLo-5tDsCl)4g}#l!npJvql= zPd&3JY-E&`SmC>-wsP4xY5i6n=GZ6{i~teRO^h+eR&a^&c%K{@PC4xf-(|hDoF^_= zF@W{hDST#UrQqGW1z2q#7Fn6BU zo;WwsT80jVm48Wo-P&ss?hC!>ZRzA;N|?f>ZR}4{+`_Rnlgw?lb$^Ib%_+~y7x_!) zHE=Z#xu#d`c-CQGaJuI56TzAd>ZM}1MzIa^%{B`Egy>H1ZC@r#f57~iN|?IoUr4Vh z5MXE`h@MC}^@uegFE+_Ku&6n>Hv4*0^<_QA@2>5-h1Hi=1TQCCNOr~FSIu)7#_wAY z#aMVhMGeNwvjql{CdR{D+T`>qH5Jd%_S5O6nb!ZC`B5`6~q2UzvX_40hXK# znk1Se^Gx>L>MaQ_1IOO2-sU`49_ZKFEk{!T{WQ-#E8y73;llHO@kCNVnqt7(rF_Ry zjzLWD&OI2o8B2cPT><^3PkY}g3SJ%zljzsv)C9zLt!%qiI~wRIfsF!a&>cTe4bKU~5(sr5B+>JQX ztf%E-JPhIkeI^OI^uFT%4 z)vuOL7sQSMTR@Q^lK6805}Y%hd~m~ZEN6_MVkU%u0gqYGo(aiTm|{;7+zgU0`@h|S zZ+j9D5@1*Oy&8eKts@5CAi)`kz|U|JeAD1^=M&2D39wz~DG(Ack2Mmu(qU0~d;#)6 zRhSAP>H>@2HvX_e@)Ypyq&L|uh0 z|5L$xa|lvoq?u@k|!4|4PaXtrp4CH z#hdk+-c(Gpz0dvr4g;Wvg;2a>GGV>7*6O@-j#6ylm79;8A>Ez~PoKh7D%DWPxuaIG z&ocKmroVLkA!76+)la+2*{Ih`pK^|m_-m}V?Dp}mP2-*(9ZT1ljHrcwv;8P=WReNu zrC(3s{DkQn9=Rir(;#}9vv`8s(ymS4(Eq4^KVxKsk8*~hoknyZ6b~)PgP28W3J-mQ zx1+dWJZ5s9ECf9d5`%^58$z%5JexSpIfYXtuC6Y=gKvQ}8pIX|*5**7Zw%Vf^*ogR z*ip81@{u2eKmMm<(7qFMl+s)RVP1(ZhjPCMZ>0FVPyF?jSQ1Ki{IGt)))Zp0{aPj| zM+Q=6H9qPW5|8XBc2<1xv>MTZ&}{V{qlurzt=jaBy@w`qvxt@3`XDlV<7m~a#`m-) zwSrs4gPlKBq$gGz!e>roOpe+1r_eWY6)5$!#a*jTK@4~t*=bf|#Po-7;|JTZ!7-uB z^ax-=->@PhhINZ&Xud5XLDyBv9p9I8io39;rYATIu_611WP<0izLfs4#~L)9hjn73NT34i#o( z$T=hYHhP$~jc{zljRA^giO6S#Du!BN%fSXMF!UD!yLh3Ii^sgW ztLa_5w8qGh8PB5aj9$SF5ApBkC*x*(%>VB5;Pp?OoaYK&{|RESU}z!HwH5P{9~{wwo+2+f3_yEC z^><_ML2!&i{DKy8lNVraa=O;b3ICBFSgb)h(f(T^HP(O=Vh!jTyd6aPuL?H`{O*FP zHcY}%%+D@AA&gS2fu9|l1iD79Jmo{}6s`*cd?Ps8XSKd~J)1a1jaH~pyFFBaMJqt` z0f+5(FL^9*pcSa|n+ba}cstDf6b{Mn^sS87rISU|@e0Ks#t&jK7Z82mCrsb)h#4u% zfS8Nr#S;`1b3xyb=I(96JoP&$FCgz7=BeicPd(N+2yS~mvh-WxJq5LKwof2Ck04L| ztk&k8w3#?X1%9I%-Aqs$d+jH0V>2;0Jrne;n!B^Ru$=l;Y8R#(v@SFg>3T5TSS#FE zh(#+TPKE;ZDOOO(a-$}bp%7&<-LQ%!9(GO~TKo`V6%Z!UH@?$7{rd@s&afvChq&&- z6Py&@uT!{2fsDy{xoQac01al287WQFV2@$$?=Er_pL@h#FlV83#}A7q4w}RKUGAKo zKl)a>aAPdi_ms)t-(NSOZ&<|;4|mgdp02?<(t%-c=B)1W6n|*2on$%;O9i2V$B~hD z0Q{}e71Y_MYZ#~%tScVu`Kh8mv0NYe=#q#qIE_+P4LC~!{Tt%F6Gr3gdugDjYsg~V zA087*qwn$T?nh7I{%QYh{^9piCiwKAGmcnia%CtV>(si$JRnj9EufEeL|TIY3y|7< zBl;(1KfV@wVh!!_26&v-=-5VSM3a~Ma@gqm9%+hC^z`KYK7N=xamWJt5>J~v`c}qU z--#n=`4+FPQ@edUcbsJxtPnpGZ8WR(v$!|MigNB$;;+f#vQWC?5C`E#ciS<5U)E$G zWOdwtp)TN_-8b8z`FEyq&$iJzcXD3eH0oi;M1+Q_CqAg%>u*8RXEixbvv%oQo9p)zU0RDf>%VvwjEF;=l=>SvGAqJ>=hLt7?g8uJ71fRdHFO609EgBWM7*lb6r{-b~>b>VbOv@%ZmYLVF(}P!x|Y1VWUz2LQ-L z1>h9!HXcEc_*seQCpRJ<37si|&KULQuAwxdSvx(2^WlQk>(bpawB*qg?ukFpqO(;e zxqEMHB#)CrcfwbRU3@TZZ%FPU{IT`kfdDr93+vo#4;w^V4sAJ^W?y6M@DJN|2A#=D>}T&;uU$($Rz35suj zZrTt&QF!-_aQ{uw++@`YIXg{#uYQWaM{3uIs`sUTyjvg|%M;<{X;wTl614iv4w0^J z**J2!;Y%l{JWkaY-$Dn1J%RRv0ySn?k~wjQhl$?}5fa*tE(l?r{V|=RV53jiZ&Ub5H*N0YYsLwH*(! z+!a0As3E{?Bc+A=)+HTJj}Z#<3JT0#I*>XDg2M;HBF9L$nxCs6oto|x?o5C(B-@K1 zwusz0GgjOQ6W*c~s(NIsdkXifDv9fei165##9uE{Uiwo!)=d7CUQ-cA@hqMkavFq~ zyo6jxP$+clAqMTqTsFVy9Jyn){ty!|*?YM^=uD?2((M4b{!NFMZjhlf3Qwi%N!9No-D8 zt}d=?Z#vv7I=53+#O02r6t9Eod7-VDng1;B-`x4RoRM=lK*NEeBg|y!0pXAvUmw+I z-dGx59Mk#f@TW)-*8&j_Fy7mm7`@hldG}T(bM(; zxVFr!RRsyH0Z->gguh}-yFCUb^^czZKG;m4i!?UG=M>fiV@ccFXno1*(>=>)@y zhBZbBTPYq`FR_rD1CF57aN!jkt`+*z?fyO-f~ z=H6Xmz4;GE?&u%>yh!BY>X)6N?;h9&3W(i|6Y?LBemWszY8ia>$u%ocSM7#P{9ku^ zsdO>DJY&%cyJOkh1A4S-t$y9Qn(BS#&b5h~kDs5l38@RVRVO>pe=EVUKFR)ZRYAa4D6WlBL=mwu5Ex zMHTI=Kn!F)RxTL&mLAdOvF6H~H>>$hs_eS-N}(rVNzoCuGkiq@2mD>;k-m^ZKROh-pASAH z#H#Mj-%gnjI_Ir?sVe2KkhGmgHRdle%yoQJcT?G`-`2Wb*>E0r7U7ghT6EH_8!qB) z*CM?cJ}0nd4&`o6ED^X|d@kpVWuMDryxUQi@a@Hq=F8U|tNE%j*sH$mSgC0FJ^aDL z2RSwn&hPtNf5VonSI~bTF5PJg$1R?}Nal{pxaA($ zTHr_Cf6AUYz2NlPupO1@6?r1j%U|~W(pKyBwDVR@-jo^@E_nFM{IHZwJg=jlKz&fI4PwbvT*vC zcZE-fUbD6E)+R`gpodG;?LVhFKhst02SbU>ai)?*=i92^bQv!pU%yq2uS@8zH#&cj zT+dz@^z$^SORaTL3}x|=??>{}`{dpZo)om_|3~InB8$@Dpb)tX12ZZ?Z8HhO-UTu2DetTyl&|a;&@Zs zU!M1}f-rIAwCrCQWNFU;+~BU(h~|SAETp=QZq4kuK41}%x2Rd-*|iVNpNGKLIQ*KL zh#ImtvsS8c4;it#yM6vVP;e2iGprpR`=&!Zazk$7m+a*G9Gjo6@qIPI(=uvJWK-+f zHidhw0$~(<+T;e|F#y7N+gbiIl0?DXP2D8Ffm>C*u_=1EPpcWCpDN=R`0aa!pRl@2 z;W&+2Str?S$F%dNaBWb1$1kJbmvUZ?oYFuXI8lB^V^lxQomldwjX1w2u|a9)3GG2q zUHRw+{uMZ`ig|W-AOCkh@vUXyaTrtR=RUG{vvAew>gJ7R3q$7lT{$y_6Z6epoRP4# z=gBYLxBu0bAJ~rNsSQGDz#)duP)2*Ex`&IqRmpfvkV4mSFB#s9u%!px&Ihwv+JE5g zX7==+j60(-l4q9lE%Csn7ID#Lm7_&RiG9~Bi3d3+H>}XMzI`%8#kviD=;X2ZuA%~o z@x|;bo{Ze&EVE7wez>L0&}e%tSu=a-;=4*OJVZ(*>?+^5T)E#+E-ZCH_Kd|P>sl6-%S}CiY9TLSo zzRHNciGTexu1_hUDKC9**8H{!O=C z?wm2+JqQ>|T=iL| zh`s*k>YzSB#?$rN5|->dvnIPcrhq}NSNZBL>9(DxUKRX$sb#gA+2f1b-eaxt_GYzh zBpD6UqN0!{2|w92gn9cqw`^U*{{;McH8pLH?H`>}j4ScGPHgF0^Tz%A8x7fK6%FiO zFF1o^-*MSh|8+_=gVXZiI&-fe*W6qRbHUq;jefFX{bj*f-|ofKyX}Qo2dAAW8`ho{ zc5Zii`Le>S;eP1F_av79XUlQTw&tB{E`Qx2wu|ZDezgt#M|A}~Eqhxo{SfQ_5t{m~ zpy6@6Vq&LPvc5-<gBJz1-b* zd#Jewf6!wGc6q5Wie9#KSjyGK?#hY#4QDTpw<5H zXeGRF3jXZ(&f%zh*t(;S1}d)MAWH3DuCxx}#pFGQ+Ku>)vOjzbJmjc(K{-hIn(kO* zQ)fSp!{|bSOkV%Np#8Od-`{uFr8tR=ZFQ=&8osPKfs#^O&%^L~bmE2Dg5f^S zH;%1+?8~`yQ#Dppe7NSjk5%UFVcnPQtRZd<4-+0;Z|i$jE_pn>y_nOGq56m3!~U|f z2KS%1Pcp1wJ}vy5oA+NQ>v~)~DmH}^^#@v75IQLl@mL)&n4S1)Rb_E6C=hz0N!;qm z$qybB+{usV_~HKfoLJM(*H109v)1_RUEQA8!12DO{8oRI{)u&ChU7OB`BS)Ko&~j^ zPYd9Na~F@sY4n%H{<7}~$kP%pR^CvM_@f_X>Jqb?%TFDjbhv!ytnV$MX8|d*jlEvS`lZ_VA20b9{(P&u zSi51h{PQMP%uUgX_{J=)d zoaJaTxV_Bx#8}24-xN;p=EgRccku@YC?lO#_YTD?N%rn-S7GNtey4i3GPGfi;u+)6FNV%`$QLwdHlNHq&Cnn zHvXulz4?)&eMVO^OU*J5ONj`Bq|SZjjf)e#QX_}0`yb%w^xjZSf$a$?jp>Bkrd}9<^4XO7Z^Yf|RW}PNwS}3ohTwbGUj{ zaG{pR3;UONhED`idCPNQ?`F}M=RpNlO7?dfKi%SwBgm93RSrW%HVF89A~t`KD?Q?D>Ry194P#% z$92t0XR1oHf2v(({Uv-SY4MrIUVT3X$+pE)xbH?^ZGK&#NbhcNHv+U^$!GN~|_0KPn_uD(BS$_*7C@F-0;)>!5FIqP5>H_bpI94-t zK^0H!fC{n*5UYnOttZCh6gHeVJy7X)Bw9?(`<;qqVKDRY&id~xx$6TB2Wy)OO<%lW zU5gX^xruzp=*44EA?bnilDVWdFh*1 zM%FEJBR@#+Sm}L|PpO@3HfolmP@ZRfqQzg)CVAIVeUHlp4$FROeV5)hF;H6{P@i2gjBE=que?Id4`g2+m{qvVC8UsW0iW9=Y59%&hRj zuV>L4Y?{{o<>8rj`=I9XTV~-wUj{3-?h!7v=etCfYb_Sm?%XP#oO?*&`pZ459oEVV zdCFQ8{#r3;ZZ~>4xkt${z99Ia6sf!}=nxOzZJWHsc>CnXs=w-6(mig-EqCv>T{?N; zU319_dF`6YwD_PkM-7I>Zf$TMqBw*t(EA^mIB>3@%K!MnF^`r!T~O?JXXGGjPb5X; z;g1J$oX&O)Zq9vqTU>)533#j2JWKq!fzUGCpZKmQcMQUtM|viuU*mIZkB1xbBv{ls ztQ($piudm1eYiv*I!)$K>I;rs@wsZhtZcpdV)syrmZ3J~vG7>!=WlgxRx`0xmxAMW%(bcdA zq#&VDH)NmTjq1w|b|?MU3tid7-!p2J?XJ~8Ty*wr{evABIlCnsLv$+1Z%dA>%dIU_ z|5ff`6-1G6k9o09+a;@fzM|79FH#-H>Wx{iL=&@04~}ZT5cqZOh-hKe&2wv?uwM&5 zr7f=H;2D&l#f5X~K8qVji2MvG67`x-yNGZ0QM{{X5q_yb@VTeM=YzSedt;_>ivAQB z>cfgMB9F3ud+h3awH+6h5AME7dhiv zida{Mk<}DVt8n+FPoAQBXAdVH`#JHW^vKytzUX?}CG$^y;k$PyK=Sq^`7W_*ZGQ2@ zrXEFOXX~=SRg3mEYehU2XW1#<`=;Vp(NSSry=z%y#(_t@lat?L#!tKrm47^TbD6HH z?pWw+^)PshocS*U~v?wIy)ceTKBf;73 za!<~#tW`FdAANGM+|}p;VQHys#h>OwGCzZacC@5<+>C3N6FwtS=(V7OS?9tY8M(-} zp^0t5m;L#N4j)wcrXaU$bfRsz#OY+e^z);A1cwA08{tQ**DEuHoKAbG_Pf**vYaJ1D=uhDNFuzC-?Oi$!EOE<5p^as8d}`Zzy8TqqZgk zLG=ksw05(nSVegpP#CXo^l0@`D>H6(zqMrl{KEsj{NZO>gNvdV zb$S~4%iME~{?r{yYDS?H>NY-#Nw3=I16-@B1lhP~p!wR!Zlet?8a{XDv-(O>CML!g zwAj?2q75wc4_nV3*5Ld-!n-H0=H{FT{h68F1!xP zgv~5|nVh=P%eAKzBi}GTH+-&^fP z@o3$ZP+t6D zZTmJ8p%|}9n}bV#3TI9D_q-@}9t#M-^{UD^M_X4spLCwsaX+M^u0c#U`S5j@OFW-{ z@tc)f^jn1v6B#PJWFw;A)rYgSR0Q3tme0Sy_QhdBsO`SB1mo$(#(1rf(~YkZPG4zy zwf5#;yFx`tP~>gS)jJ1+|0i%DWz%Nh1)jD=lB5(Z8?1AONqcz{z$c zXxpZg%Qk#1`Bg7j=j*bnUOh&FjZU*H?4QBR4;uDtW}wzeAg`RlUE`d>NsJ_t_kdJ_ zgtq=X%0gl#xay-$8&PC$sjej3R4G)jz|%6VE{hG-jKVLcE!Gs&OH!T8yr{#!kA3rS z`_=kTDEdej8o0#zlg-XgnkNX#VK&>JrbLTNz0^LcE%IRNfLvKguPm`uqL%!;j5Xn; zaJE;?XuwyeB<}Rod(i97{t$Mn??*?^z28&RbXn)iJeGG1t9ETJQ);jFD-dQnyQpH- zgy7?q52A8o&tHz%wxh{`(PGI}`gai^znWEr*d!b7WwUxjMG2YZA2q8cCG(Cuk)f1vyB z?U6<`UmOYB|JNqE3$5VapQR+k7Qy6KTk+iV@fY~C4)~r?(9NQ8n;+=Q)>vJr(HHIS zs8P!Qsygw^bo->X+6Qyi8yP4w_wZSqulfXByYMpB{_*_=lkWJFastAPI mini documentary released at the end of 2025, you can watch it online: + +FastAPI Mini Documentary + ## **Typer**, the FastAPI of CLIs { #typer-the-fastapi-of-clis } From 23caa2709be0d7dfb18b388299235a83258a19c9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 25 Dec 2025 11:02:01 +0000 Subject: [PATCH 006/243] =?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 30d5c4403..dbaa2db87 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Add documentary to website. PR [#14600](https://github.com/fastapi/fastapi/pull/14600) by [@tiangolo](https://github.com/tiangolo). + ### Translations * 🌐 Update translations for de (update-outdated). PR [#14581](https://github.com/fastapi/fastapi/pull/14581) by [@nilslindemann](https://github.com/nilslindemann). From a4d04c9b7e97ee5aff0c27e5ea92225f77c7a348 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 26 Dec 2025 09:53:59 +0100 Subject: [PATCH 007/243] =?UTF-8?q?=F0=9F=91=B7=20Remove=20`lint`=20job=20?= =?UTF-8?q?from=20`test`=20CI=20workflow=20(#14593)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5a12d69c8..3ad630d94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,29 +16,6 @@ env: UV_SYSTEM_PYTHON: 1 jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.11" - - name: Setup uv - uses: astral-sh/setup-uv@v7 - with: - cache-dependency-glob: | - requirements**.txt - pyproject.toml - - name: Install Dependencies - run: uv pip install -r requirements-tests.txt - - name: Lint - run: bash scripts/lint.sh - test: strategy: matrix: From 55b556a7d1cc71cca9f557aa82b144183e2da055 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 08:54:23 +0000 Subject: [PATCH 008/243] =?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 dbaa2db87..d54193ffe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Internal +* 👷 Remove `lint` job from `test` CI workflow. PR [#14593](https://github.com/fastapi/fastapi/pull/14593) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo). * 👷 Run CodSpeed tests in parallel to other tests to speed up CI. PR [#14586](https://github.com/fastapi/fastapi/pull/14586) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update scripts and pre-commit to autofix files. PR [#14585](https://github.com/fastapi/fastapi/pull/14585) by [@tiangolo](https://github.com/tiangolo). From 7c751a2e1cea212a6a39032a9aac0fab937a2b20 Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Fri, 26 Dec 2025 10:39:53 +0100 Subject: [PATCH 009/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20de=20(update-outdated)=20(#14602)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sync with #14600 * A few changes The LLM suggested a few changes when retranslating the document, these are the good ones. I also added a term to the llm prompt, the LLM instead used just "Abdeckung", which is too broad in this context. --- docs/de/docs/index.md | 18 ++++++++++++------ docs/de/llm-prompt.md | 1 + 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index 1920df8ff..11fb6c983 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -117,6 +117,12 @@ Seine Schlüssel-Merkmale sind: --- +## FastAPI Mini-Dokumentarfilm { #fastapi-mini-documentary } + +Es gibt einen FastAPI-Mini-Dokumentarfilm, veröffentlicht Ende 2025, Sie können ihn online ansehen: + +FastAPI Mini-Dokumentarfilm + ## **Typer**, das FastAPI der CLIs { #typer-the-fastapi-of-clis } @@ -233,7 +239,7 @@ INFO: Application startup complete.

-Was der Befehl fastapi dev main.py macht ... +Über den Befehl fastapi dev main.py ... Der Befehl `fastapi dev` liest Ihre `main.py`-Datei, erkennt die **FastAPI**-App darin und startet einen Server mit Uvicorn. @@ -276,7 +282,7 @@ Sie sehen die alternative automatische Dokumentation (bereitgestellt von Body eines `PUT`-Requests zu empfangen. @@ -326,7 +332,7 @@ Gehen Sie jetzt auf Dependency Injection**. +* Ein sehr leistungsfähiges und einfach zu bedienendes System für **Dependency Injection**. * Sicherheit und Authentifizierung, einschließlich Unterstützung für **OAuth2** mit **JWT-Tokens** und **HTTP Basic** Authentifizierung. * Fortgeschrittenere (aber ebenso einfache) Techniken zur Deklaration **tief verschachtelter JSON-Modelle** (dank Pydantic). * **GraphQL**-Integration mit Strawberry und anderen Bibliotheken. @@ -452,7 +458,7 @@ Für ein vollständigeres Beispiel, mit weiteren Funktionen, siehe das FastAPI Cloud deployen, treten Sie der Warteliste bei, falls noch nicht geschehen. 🚀 +Optional können Sie Ihre FastAPI-App in die FastAPI Cloud deployen, gehen Sie und treten Sie der Warteliste bei, falls noch nicht geschehen. 🚀 Wenn Sie bereits ein **FastAPI Cloud**-Konto haben (wir haben Sie von der Warteliste eingeladen 😉), können Sie Ihre Anwendung mit einem einzigen Befehl deployen. @@ -494,7 +500,7 @@ Es vereinfacht den Prozess des **Erstellens**, **Deployens** und **Zugreifens** Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉 -FastAPI Cloud ist der Hauptsponsor und Finanzierer der „FastAPI and friends“ Open-Source-Projekte. ✨ +FastAPI Cloud ist der Hauptsponsor und Finanzierer der *FastAPI and friends* Open-Source-Projekte. ✨ #### Bei anderen Cloudanbietern deployen { #deploy-to-other-cloud-providers } diff --git a/docs/de/llm-prompt.md b/docs/de/llm-prompt.md index 5df904ac7..35ca9f069 100644 --- a/docs/de/llm-prompt.md +++ b/docs/de/llm-prompt.md @@ -251,6 +251,7 @@ Below is a list of English terms and their preferred German translations, separa * «the button»: «der Button» * «the cloud provider»: «der Cloudanbieter» * «the CLI»: «Das CLI» +* «the coverage»: «Die Testabdeckung» * «the command line interface»: «Das Kommandozeileninterface» * «the default value»: «der Defaultwert» * «the default value»: NOT «der Standardwert» From 5eb8d6ed8ac4e638f4d5883c954a8031ef3f3c42 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 09:40:18 +0000 Subject: [PATCH 010/243] =?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 d54193ffe..bc2441f7f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Update translations for de (update-outdated). PR [#14602](https://github.com/fastapi/fastapi/pull/14602) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Update translations for de (update-outdated). PR [#14581](https://github.com/fastapi/fastapi/pull/14581) by [@nilslindemann](https://github.com/nilslindemann). ### Internal From 3063ada72f4cd493393e1d8f13bb6498f8d5cb93 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 26 Dec 2025 11:43:02 +0100 Subject: [PATCH 011/243] =?UTF-8?q?=E2=9C=85=20Add=20missing=20tests=20for?= =?UTF-8?q?=20code=20examples=20(#14569)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: github-actions[bot] Co-authored-by: Nils-Hero Lindemann --- docs/de/docs/advanced/dataclasses.md | 6 +- docs/de/docs/how-to/graphql.md | 2 +- docs/en/docs/advanced/dataclasses.md | 6 +- docs/en/docs/how-to/graphql.md | 2 +- docs/es/docs/advanced/dataclasses.md | 6 +- docs/es/docs/how-to/graphql.md | 2 +- docs/pt/docs/advanced/dataclasses.md | 6 +- docs/pt/docs/how-to/graphql.md | 2 +- docs/ru/docs/advanced/dataclasses.md | 6 +- docs/ru/docs/how-to/graphql.md | 2 +- docs/zh/docs/advanced/dataclasses.md | 6 +- docs_src/additional_responses/__init__.py | 0 docs_src/additional_status_codes/__init__.py | 0 docs_src/advanced_middleware/__init__.py | 0 .../__init__.py | 0 docs_src/background_tasks/__init__.py | 0 docs_src/behind_a_proxy/__init__.py | 0 docs_src/body/__init__.py | 0 docs_src/body_fields/__init__.py | 0 docs_src/body_multiple_params/__init__.py | 0 docs_src/body_nested_models/__init__.py | 0 docs_src/body_updates/__init__.py | 0 docs_src/conditional_openapi/__init__.py | 0 docs_src/configure_swagger_ui/__init__.py | 0 docs_src/cookie_param_models/__init__.py | 0 docs_src/cookie_params/__init__.py | 0 docs_src/cors/__init__.py | 0 docs_src/custom_docs_ui/__init__.py | 0 docs_src/custom_request_and_route/__init__.py | 0 docs_src/custom_response/__init__.py | 0 docs_src/dataclasses_/__init__.py | 0 .../tutorial001_py310.py | 0 .../tutorial001_py39.py | 0 .../tutorial002_py310.py | 0 .../tutorial002_py39.py | 0 .../tutorial003_py310.py | 0 .../tutorial003_py39.py | 0 docs_src/debugging/__init__.py | 0 docs_src/dependencies/__init__.py | 0 docs_src/dependency_testing/__init__.py | 0 docs_src/encoder/__init__.py | 0 docs_src/events/__init__.py | 0 docs_src/extending_openapi/__init__.py | 0 docs_src/extra_data_types/__init__.py | 0 docs_src/extra_models/__init__.py | 0 docs_src/first_steps/__init__.py | 0 docs_src/generate_clients/__init__.py | 0 docs_src/graphql_/__init__.py | 0 .../{graphql => graphql_}/tutorial001_py39.py | 0 docs_src/handling_errors/__init__.py | 0 docs_src/header_param_models/__init__.py | 0 docs_src/header_params/__init__.py | 0 docs_src/metadata/__init__.py | 0 docs_src/middleware/__init__.py | 0 docs_src/openapi_callbacks/__init__.py | 0 docs_src/openapi_webhooks/__init__.py | 0 .../__init__.py | 0 .../path_operation_configuration/__init__.py | 0 docs_src/path_params/__init__.py | 0 .../__init__.py | 0 docs_src/pydantic_v1_in_v2/__init__.py | 0 docs_src/python_types/__init__.py | 0 docs_src/query_param_models/__init__.py | 0 docs_src/query_params/__init__.py | 0 .../query_params_str_validations/__init__.py | 0 docs_src/request_files/__init__.py | 0 docs_src/request_form_models/__init__.py | 0 docs_src/request_forms/__init__.py | 0 docs_src/request_forms_and_files/__init__.py | 0 .../response_change_status_code/__init__.py | 0 docs_src/response_cookies/__init__.py | 0 docs_src/response_directly/__init__.py | 0 docs_src/response_headers/__init__.py | 0 docs_src/response_model/__init__.py | 0 docs_src/response_status_code/__init__.py | 0 docs_src/schema_extra_example/__init__.py | 0 docs_src/security/__init__.py | 0 docs_src/separate_openapi_schemas/__init__.py | 0 docs_src/settings/__init__.py | 0 docs_src/static_files/__init__.py | 0 docs_src/sub_applications/__init__.py | 0 docs_src/templates/__init__.py | 0 docs_src/templates/static/__init__.py | 0 docs_src/templates/templates/__init__.py | 0 docs_src/using_request_directly/__init__.py | 0 docs_src/wsgi/__init__.py | 0 pyproject.toml | 11 + requirements-tests.txt | 1 + .../test_body/test_tutorial002.py | 161 ++++++++ .../test_body/test_tutorial003.py | 171 +++++++++ .../test_body/test_tutorial004.py | 182 +++++++++ .../test_tutorial002.py | 361 +++++++++++++++++ .../test_tutorial004.py | 290 ++++++++++++++ .../test_tutorial005.py | 272 +++++++++++++ ...est_tutorial001_tutorial002_tutorial003.py | 251 ++++++++++++ .../test_tutorial004.py | 275 +++++++++++++ .../test_tutorial005.py | 301 +++++++++++++++ .../test_tutorial006.py | 269 +++++++++++++ .../test_tutorial007.py | 344 +++++++++++++++++ .../test_tutorial008.py | 157 ++++++++ .../test_body_updates/test_tutorial002.py | 207 ++++++++++ .../test_custom_response/test_tutorial001.py | 20 +- ...est_tutorial002_tutorial003_tutorial004.py | 68 ++++ .../test_dataclasses/test_tutorial001.py | 2 +- .../test_dataclasses/test_tutorial002.py | 2 +- .../test_dataclasses/test_tutorial003.py | 2 +- .../test_tutorial/test_debugging/__init__.py | 0 .../test_debugging/test_tutorial001.py | 64 +++ ....py => test_tutorial001_tutorial001_02.py} | 31 +- ...st_tutorial002_tutorial003_tutorial004.py} | 23 +- .../test_dependencies/test_tutorial005.py | 139 +++++++ .../test_dependencies/test_tutorial007.py | 24 ++ .../test_dependencies/test_tutorial008.py | 58 +++ .../test_dependencies/test_tutorial010.py | 29 ++ .../test_dependencies/test_tutorial011.py | 120 ++++++ tests/test_tutorial/test_encoder/__init__.py | 0 .../test_encoder/test_tutorial001.py | 208 ++++++++++ .../test_tutorial001_tutorial002.py | 156 ++++++++ ...st_tutorial001_tutorial002_tutorial003.py} | 19 +- .../test_generate_clients/test_tutorial001.py | 142 +++++++ .../test_generate_clients/test_tutorial002.py | 187 +++++++++ .../test_generate_clients/test_tutorial004.py | 230 +++++++++++ tests/test_tutorial/test_graphql/__init__.py | 0 .../test_graphql/test_tutorial001.py | 70 ++++ .../test_tutorial002.py} | 36 +- .../test_metadata/test_tutorial003.py | 53 +++ .../test_tutorial/test_middleware/__init__.py | 0 .../test_middleware/test_tutorial001.py | 24 ++ .../test_tutorial001.py | 186 +++++++++ .../test_tutorial002.py | 223 +++++++++++ .../test_tutorial003_tutorial004.py | 208 ++++++++++ .../test_path_params/test_tutorial001.py | 116 ++++++ .../test_path_params/test_tutorial002.py | 124 ++++++ .../test_path_params/test_tutorial003.py | 133 +++++++ .../test_path_params/test_tutorial003b.py | 44 +++ .../__init__.py | 0 .../test_tutorial001.py | 164 ++++++++ .../test_tutorial002_tutorial003.py | 170 ++++++++ .../test_tutorial004.py | 185 +++++++++ .../test_tutorial005.py | 202 ++++++++++ .../test_tutorial006.py | 221 +++++++++++ .../test_python_types/__init__.py | 0 .../test_tutorial001_tutorial002.py | 18 + .../test_python_types/test_tutorial003.py | 12 + .../test_python_types/test_tutorial004.py | 5 + .../test_python_types/test_tutorial005.py | 12 + .../test_python_types/test_tutorial006.py | 16 + .../test_python_types/test_tutorial007.py | 8 + .../test_python_types/test_tutorial008.py | 17 + .../test_python_types/test_tutorial008b.py | 27 ++ .../test_tutorial009_tutorial009b.py | 33 ++ .../test_python_types/test_tutorial009c.py | 33 ++ .../test_python_types/test_tutorial010.py | 5 + .../test_python_types/test_tutorial011.py | 25 ++ .../test_python_types/test_tutorial012.py | 7 + .../test_python_types/test_tutorial013.py | 5 + .../test_query_params/test_tutorial001.py | 126 ++++++ .../test_query_params/test_tutorial002.py | 127 ++++++ .../test_query_params/test_tutorial003.py | 148 +++++++ .../test_query_params/test_tutorial004.py | 156 ++++++++ .../test_tutorial001.py | 121 ++++++ .../test_tutorial002.py | 142 +++++++ .../test_tutorial003.py | 153 ++++++++ .../test_tutorial004.py | 147 +++++++ .../test_tutorial005.py | 131 +++++++ .../test_tutorial006.py | 136 +++++++ .../test_tutorial006c.py | 148 +++++++ .../test_tutorial007.py | 136 +++++++ .../test_tutorial008.py | 138 +++++++ .../test_tutorial009.py | 123 ++++++ .../test_tutorial002.py | 65 ++++ .../test_tutorial001_tutorial001_01.py | 193 ++++++++++ .../test_response_model/test_tutorial002.py | 129 +++++++ .../test_response_status_code/__init__.py | 0 .../test_tutorial001_tutorial002.py | 96 +++++ .../test_tutorial002.py | 141 +++++++ .../test_tutorial003.py | 143 +++++++ .../test_security/test_tutorial002.py | 71 ++++ .../test_security/test_tutorial004.py | 363 ++++++++++++++++++ .../test_security/test_tutorial007.py | 89 +++++ .../test_tutorial/test_settings/test_app01.py | 78 ++++ .../test_static_files/__init__.py | 0 .../test_static_files/test_tutorial001.py | 40 ++ 183 files changed, 10459 insertions(+), 86 deletions(-) create mode 100644 docs_src/additional_responses/__init__.py create mode 100644 docs_src/additional_status_codes/__init__.py create mode 100644 docs_src/advanced_middleware/__init__.py create mode 100644 docs_src/authentication_error_status_code/__init__.py create mode 100644 docs_src/background_tasks/__init__.py create mode 100644 docs_src/behind_a_proxy/__init__.py create mode 100644 docs_src/body/__init__.py create mode 100644 docs_src/body_fields/__init__.py create mode 100644 docs_src/body_multiple_params/__init__.py create mode 100644 docs_src/body_nested_models/__init__.py create mode 100644 docs_src/body_updates/__init__.py create mode 100644 docs_src/conditional_openapi/__init__.py create mode 100644 docs_src/configure_swagger_ui/__init__.py create mode 100644 docs_src/cookie_param_models/__init__.py create mode 100644 docs_src/cookie_params/__init__.py create mode 100644 docs_src/cors/__init__.py create mode 100644 docs_src/custom_docs_ui/__init__.py create mode 100644 docs_src/custom_request_and_route/__init__.py create mode 100644 docs_src/custom_response/__init__.py create mode 100644 docs_src/dataclasses_/__init__.py rename docs_src/{dataclasses => dataclasses_}/tutorial001_py310.py (100%) rename docs_src/{dataclasses => dataclasses_}/tutorial001_py39.py (100%) rename docs_src/{dataclasses => dataclasses_}/tutorial002_py310.py (100%) rename docs_src/{dataclasses => dataclasses_}/tutorial002_py39.py (100%) rename docs_src/{dataclasses => dataclasses_}/tutorial003_py310.py (100%) rename docs_src/{dataclasses => dataclasses_}/tutorial003_py39.py (100%) create mode 100644 docs_src/debugging/__init__.py create mode 100644 docs_src/dependencies/__init__.py create mode 100644 docs_src/dependency_testing/__init__.py create mode 100644 docs_src/encoder/__init__.py create mode 100644 docs_src/events/__init__.py create mode 100644 docs_src/extending_openapi/__init__.py create mode 100644 docs_src/extra_data_types/__init__.py create mode 100644 docs_src/extra_models/__init__.py create mode 100644 docs_src/first_steps/__init__.py create mode 100644 docs_src/generate_clients/__init__.py create mode 100644 docs_src/graphql_/__init__.py rename docs_src/{graphql => graphql_}/tutorial001_py39.py (100%) create mode 100644 docs_src/handling_errors/__init__.py create mode 100644 docs_src/header_param_models/__init__.py create mode 100644 docs_src/header_params/__init__.py create mode 100644 docs_src/metadata/__init__.py create mode 100644 docs_src/middleware/__init__.py create mode 100644 docs_src/openapi_callbacks/__init__.py create mode 100644 docs_src/openapi_webhooks/__init__.py create mode 100644 docs_src/path_operation_advanced_configuration/__init__.py create mode 100644 docs_src/path_operation_configuration/__init__.py create mode 100644 docs_src/path_params/__init__.py create mode 100644 docs_src/path_params_numeric_validations/__init__.py create mode 100644 docs_src/pydantic_v1_in_v2/__init__.py create mode 100644 docs_src/python_types/__init__.py create mode 100644 docs_src/query_param_models/__init__.py create mode 100644 docs_src/query_params/__init__.py create mode 100644 docs_src/query_params_str_validations/__init__.py create mode 100644 docs_src/request_files/__init__.py create mode 100644 docs_src/request_form_models/__init__.py create mode 100644 docs_src/request_forms/__init__.py create mode 100644 docs_src/request_forms_and_files/__init__.py create mode 100644 docs_src/response_change_status_code/__init__.py create mode 100644 docs_src/response_cookies/__init__.py create mode 100644 docs_src/response_directly/__init__.py create mode 100644 docs_src/response_headers/__init__.py create mode 100644 docs_src/response_model/__init__.py create mode 100644 docs_src/response_status_code/__init__.py create mode 100644 docs_src/schema_extra_example/__init__.py create mode 100644 docs_src/security/__init__.py create mode 100644 docs_src/separate_openapi_schemas/__init__.py create mode 100644 docs_src/settings/__init__.py create mode 100644 docs_src/static_files/__init__.py create mode 100644 docs_src/sub_applications/__init__.py create mode 100644 docs_src/templates/__init__.py create mode 100644 docs_src/templates/static/__init__.py create mode 100644 docs_src/templates/templates/__init__.py create mode 100644 docs_src/using_request_directly/__init__.py create mode 100644 docs_src/wsgi/__init__.py create mode 100644 tests/test_tutorial/test_body/test_tutorial002.py create mode 100644 tests/test_tutorial/test_body/test_tutorial003.py create mode 100644 tests/test_tutorial/test_body/test_tutorial004.py create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial002.py create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial004.py create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial005.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial004.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial005.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial006.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial007.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial008.py create mode 100644 tests/test_tutorial/test_body_updates/test_tutorial002.py create mode 100644 tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py create mode 100644 tests/test_tutorial/test_debugging/__init__.py create mode 100644 tests/test_tutorial/test_debugging/test_tutorial001.py rename tests/test_tutorial/test_dependencies/{test_tutorial001.py => test_tutorial001_tutorial001_02.py} (86%) rename tests/test_tutorial/test_dependencies/{test_tutorial004.py => test_tutorial002_tutorial003_tutorial004.py} (89%) create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial005.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial007.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial008.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial010.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial011.py create mode 100644 tests/test_tutorial/test_encoder/__init__.py create mode 100644 tests/test_tutorial/test_encoder/test_tutorial001.py create mode 100644 tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py rename tests/test_tutorial/test_first_steps/{test_tutorial001.py => test_tutorial001_tutorial002_tutorial003.py} (70%) create mode 100644 tests/test_tutorial/test_generate_clients/test_tutorial001.py create mode 100644 tests/test_tutorial/test_generate_clients/test_tutorial002.py create mode 100644 tests/test_tutorial/test_generate_clients/test_tutorial004.py create mode 100644 tests/test_tutorial/test_graphql/__init__.py create mode 100644 tests/test_tutorial/test_graphql/test_tutorial001.py rename tests/test_tutorial/{test_custom_response/test_tutorial004.py => test_metadata/test_tutorial002.py} (61%) create mode 100644 tests/test_tutorial/test_metadata/test_tutorial003.py create mode 100644 tests/test_tutorial/test_middleware/__init__.py create mode 100644 tests/test_tutorial/test_middleware/test_tutorial001.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py create mode 100644 tests/test_tutorial/test_path_params/test_tutorial001.py create mode 100644 tests/test_tutorial/test_path_params/test_tutorial002.py create mode 100644 tests/test_tutorial/test_path_params/test_tutorial003.py create mode 100644 tests/test_tutorial/test_path_params/test_tutorial003b.py create mode 100644 tests/test_tutorial/test_path_params_numeric_validations/__init__.py create mode 100644 tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py create mode 100644 tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py create mode 100644 tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py create mode 100644 tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py create mode 100644 tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py create mode 100644 tests/test_tutorial/test_python_types/__init__.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial003.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial004.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial005.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial006.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial007.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial008.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial008b.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial009c.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial010.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial011.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial012.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial013.py create mode 100644 tests/test_tutorial/test_query_params/test_tutorial001.py create mode 100644 tests/test_tutorial/test_query_params/test_tutorial002.py create mode 100644 tests/test_tutorial/test_query_params/test_tutorial003.py create mode 100644 tests/test_tutorial/test_query_params/test_tutorial004.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py create mode 100644 tests/test_tutorial/test_response_directly/test_tutorial002.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial002.py create mode 100644 tests/test_tutorial/test_response_status_code/__init__.py create mode 100644 tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial002.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial003.py create mode 100644 tests/test_tutorial/test_security/test_tutorial002.py create mode 100644 tests/test_tutorial/test_security/test_tutorial004.py create mode 100644 tests/test_tutorial/test_security/test_tutorial007.py create mode 100644 tests/test_tutorial/test_settings/test_app01.py create mode 100644 tests/test_tutorial/test_static_files/__init__.py create mode 100644 tests/test_tutorial/test_static_files/test_tutorial001.py diff --git a/docs/de/docs/advanced/dataclasses.md b/docs/de/docs/advanced/dataclasses.md index e2d59c776..52b9634ae 100644 --- a/docs/de/docs/advanced/dataclasses.md +++ b/docs/de/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI basiert auf **Pydantic**, und ich habe Ihnen gezeigt, wie Sie Pydantic-M Aber FastAPI unterstützt auf die gleiche Weise auch die Verwendung von `dataclasses`: -{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} Das ist dank **Pydantic** ebenfalls möglich, da es `dataclasses` intern unterstützt. @@ -32,7 +32,7 @@ Wenn Sie jedoch eine Menge Datenklassen herumliegen haben, ist dies ein guter Tr Sie können `dataclasses` auch im Parameter `response_model` verwenden: -{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} Die Datenklasse wird automatisch in eine Pydantic-Datenklasse konvertiert. @@ -48,7 +48,7 @@ In einigen Fällen müssen Sie möglicherweise immer noch Pydantics Version von In diesem Fall können Sie einfach die Standard-`dataclasses` durch `pydantic.dataclasses` ersetzen, was einen direkten Ersatz darstellt: -{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Wir importieren `field` weiterhin von Standard-`dataclasses`. diff --git a/docs/de/docs/how-to/graphql.md b/docs/de/docs/how-to/graphql.md index 0583faf4a..5c908cec4 100644 --- a/docs/de/docs/how-to/graphql.md +++ b/docs/de/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Abhängig von Ihrem Anwendungsfall könnten Sie eine andere Bibliothek vorziehen Hier ist eine kleine Vorschau, wie Sie Strawberry mit FastAPI integrieren können: -{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} Weitere Informationen zu Strawberry finden Sie in der Strawberry-Dokumentation. diff --git a/docs/en/docs/advanced/dataclasses.md b/docs/en/docs/advanced/dataclasses.md index 574beb65f..dbc91409a 100644 --- a/docs/en/docs/advanced/dataclasses.md +++ b/docs/en/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI is built on top of **Pydantic**, and I have been showing you how to use But FastAPI also supports using `dataclasses` the same way: -{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} This is still supported thanks to **Pydantic**, as it has internal support for `dataclasses`. @@ -32,7 +32,7 @@ But if you have a bunch of dataclasses laying around, this is a nice trick to us You can also use `dataclasses` in the `response_model` parameter: -{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} The dataclass will be automatically converted to a Pydantic dataclass. @@ -48,7 +48,7 @@ In some cases, you might still have to use Pydantic's version of `dataclasses`. In that case, you can simply swap the standard `dataclasses` with `pydantic.dataclasses`, which is a drop-in replacement: -{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. We still import `field` from standard `dataclasses`. diff --git a/docs/en/docs/how-to/graphql.md b/docs/en/docs/how-to/graphql.md index a002c08ca..666f819b0 100644 --- a/docs/en/docs/how-to/graphql.md +++ b/docs/en/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Depending on your use case, you might prefer to use a different library, but if Here's a small preview of how you could integrate Strawberry with FastAPI: -{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} You can learn more about Strawberry in the Strawberry documentation. diff --git a/docs/es/docs/advanced/dataclasses.md b/docs/es/docs/advanced/dataclasses.md index 8d96171c7..3a07482ad 100644 --- a/docs/es/docs/advanced/dataclasses.md +++ b/docs/es/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI está construido sobre **Pydantic**, y te he estado mostrando cómo usar Pero FastAPI también soporta el uso de `dataclasses` de la misma manera: -{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} Esto sigue siendo soportado gracias a **Pydantic**, ya que tiene soporte interno para `dataclasses`. @@ -32,7 +32,7 @@ Pero si tienes un montón de dataclasses por ahí, este es un buen truco para us También puedes usar `dataclasses` en el parámetro `response_model`: -{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} El dataclass será automáticamente convertido a un dataclass de Pydantic. @@ -48,7 +48,7 @@ En algunos casos, todavía podrías tener que usar la versión de `dataclasses` En ese caso, simplemente puedes intercambiar los `dataclasses` estándar con `pydantic.dataclasses`, que es un reemplazo directo: -{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Todavía importamos `field` de los `dataclasses` estándar. diff --git a/docs/es/docs/how-to/graphql.md b/docs/es/docs/how-to/graphql.md index 2ebfb3dd0..e50c1ae0a 100644 --- a/docs/es/docs/how-to/graphql.md +++ b/docs/es/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Dependiendo de tu caso de uso, podrías preferir usar un paquete diferente, pero Aquí tienes una pequeña vista previa de cómo podrías integrar Strawberry con FastAPI: -{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} Puedes aprender más sobre Strawberry en la documentación de Strawberry. diff --git a/docs/pt/docs/advanced/dataclasses.md b/docs/pt/docs/advanced/dataclasses.md index 646737696..6dc9feb29 100644 --- a/docs/pt/docs/advanced/dataclasses.md +++ b/docs/pt/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI é construído em cima do **Pydantic**, e eu tenho mostrado como usar mo Mas o FastAPI também suporta o uso de `dataclasses` da mesma forma: -{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} Isso ainda é suportado graças ao **Pydantic**, pois ele tem suporte interno para `dataclasses`. @@ -32,7 +32,7 @@ Mas se você tem um monte de dataclasses por aí, este é um truque legal para u Você também pode usar `dataclasses` no parâmetro `response_model`: -{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} A dataclass será automaticamente convertida para uma dataclass Pydantic. @@ -48,7 +48,7 @@ Em alguns casos, você ainda pode ter que usar a versão do Pydantic das `datacl Nesse caso, você pode simplesmente trocar as `dataclasses` padrão por `pydantic.dataclasses`, que é um substituto direto: -{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Ainda importamos `field` das `dataclasses` padrão. diff --git a/docs/pt/docs/how-to/graphql.md b/docs/pt/docs/how-to/graphql.md index 7af4c6b75..98266cc28 100644 --- a/docs/pt/docs/how-to/graphql.md +++ b/docs/pt/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Dependendo do seu caso de uso, você pode preferir usar uma biblioteca diferente Aqui está uma pequena prévia de como você poderia integrar Strawberry com FastAPI: -{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} Você pode aprender mais sobre Strawberry na documentação do Strawberry. diff --git a/docs/ru/docs/advanced/dataclasses.md b/docs/ru/docs/advanced/dataclasses.md index c37ce3023..b3ced37c1 100644 --- a/docs/ru/docs/advanced/dataclasses.md +++ b/docs/ru/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в Но FastAPI также поддерживает использование `dataclasses` тем же способом: -{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} Это по-прежнему поддерживается благодаря **Pydantic**, так как в нём есть встроенная поддержка `dataclasses`. @@ -32,7 +32,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в Вы также можете использовать `dataclasses` в параметре `response_model`: -{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} Этот dataclass будет автоматически преобразован в Pydantic dataclass. @@ -48,7 +48,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в В таком случае вы можете просто заменить стандартные `dataclasses` на `pydantic.dataclasses`, которая является полностью совместимой заменой (drop-in replacement): -{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Мы по-прежнему импортируем `field` из стандартных `dataclasses`. diff --git a/docs/ru/docs/how-to/graphql.md b/docs/ru/docs/how-to/graphql.md index 97278069a..50c321e7d 100644 --- a/docs/ru/docs/how-to/graphql.md +++ b/docs/ru/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Вот небольшой пример того, как можно интегрировать Strawberry с FastAPI: -{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} Подробнее о Strawberry можно узнать в документации Strawberry. diff --git a/docs/zh/docs/advanced/dataclasses.md b/docs/zh/docs/advanced/dataclasses.md index c74ce65c3..4e8e77d2a 100644 --- a/docs/zh/docs/advanced/dataclasses.md +++ b/docs/zh/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI 基于 **Pydantic** 构建,前文已经介绍过如何使用 Pydantic 但 FastAPI 还可以使用数据类(`dataclasses`): -{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} +{* ../../docs_src/dataclasses_/tutorial001.py hl[1,7:12,19:20] *} 这还是借助于 **Pydantic** 及其内置的 `dataclasses`。 @@ -32,7 +32,7 @@ FastAPI 基于 **Pydantic** 构建,前文已经介绍过如何使用 Pydantic 在 `response_model` 参数中使用 `dataclasses`: -{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *} +{* ../../docs_src/dataclasses_/tutorial002.py hl[1,7:13,19] *} 本例把数据类自动转换为 Pydantic 数据类。 @@ -49,7 +49,7 @@ API 文档中也会显示相关概图: 本例把标准的 `dataclasses` 直接替换为 `pydantic.dataclasses`: ```{ .python .annotate hl_lines="1 5 8-11 14-17 23-25 28" } -{!../../docs_src/dataclasses/tutorial003.py!} +{!../../docs_src/dataclasses_/tutorial003.py!} ``` 1. 本例依然要从标准的 `dataclasses` 中导入 `field`; diff --git a/docs_src/additional_responses/__init__.py b/docs_src/additional_responses/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/additional_status_codes/__init__.py b/docs_src/additional_status_codes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/advanced_middleware/__init__.py b/docs_src/advanced_middleware/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/authentication_error_status_code/__init__.py b/docs_src/authentication_error_status_code/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/background_tasks/__init__.py b/docs_src/background_tasks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/behind_a_proxy/__init__.py b/docs_src/behind_a_proxy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/body/__init__.py b/docs_src/body/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/body_fields/__init__.py b/docs_src/body_fields/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/body_multiple_params/__init__.py b/docs_src/body_multiple_params/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/body_nested_models/__init__.py b/docs_src/body_nested_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/body_updates/__init__.py b/docs_src/body_updates/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/conditional_openapi/__init__.py b/docs_src/conditional_openapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/configure_swagger_ui/__init__.py b/docs_src/configure_swagger_ui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/cookie_param_models/__init__.py b/docs_src/cookie_param_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/cookie_params/__init__.py b/docs_src/cookie_params/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/cors/__init__.py b/docs_src/cors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/custom_docs_ui/__init__.py b/docs_src/custom_docs_ui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/custom_request_and_route/__init__.py b/docs_src/custom_request_and_route/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/custom_response/__init__.py b/docs_src/custom_response/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/dataclasses_/__init__.py b/docs_src/dataclasses_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/dataclasses/tutorial001_py310.py b/docs_src/dataclasses_/tutorial001_py310.py similarity index 100% rename from docs_src/dataclasses/tutorial001_py310.py rename to docs_src/dataclasses_/tutorial001_py310.py diff --git a/docs_src/dataclasses/tutorial001_py39.py b/docs_src/dataclasses_/tutorial001_py39.py similarity index 100% rename from docs_src/dataclasses/tutorial001_py39.py rename to docs_src/dataclasses_/tutorial001_py39.py diff --git a/docs_src/dataclasses/tutorial002_py310.py b/docs_src/dataclasses_/tutorial002_py310.py similarity index 100% rename from docs_src/dataclasses/tutorial002_py310.py rename to docs_src/dataclasses_/tutorial002_py310.py diff --git a/docs_src/dataclasses/tutorial002_py39.py b/docs_src/dataclasses_/tutorial002_py39.py similarity index 100% rename from docs_src/dataclasses/tutorial002_py39.py rename to docs_src/dataclasses_/tutorial002_py39.py diff --git a/docs_src/dataclasses/tutorial003_py310.py b/docs_src/dataclasses_/tutorial003_py310.py similarity index 100% rename from docs_src/dataclasses/tutorial003_py310.py rename to docs_src/dataclasses_/tutorial003_py310.py diff --git a/docs_src/dataclasses/tutorial003_py39.py b/docs_src/dataclasses_/tutorial003_py39.py similarity index 100% rename from docs_src/dataclasses/tutorial003_py39.py rename to docs_src/dataclasses_/tutorial003_py39.py diff --git a/docs_src/debugging/__init__.py b/docs_src/debugging/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/dependencies/__init__.py b/docs_src/dependencies/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/dependency_testing/__init__.py b/docs_src/dependency_testing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/encoder/__init__.py b/docs_src/encoder/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/events/__init__.py b/docs_src/events/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/extending_openapi/__init__.py b/docs_src/extending_openapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/extra_data_types/__init__.py b/docs_src/extra_data_types/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/extra_models/__init__.py b/docs_src/extra_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/first_steps/__init__.py b/docs_src/first_steps/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/generate_clients/__init__.py b/docs_src/generate_clients/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/graphql_/__init__.py b/docs_src/graphql_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/graphql/tutorial001_py39.py b/docs_src/graphql_/tutorial001_py39.py similarity index 100% rename from docs_src/graphql/tutorial001_py39.py rename to docs_src/graphql_/tutorial001_py39.py diff --git a/docs_src/handling_errors/__init__.py b/docs_src/handling_errors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/header_param_models/__init__.py b/docs_src/header_param_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/header_params/__init__.py b/docs_src/header_params/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/metadata/__init__.py b/docs_src/metadata/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/middleware/__init__.py b/docs_src/middleware/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/openapi_callbacks/__init__.py b/docs_src/openapi_callbacks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/openapi_webhooks/__init__.py b/docs_src/openapi_webhooks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/path_operation_advanced_configuration/__init__.py b/docs_src/path_operation_advanced_configuration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/path_operation_configuration/__init__.py b/docs_src/path_operation_configuration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/path_params/__init__.py b/docs_src/path_params/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/path_params_numeric_validations/__init__.py b/docs_src/path_params_numeric_validations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/pydantic_v1_in_v2/__init__.py b/docs_src/pydantic_v1_in_v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/python_types/__init__.py b/docs_src/python_types/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/query_param_models/__init__.py b/docs_src/query_param_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/query_params/__init__.py b/docs_src/query_params/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/query_params_str_validations/__init__.py b/docs_src/query_params_str_validations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/request_files/__init__.py b/docs_src/request_files/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/request_form_models/__init__.py b/docs_src/request_form_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/request_forms/__init__.py b/docs_src/request_forms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/request_forms_and_files/__init__.py b/docs_src/request_forms_and_files/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/response_change_status_code/__init__.py b/docs_src/response_change_status_code/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/response_cookies/__init__.py b/docs_src/response_cookies/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/response_directly/__init__.py b/docs_src/response_directly/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/response_headers/__init__.py b/docs_src/response_headers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/response_model/__init__.py b/docs_src/response_model/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/response_status_code/__init__.py b/docs_src/response_status_code/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/schema_extra_example/__init__.py b/docs_src/schema_extra_example/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/security/__init__.py b/docs_src/security/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/separate_openapi_schemas/__init__.py b/docs_src/separate_openapi_schemas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/__init__.py b/docs_src/settings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/static_files/__init__.py b/docs_src/static_files/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/sub_applications/__init__.py b/docs_src/sub_applications/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/templates/__init__.py b/docs_src/templates/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/templates/static/__init__.py b/docs_src/templates/static/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/templates/templates/__init__.py b/docs_src/templates/templates/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/using_request_directly/__init__.py b/docs_src/using_request_directly/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/wsgi/__init__.py b/docs_src/wsgi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyproject.toml b/pyproject.toml index ae97cb71b..8f824af5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -196,6 +196,17 @@ dynamic_context = "test_function" omit = [ "docs_src/response_model/tutorial003_04_py39.py", "docs_src/response_model/tutorial003_04_py310.py", + "docs_src/dependencies/tutorial008_an_py39.py", # difficult to mock + "docs_src/dependencies/tutorial013_an_py310.py", # temporary code example? + "docs_src/dependencies/tutorial014_an_py310.py", # temporary code example? + # Pydantic V1 + "docs_src/schema_extra_example/tutorial001_pv1_py310.py", + "docs_src/query_param_models/tutorial002_pv1_py310.py", + "docs_src/query_param_models/tutorial002_pv1_an_py310.py", + "docs_src/header_param_models/tutorial002_pv1_py310.py", + "docs_src/header_param_models/tutorial002_pv1_an_py310.py", + "docs_src/cookie_param_models/tutorial002_pv1_py310.py", + "docs_src/cookie_param_models/tutorial002_pv1_an_py310.py", ] [tool.coverage.report] diff --git a/requirements-tests.txt b/requirements-tests.txt index ee188b496..1604a2858 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -6,6 +6,7 @@ mypy ==1.14.1 dirty-equals ==0.9.0 sqlmodel==0.0.27 flask >=1.1.2,<4.0.0 +strawberry-graphql >=0.200.0,< 1.0.0 anyio[trio] >=3.2.1,<5.0.0 PyJWT==2.9.0 pyyaml >=5.3.1,<7.0.0 diff --git a/tests/test_tutorial/test_body/test_tutorial002.py b/tests/test_tutorial/test_body/test_tutorial002.py new file mode 100644 index 000000000..b6d51d523 --- /dev/null +++ b/tests/test_tutorial/test_body/test_tutorial002.py @@ -0,0 +1,161 @@ +import importlib +from typing import Union + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize("price", ["50.5", 50.5]) +def test_post_with_tax(client: TestClient, price: Union[str, float]): + response = client.post( + "/items/", + json={"name": "Foo", "price": price, "description": "Some Foo", "tax": 0.3}, + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.3, + "price_with_tax": 50.8, + } + + +@pytest.mark.parametrize("price", ["50.5", 50.5]) +def test_post_without_tax(client: TestClient, price: Union[str, float]): + response = client.post( + "/items/", json={"name": "Foo", "price": price, "description": "Some Foo"} + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": None, + } + + +def test_post_with_no_data(client: TestClient): + response = client.post("/items/", json={}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +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/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + }, + "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/test_tutorial003.py b/tests/test_tutorial/test_body/test_tutorial003.py new file mode 100644 index 000000000..227a125e7 --- /dev/null +++ b/tests/test_tutorial/test_body/test_tutorial003.py @@ -0,0 +1,171 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={"name": "Foo", "price": 50.1, "description": "Some Foo", "tax": 0.3}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "name": "Foo", + "price": 50.1, + "description": "Some Foo", + "tax": 0.3, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/123", + json={"name": "Foo", "price": 50.1}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "name": "Foo", + "price": 50.1, + "description": None, + "tax": None, + } + + +def test_put_with_no_data(client: TestClient): + response = client.put("/items/123", json={}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +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": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "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", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + }, + "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/test_tutorial004.py b/tests/test_tutorial/test_body/test_tutorial004.py new file mode 100644 index 000000000..10212843e --- /dev/null +++ b/tests/test_tutorial/test_body/test_tutorial004.py @@ -0,0 +1,182 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={"name": "Foo", "price": 50.1, "description": "Some Foo", "tax": 0.3}, + params={"q": "somequery"}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "name": "Foo", + "price": 50.1, + "description": "Some Foo", + "tax": 0.3, + "q": "somequery", + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/123", + json={"name": "Foo", "price": 50.1}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "name": "Foo", + "price": 50.1, + "description": None, + "tax": None, + } + + +def test_put_with_no_data(client: TestClient): + response = client.put("/items/123", json={}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +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": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "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", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + }, + "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_tutorial002.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py new file mode 100644 index 000000000..e98d5860f --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py @@ -0,0 +1,361 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_all(client: TestClient): + response = client.put( + "/items/5", + json={ + "item": { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.1, + }, + "user": {"username": "johndoe", "full_name": "John Doe"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.1, + }, + "user": {"username": "johndoe", "full_name": "John Doe"}, + } + + +def test_post_required(client: TestClient): + response = client.put( + "/items/5", + json={ + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "johndoe"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "johndoe", "full_name": None}, + } + + +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + { + "input": None, + "loc": [ + "body", + "user", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_no_item(client: TestClient): + response = client.put("/items/5", json={"user": {"username": "johndoe"}}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_no_user(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 50.5}}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "user", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_missing_required_field_in_item(client: TestClient): + response = client.put( + "/items/5", json={"item": {"name": "Foo"}, "user": {"username": "johndoe"}} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": {"name": "Foo"}, + "loc": [ + "body", + "item", + "price", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_missing_required_field_in_user(client: TestClient): + response = client.put( + "/items/5", + json={"item": {"name": "Foo", "price": 50.5}, "user": {"ful_name": "John Doe"}}, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": {"ful_name": "John Doe"}, + "loc": [ + "body", + "user", + "username", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_id_foo(client: TestClient): + response = client.put( + "/items/foo", + json={ + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "johndoe"}, + }, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/{item_id}": { + "put": { + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Update Item", + }, + }, + }, + "components": { + "schemas": { + "Body_update_item_items__item_id__put": { + "properties": { + "item": { + "$ref": "#/components/schemas/Item", + }, + "user": { + "$ref": "#/components/schemas/User", + }, + }, + "required": [ + "item", + "user", + ], + "title": "Body_update_item_items__item_id__put", + "type": "object", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "User": { + "properties": { + "username": { + "title": "Username", + "type": "string", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + "required": [ + "username", + ], + "title": "User", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py new file mode 100644 index 000000000..979c054cd --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py @@ -0,0 +1,290 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + pytest.param("tutorial004_an_py39"), + pytest.param("tutorial004_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(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + params={"q": "somequery"}, + ) + 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}, + "q": "somequery", + } + + +def test_put_only_required(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_put_missing_body(client: TestClient): + response = client.put("/items/5") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + { + "input": None, + "loc": [ + "body", + "user", + ], + "msg": "Field required", + "type": "missing", + }, + { + "input": None, + "loc": [ + "body", + "importance", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_put_empty_body(client: TestClient): + response = client.put("/items/5", json={}) + assert response.status_code == 422 + assert response.json() == { + "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, + }, + ] + } + + +def test_put_invalid_importance(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 0, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "loc": ["body", "importance"], + "msg": "Input should be greater than 0", + "type": "greater_than", + "input": 0, + "ctx": {"gt": 0}, + }, + ], + } + + +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", + }, + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "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": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "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", + "exclusiveMinimum": 0.0, + }, + }, + }, + "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_tutorial005.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py new file mode 100644 index 000000000..d47aa1b4f --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py @@ -0,0 +1,272 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_py310", marks=needs_py310), + pytest.param("tutorial005_an_py39"), + pytest.param("tutorial005_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(mod.app) + return client + + +def test_post_all(client: TestClient): + response = client.put( + "/items/5", + json={ + "item": { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.1, + }, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.1, + }, + } + + +def test_post_required(client: TestClient): + response = client.put( + "/items/5", + json={ + "item": {"name": "Foo", "price": 50.5}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + } + + +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_like_not_embeded(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 50.5, + }, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_missing_required_field_in_item(client: TestClient): + response = client.put( + "/items/5", json={"item": {"name": "Foo"}, "user": {"username": "johndoe"}} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": {"name": "Foo"}, + "loc": [ + "body", + "item", + "price", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/{item_id}": { + "put": { + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Update Item", + }, + }, + }, + "components": { + "schemas": { + "Body_update_item_items__item_id__put": { + "properties": { + "item": { + "$ref": "#/components/schemas/Item", + }, + }, + "required": ["item"], + "title": "Body_update_item_items__item_id__put", + "type": "object", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py b/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py new file mode 100644 index 000000000..d452929c3 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py @@ -0,0 +1,251 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +UNTYPED_LIST_SCHEMA = {"type": "array", "items": {}} + +LIST_OF_STR_SCHEMA = {"type": "array", "items": {"type": "string"}} + +SET_OF_STR_SCHEMA = {"type": "array", "items": {"type": "string"}, "uniqueItems": True} + + +@pytest.fixture( + name="mod_name", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_mod_name(request: pytest.FixtureRequest): + return request.param + + +@pytest.fixture(name="client") +def get_client(mod_name: str): + mod = importlib.import_module(f"docs_src.body_nested_models.{mod_name}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient, mod_name: str): + if mod_name.startswith("tutorial003"): + tags_expected = IsList("foo", "bar", check_order=False) + else: + tags_expected = ["foo", "bar", "foo"] + + response = client.put( + "/items/123", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo", "bar", "foo"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 123, + "item": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": tags_expected, + }, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={"name": "Foo", "price": 35.4}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + }, + } + + +def test_put_empty_body(client: TestClient): + response = client.put( + "/items/5", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required(client: TestClient): + response = client.put( + "/items/5", + json={"description": "A very nice Item"}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_openapi_schema(client: TestClient, mod_name: str): + tags_schema = {"default": [], "title": "Tags"} + if mod_name.startswith("tutorial001"): + tags_schema.update(UNTYPED_LIST_SCHEMA) + elif mod_name.startswith("tutorial002"): + tags_schema.update(LIST_OF_STR_SCHEMA) + elif mod_name.startswith("tutorial003"): + tags_schema.update(SET_OF_STR_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/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "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", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": tags_schema, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "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_nested_models/test_tutorial004.py b/tests/test_tutorial/test_body_nested_models/test_tutorial004.py new file mode 100644 index 000000000..ff9596943 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial004.py @@ -0,0 +1,275 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo", "bar", "foo"], + "image": {"url": "http://example.com/image.png", "name": "example image"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 123, + "item": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": IsList("foo", "bar", check_order=False), + "image": {"url": "http://example.com/image.png", "name": "example image"}, + }, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={"name": "Foo", "price": 35.4}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + "image": None, + }, + } + + +def test_put_empty_body(client: TestClient): + response = client.put( + "/items/5", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_item(client: TestClient): + response = client.put( + "/items/5", + json={"description": "A very nice Item"}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_image(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 35.4, + "image": {"url": "http://example.com/image.png"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "image", "name"], + "input": {"url": "http://example.com/image.png"}, + "msg": "Field required", + "type": "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": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "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", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": {"type": "string"}, + "uniqueItems": True, + }, + "image": { + "anyOf": [ + {"$ref": "#/components/schemas/Image"}, + {"type": "null"}, + ], + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "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_nested_models/test_tutorial005.py b/tests/test_tutorial/test_body_nested_models/test_tutorial005.py new file mode 100644 index 000000000..9a07a904e --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial005.py @@ -0,0 +1,301 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo", "bar", "foo"], + "image": {"url": "http://example.com/image.png", "name": "example image"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 123, + "item": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": IsList("foo", "bar", check_order=False), + "image": {"url": "http://example.com/image.png", "name": "example image"}, + }, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={"name": "Foo", "price": 35.4}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + "image": None, + }, + } + + +def test_put_empty_body(client: TestClient): + response = client.put( + "/items/5", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_item(client: TestClient): + response = client.put( + "/items/5", + json={"description": "A very nice Item"}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_image(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 35.4, + "image": {"url": "http://example.com/image.png"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "image", "name"], + "input": {"url": "http://example.com/image.png"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_wrong_url(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 35.4, + "image": {"url": "not a valid url", "name": "example image"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "image", "url"], + "input": "not a valid url", + "msg": "Input should be a valid URL, relative URL without a base", + "type": "url_parsing", + "ctx": {"error": "relative URL without a base"}, + }, + ] + } + + +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": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "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", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": {"type": "string"}, + "uniqueItems": True, + }, + "image": { + "anyOf": [ + {"$ref": "#/components/schemas/Image"}, + {"type": "null"}, + ], + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "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_nested_models/test_tutorial006.py b/tests/test_tutorial/test_body_nested_models/test_tutorial006.py new file mode 100644 index 000000000..088177cb9 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial006.py @@ -0,0 +1,269 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial006_py39"), + pytest.param("tutorial006_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo", "bar", "foo"], + "images": [ + {"url": "http://example.com/image.png", "name": "example image"} + ], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 123, + "item": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": IsList("foo", "bar", check_order=False), + "images": [ + {"url": "http://example.com/image.png", "name": "example image"} + ], + }, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={"name": "Foo", "price": 35.4}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + "images": None, + }, + } + + +def test_put_empty_body(client: TestClient): + response = client.put( + "/items/5", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_images_not_list(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 35.4, + "images": {"url": "http://example.com/image.png", "name": "example image"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "images"], + "input": { + "url": "http://example.com/image.png", + "name": "example image", + }, + "msg": "Input should be a valid list", + "type": "list_type", + }, + ] + } + + +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": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "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", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": {"type": "string"}, + "uniqueItems": True, + }, + "images": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Image", + }, + "type": "array", + }, + { + "type": "null", + }, + ], + "title": "Images", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "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_nested_models/test_tutorial007.py b/tests/test_tutorial/test_body_nested_models/test_tutorial007.py new file mode 100644 index 000000000..a30281950 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial007.py @@ -0,0 +1,344 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial007_py39"), + pytest.param("tutorial007_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_all(client: TestClient): + data = { + "name": "Special Offer", + "description": "This is a special offer", + "price": 38.6, + "items": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo"], + "images": [ + { + "url": "http://example.com/image.png", + "name": "example image", + } + ], + } + ], + } + + response = client.post( + "/offers/", + json=data, + ) + assert response.status_code == 200, response.text + assert response.json() == data + + +def test_put_only_required(client: TestClient): + response = client.post( + "/offers/", + json={ + "name": "Special Offer", + "price": 38.6, + "items": [ + { + "name": "Foo", + "price": 35.4, + "images": [ + { + "url": "http://example.com/image.png", + "name": "example image", + } + ], + } + ], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Special Offer", + "description": None, + "price": 38.6, + "items": [ + { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + "images": [ + { + "url": "http://example.com/image.png", + "name": "example image", + } + ], + } + ], + } + + +def test_put_empty_body(client: TestClient): + response = client.post( + "/offers/", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "items"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_items(client: TestClient): + response = client.post( + "/offers/", + json={ + "name": "Special Offer", + "price": 38.6, + "items": [{}], + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "items", 0, "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "items", 0, "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_images(client: TestClient): + response = client.post( + "/offers/", + json={ + "name": "Special Offer", + "price": 38.6, + "items": [ + {"name": "Foo", "price": 35.4, "images": [{}]}, + ], + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "items", 0, "images", 0, "url"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "items", 0, "images", 0, "name"], + "input": {}, + "msg": "Field required", + "type": "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": { + "/offers/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Offer", + "operationId": "create_offer_offers__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Offer", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": {"type": "string"}, + "uniqueItems": True, + }, + "images": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Image", + }, + "type": "array", + }, + { + "type": "null", + }, + ], + "title": "Images", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "Offer": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + }, + }, + "required": ["name", "price", "items"], + "title": "Offer", + "type": "object", + }, + "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_nested_models/test_tutorial008.py b/tests/test_tutorial/test_body_nested_models/test_tutorial008.py new file mode 100644 index 000000000..32eb8ee75 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial008.py @@ -0,0 +1,157 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial008_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_body(client: TestClient): + data = [ + {"url": "http://example.com/", "name": "Example"}, + {"url": "http://fastapi.tiangolo.com/", "name": "FastAPI"}, + ] + response = client.post("/images/multiple", json=data) + assert response.status_code == 200, response.text + assert response.json() == data + + +def test_post_invalid_list_item(client: TestClient): + data = [{"url": "not a valid url", "name": "Example"}] + response = client.post("/images/multiple", json=data) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", 0, "url"], + "input": "not a valid url", + "msg": "Input should be a valid URL, relative URL without a base", + "type": "url_parsing", + "ctx": {"error": "relative URL without a base"}, + }, + ] + } + + +def test_post_not_a_list(client: TestClient): + data = {"url": "http://example.com/", "name": "Example"} + response = client.post("/images/multiple", json=data) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body"], + "input": { + "name": "Example", + "url": "http://example.com/", + }, + "msg": "Input should be a valid list", + "type": "list_type", + } + ] + } + + +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": { + "/images/multiple/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Multiple Images", + "operationId": "create_multiple_images_images_multiple__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Images", + "type": "array", + "items": {"$ref": "#/components/schemas/Image"}, + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "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_updates/test_tutorial002.py b/tests/test_tutorial/test_body_updates/test_tutorial002.py new file mode 100644 index 000000000..466e6af8f --- /dev/null +++ b/tests/test_tutorial/test_body_updates/test_tutorial002.py @@ -0,0 +1,207 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_updates.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_get(client: TestClient): + response = client.get("/items/baz") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [], + } + + +def test_patch_all(client: TestClient): + response = client.patch( + "/items/foo", + json={ + "name": "Fooz", + "description": "Item description", + "price": 3, + "tax": 10.5, + "tags": ["tag1", "tag2"], + }, + ) + assert response.json() == { + "name": "Fooz", + "description": "Item description", + "price": 3, + "tax": 10.5, + "tags": ["tag1", "tag2"], + } + + +def test_patch_name(client: TestClient): + response = client.patch( + "/items/bar", + json={"name": "Barz"}, + ) + assert response.json() == { + "name": "Barz", + "description": "The bartenders", + "price": 62, + "tax": 20.2, + "tags": [], + } + + +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}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "patch": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "type": "object", + "title": "Item", + "properties": { + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Price", + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "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_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index c81e991eb..f1d2accef 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -1,17 +1,29 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial001_py39 import app -client = TestClient(app) +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial010_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.custom_response.{request.param}") + client = TestClient(mod.app) + return client -def test_get_custom_response(): +def test_get_custom_response(client: TestClient): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py b/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py new file mode 100644 index 000000000..22e2e0254 --- /dev/null +++ b/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py @@ -0,0 +1,68 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="mod_name", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial003_py39"), + pytest.param("tutorial004_py39"), + ], +) +def get_mod_name(request: pytest.FixtureRequest) -> str: + return request.param + + +@pytest.fixture(name="client") +def get_client(mod_name: str) -> TestClient: + mod = importlib.import_module(f"docs_src.custom_response.{mod_name}") + return TestClient(mod.app) + + +html_contents = """ + + + Some HTML in here + + +

Look ma! HTML!

+ + + """ + + +def test_get_custom_response(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.text == html_contents + + +def test_openapi_schema(client: TestClient, mod_name: str): + if mod_name.startswith("tutorial003"): + response_content = {"application/json": {"schema": {}}} + else: + response_content = {"text/html": {"schema": {"type": "string"}}} + + 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": response_content, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index d5f230bc4..bc407234a 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -15,7 +15,7 @@ from tests.utils import needs_py310 ], ) def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.dataclasses.{request.param}") + mod = importlib.import_module(f"docs_src.dataclasses_.{request.param}") client = TestClient(mod.app) client.headers.clear() diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 4cf893380..995d92675 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -15,7 +15,7 @@ from tests.utils import needs_py310 ], ) def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.dataclasses.{request.param}") + mod = importlib.import_module(f"docs_src.dataclasses_.{request.param}") client = TestClient(mod.app) client.headers.clear() diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index cddf4a9be..a6a9fc1c7 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -14,7 +14,7 @@ from ...utils import needs_py310 ], ) def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.dataclasses.{request.param}") + mod = importlib.import_module(f"docs_src.dataclasses_.{request.param}") client = TestClient(mod.app) client.headers.clear() diff --git a/tests/test_tutorial/test_debugging/__init__.py b/tests/test_tutorial/test_debugging/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_debugging/test_tutorial001.py b/tests/test_tutorial/test_debugging/test_tutorial001.py new file mode 100644 index 000000000..cf62c3b19 --- /dev/null +++ b/tests/test_tutorial/test_debugging/test_tutorial001.py @@ -0,0 +1,64 @@ +import importlib +import runpy +import sys +import unittest + +import pytest +from fastapi.testclient import TestClient + +MOD_NAME = "docs_src.debugging.tutorial001_py39" + + +@pytest.fixture(name="client") +def get_client(): + mod = importlib.import_module(MOD_NAME) + client = TestClient(mod.app) + return client + + +def test_uvicorn_run_is_not_called_on_import(): + if sys.modules.get(MOD_NAME): + del sys.modules[MOD_NAME] # pragma: no cover + with unittest.mock.patch("uvicorn.run") as uvicorn_run_mock: + importlib.import_module(MOD_NAME) + uvicorn_run_mock.assert_not_called() + + +def test_get_root(client: TestClient): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"hello world": "ba"} + + +def test_uvicorn_run_called_when_run_as_main(): # Just for coverage + if sys.modules.get(MOD_NAME): + del sys.modules[MOD_NAME] + with unittest.mock.patch("uvicorn.run") as uvicorn_run_mock: + runpy.run_module(MOD_NAME, run_name="__main__") + + uvicorn_run_mock.assert_called_once_with( + unittest.mock.ANY, host="0.0.0.0", port=8000 + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py similarity index 86% rename from tests/test_tutorial/test_dependencies/test_tutorial001.py rename to tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py index 8dac99cf3..50d7c4108 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -14,6 +13,8 @@ from ...utils import needs_py310 pytest.param("tutorial001_py310", marks=needs_py310), pytest.param("tutorial001_an_py39"), pytest.param("tutorial001_an_py310", marks=needs_py310), + pytest.param("tutorial001_02_an_py39"), + pytest.param("tutorial001_02_an_py310", marks=needs_py310), ], ) def get_client(request: pytest.FixtureRequest): @@ -69,16 +70,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, "name": "q", "in": "query", }, @@ -128,16 +123,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py similarity index 89% rename from tests/test_tutorial/test_dependencies/test_tutorial004.py rename to tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py index 8a1346d0d..f09d6f268 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -10,6 +9,14 @@ from ...utils import needs_py310 @pytest.fixture( name="client", params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=needs_py310), + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + pytest.param("tutorial003_an_py39"), + pytest.param("tutorial003_an_py310", marks=needs_py310), pytest.param("tutorial004_py39"), pytest.param("tutorial004_py310", marks=needs_py310), pytest.param("tutorial004_an_py39"), @@ -107,16 +114,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial005.py b/tests/test_tutorial/test_dependencies/test_tutorial005.py new file mode 100644 index 000000000..a914936ba --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial005.py @@ -0,0 +1,139 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_py310", marks=needs_py310), + pytest.param("tutorial005_an_py39"), + pytest.param("tutorial005_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + "path,cookie,expected_status,expected_response", + [ + ( + "/items", + "from_cookie", + 200, + {"q_or_cookie": "from_cookie"}, + ), + ( + "/items?q=foo", + "from_cookie", + 200, + {"q_or_cookie": "foo"}, + ), + ( + "/items", + None, + 200, + {"q_or_cookie": None}, + ), + ], +) +def test_get(path, cookie, expected_status, expected_response, client: TestClient): + if cookie is not None: + client.cookies.set("last_query", cookie) + else: + client.cookies.clear() + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +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 Query", + "operationId": "read_query_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Last Query", + }, + "name": "last_query", + "in": "cookie", + }, + ], + } + } + }, + "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_dependencies/test_tutorial007.py b/tests/test_tutorial/test_dependencies/test_tutorial007.py new file mode 100644 index 000000000..3e188abcf --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial007.py @@ -0,0 +1,24 @@ +import asyncio +from contextlib import asynccontextmanager +from unittest.mock import Mock, patch + +from docs_src.dependencies.tutorial007_py39 import get_db + + +def test_get_db(): # Just for coverage + async def test_async_gen(): + cm = asynccontextmanager(get_db) + async with cm() as db_session: + return db_session + + dbsession_moock = Mock() + + with patch( + "docs_src.dependencies.tutorial007_py39.DBSession", + return_value=dbsession_moock, + create=True, + ): + value = asyncio.run(test_async_gen()) + + assert value is dbsession_moock + dbsession_moock.close.assert_called_once() diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008.py b/tests/test_tutorial/test_dependencies/test_tutorial008.py new file mode 100644 index 000000000..9d7377ebe --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial008.py @@ -0,0 +1,58 @@ +import importlib +from types import ModuleType +from typing import Annotated, Any +from unittest.mock import Mock, patch + +import pytest +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="module", + params=[ + "tutorial008_py39", + # Fails with `NameError: name 'DepA' is not defined` + pytest.param("tutorial008_an_py39", marks=pytest.mark.xfail), + ], +) +def get_module(request: pytest.FixtureRequest): + mod_name = f"docs_src.dependencies.{request.param}" + mod = importlib.import_module(mod_name) + return mod + + +def test_get_db(module: ModuleType): + app = FastAPI() + + @app.get("/") + def read_root(c: Annotated[Any, Depends(module.dependency_c)]): + return {"c": str(c)} + + client = TestClient(app) + + a_mock = Mock() + b_mock = Mock() + c_mock = Mock() + + with ( + patch( + f"{module.__name__}.generate_dep_a", + return_value=a_mock, + create=True, + ), + patch( + f"{module.__name__}.generate_dep_b", + return_value=b_mock, + create=True, + ), + patch( + f"{module.__name__}.generate_dep_c", + return_value=c_mock, + create=True, + ), + ): + response = client.get("/") + + assert response.status_code == 200 + assert response.json() == {"c": str(c_mock)} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial010.py b/tests/test_tutorial/test_dependencies/test_tutorial010.py new file mode 100644 index 000000000..6d3815ada --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial010.py @@ -0,0 +1,29 @@ +from typing import Annotated, Any +from unittest.mock import Mock, patch + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial010_py39 import get_db + + +def test_get_db(): + app = FastAPI() + + @app.get("/") + def read_root(c: Annotated[Any, Depends(get_db)]): + return {"c": str(c)} + + client = TestClient(app) + + dbsession_mock = Mock() + + with patch( + "docs_src.dependencies.tutorial010_py39.DBSession", + return_value=dbsession_mock, + create=True, + ): + response = client.get("/") + + assert response.status_code == 200 + assert response.json() == {"c": str(dbsession_mock)} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial011.py b/tests/test_tutorial/test_dependencies/test_tutorial011.py new file mode 100644 index 000000000..4868254c0 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial011.py @@ -0,0 +1,120 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + "tutorial011_py39", + pytest.param("tutorial011_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ( + "/query-checker/", + 200, + {"fixed_content_in_query": False}, + ), + ( + "/query-checker/?q=qwerty", + 200, + {"fixed_content_in_query": False}, + ), + ( + "/query-checker/?q=foobar", + 200, + {"fixed_content_in_query": True}, + ), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +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": { + "/query-checker/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Query Check", + "operationId": "read_query_check_query_checker__get", + "parameters": [ + { + "required": False, + "schema": { + "type": "string", + "default": "", + "title": "Q", + }, + "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_encoder/__init__.py b/tests/test_tutorial/test_encoder/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_encoder/test_tutorial001.py b/tests/test_tutorial/test_encoder/test_tutorial001.py new file mode 100644 index 000000000..5c8ee054d --- /dev/null +++ b/tests/test_tutorial/test_encoder/test_tutorial001.py @@ -0,0 +1,208 @@ +import importlib +from types import ModuleType + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest): + module = importlib.import_module(f"docs_src.encoder.{request.param}") + return module + + +@pytest.fixture(name="client") +def get_client(mod: ModuleType): + client = TestClient(mod.app) + return client + + +def test_put(client: TestClient, mod: ModuleType): + fake_db = mod.fake_db + + response = client.put( + "/items/123", + json={ + "title": "Foo", + "timestamp": "2023-01-01T12:00:00", + "description": "An optional description", + }, + ) + assert response.status_code == 200 + assert "123" in fake_db + assert fake_db["123"] == { + "title": "Foo", + "timestamp": "2023-01-01T12:00:00", + "description": "An optional description", + } + + +def test_put_invalid_data(client: TestClient, mod: ModuleType): + fake_db = mod.fake_db + + response = client.put( + "/items/345", + json={ + "title": "Foo", + "timestamp": "not a date", + }, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "loc": ["body", "timestamp"], + "msg": "Input should be a valid datetime or date, invalid character in year", + "type": "datetime_from_date_parsing", + "input": "not a date", + "ctx": {"error": "invalid character in year"}, + } + ] + } + assert "345" not in fake_db + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{id}": { + "put": { + "operationId": "update_item_items__id__put", + "parameters": [ + { + "in": "path", + "name": "id", + "required": True, + "schema": { + "title": "Id", + "type": "string", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Update Item", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Description", + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string", + }, + "title": { + "title": "Title", + "type": "string", + }, + }, + "required": [ + "title", + "timestamp", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py new file mode 100644 index 000000000..3f2f508a1 --- /dev/null +++ b/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py @@ -0,0 +1,156 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.extra_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post(client: TestClient): + response = client.post( + "/user/", + json={ + "username": "johndoe", + "password": "secret", + "email": "johndoe@example.com", + "full_name": "John Doe", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + } + + +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": { + "/user/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserOut", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "UserIn": { + "title": "UserIn", + "required": IsList( + "username", "password", "email", check_order=False + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "UserOut": { + "title": "UserOut", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "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_first_steps/test_tutorial001.py b/tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py similarity index 70% rename from tests/test_tutorial/test_first_steps/test_tutorial001.py rename to tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py index c102bb999..aa65218cd 100644 --- a/tests/test_tutorial/test_first_steps/test_tutorial001.py +++ b/tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py @@ -1,9 +1,20 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from docs_src.first_steps.tutorial001_py39 import app -client = TestClient(app) +@pytest.fixture( + name="client", + params=[ + "tutorial001_py39", + "tutorial003_py39", + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.first_steps.{request.param}") + client = TestClient(mod.app) + return client @pytest.mark.parametrize( @@ -13,13 +24,13 @@ client = TestClient(app) ("/nonexistent", 404, {"detail": "Not Found"}), ], ) -def test_get_path(path, expected_status, expected_response): +def test_get_path(client: TestClient, path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial001.py b/tests/test_tutorial/test_generate_clients/test_tutorial001.py new file mode 100644 index 000000000..bbb66b451 --- /dev/null +++ b/tests/test_tutorial/test_generate_clients/test_tutorial001.py @@ -0,0 +1,142 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.generate_clients.{request.param}") + client = TestClient(mod.app) + return client + + +def test_post_items(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 5}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "item received"} + + +def test_get_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "Plumbus", "price": 3}, + {"name": "Portal Gun", "price": 9001}, + ] + + +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": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "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"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "ResponseMessage": { + "title": "ResponseMessage", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial002.py b/tests/test_tutorial/test_generate_clients/test_tutorial002.py new file mode 100644 index 000000000..ab8bc4c11 --- /dev/null +++ b/tests/test_tutorial/test_generate_clients/test_tutorial002.py @@ -0,0 +1,187 @@ +from fastapi.testclient import TestClient + +from docs_src.generate_clients.tutorial002_py39 import app + +client = TestClient(app) + + +def test_post_items(): + response = client.post("/items/", json={"name": "Foo", "price": 5}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Item received"} + + +def test_post_users(): + response = client.post( + "/users/", json={"username": "Foo", "email": "foo@example.com"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "User received"} + + +def test_get_items(): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "Plumbus", "price": 3}, + {"name": "Portal Gun", "price": 9001}, + ] + + +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": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + }, + "post": { + "tags": ["items"], + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/users/": { + "post": { + "tags": ["users"], + "summary": "Create User", + "operationId": "create_user_users__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "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"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "ResponseMessage": { + "title": "ResponseMessage", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + }, + "User": { + "title": "User", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial004.py b/tests/test_tutorial/test_generate_clients/test_tutorial004.py new file mode 100644 index 000000000..e66f6d2a1 --- /dev/null +++ b/tests/test_tutorial/test_generate_clients/test_tutorial004.py @@ -0,0 +1,230 @@ +import importlib +import json +import pathlib +from unittest.mock import patch + +from docs_src.generate_clients import tutorial003_py39 + + +def test_remove_tags(tmp_path: pathlib.Path): + tmp_file = tmp_path / "openapi.json" + openapi_json = tutorial003_py39.app.openapi() + tmp_file.write_text(json.dumps(openapi_json)) + + with patch("pathlib.Path", return_value=tmp_file): + importlib.import_module("docs_src.generate_clients.tutorial004_py39") + + modified_openapi = json.loads(tmp_file.read_text()) + assert modified_openapi == { + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ResponseMessage": { + "properties": { + "message": { + "title": "Message", + "type": "string", + }, + }, + "required": [ + "message", + ], + "title": "ResponseMessage", + "type": "object", + }, + "User": { + "properties": { + "email": { + "title": "Email", + "type": "string", + }, + "username": { + "title": "Username", + "type": "string", + }, + }, + "required": [ + "username", + "email", + ], + "title": "User", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/": { + "get": { + "operationId": "get_items", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item", + }, + "title": "Response Items-Get Items", + "type": "array", + }, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Get Items", + "tags": [ + "items", + ], + }, + "post": { + "operationId": "create_item", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage", + }, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Create Item", + "tags": [ + "items", + ], + }, + }, + "/users/": { + "post": { + "operationId": "create_user", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage", + }, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Create User", + "tags": [ + "users", + ], + }, + }, + }, + } diff --git a/tests/test_tutorial/test_graphql/__init__.py b/tests/test_tutorial/test_graphql/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_graphql/test_tutorial001.py b/tests/test_tutorial/test_graphql/test_tutorial001.py new file mode 100644 index 000000000..9ba7147b5 --- /dev/null +++ b/tests/test_tutorial/test_graphql/test_tutorial001.py @@ -0,0 +1,70 @@ +import warnings + +import pytest +from starlette.testclient import TestClient + +warnings.filterwarnings( + "ignore", + message=r"The 'lia' package has been renamed to 'cross_web'\..*", + category=DeprecationWarning, +) + +from docs_src.graphql_.tutorial001_py39 import app # noqa: E402 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + return TestClient(app) + + +def test_query(client: TestClient): + response = client.post("/graphql", json={"query": "{ user { name, age } }"}) + assert response.status_code == 200 + assert response.json() == {"data": {"user": {"name": "Patrick", "age": 100}}} + + +def test_openapi(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/graphql": { + "get": { + "operationId": "handle_http_get_graphql_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "The GraphiQL integrated development environment.", + }, + "404": { + "description": "Not found if GraphiQL or query via GET are not enabled.", + }, + }, + "summary": "Handle Http Get", + }, + "post": { + "operationId": "handle_http_post_graphql_post", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Handle Http Post", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial004.py b/tests/test_tutorial/test_metadata/test_tutorial002.py similarity index 61% rename from tests/test_tutorial/test_custom_response/test_tutorial004.py rename to tests/test_tutorial/test_metadata/test_tutorial002.py index 0e7d69791..e2814c88f 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial004.py +++ b/tests/test_tutorial/test_metadata/test_tutorial002.py @@ -1,45 +1,41 @@ from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial004_py39 import app +from docs_src.metadata.tutorial002_py39 import app client = TestClient(app) -html_contents = """ - - - Some HTML in here - - -

Look ma! HTML!

- - - """ - - -def test_get_custom_response(): +def test_items(): response = client.get("/items/") assert response.status_code == 200, response.text - assert response.text == html_contents + assert response.json() == [{"name": "Foo"}] + + +def test_get_openapi_json_default_url(): + response = client.get("/openapi.json") + assert response.status_code == 404, response.text def test_openapi_schema(): - response = client.get("/openapi.json") + response = client.get("/api/v1/openapi.json") assert response.status_code == 200, response.text assert response.json() == { "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.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": {"text/html": {"schema": {"type": "string"}}}, + "content": {"application/json": {"schema": {}}}, } }, - "summary": "Read Items", - "operationId": "read_items_items__get", } } }, diff --git a/tests/test_tutorial/test_metadata/test_tutorial003.py b/tests/test_tutorial/test_metadata/test_tutorial003.py new file mode 100644 index 000000000..085c271cd --- /dev/null +++ b/tests/test_tutorial/test_metadata/test_tutorial003.py @@ -0,0 +1,53 @@ +from fastapi.testclient import TestClient + +from docs_src.metadata.tutorial003_py39 import app + +client = TestClient(app) + + +def test_items(): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [{"name": "Foo"}] + + +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": {}}}, + } + }, + } + } + }, + } + + +def test_swagger_ui_default_url(): + response = client.get("/docs") + assert response.status_code == 404, response.text + + +def test_swagger_ui_custom_url(): + response = client.get("/documentation") + assert response.status_code == 200, response.text + assert "FastAPI - Swagger UI" in response.text + + +def test_redoc_ui_default_url(): + response = client.get("/redoc") + assert response.status_code == 404, response.text diff --git a/tests/test_tutorial/test_middleware/__init__.py b/tests/test_tutorial/test_middleware/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_middleware/test_tutorial001.py b/tests/test_tutorial/test_middleware/test_tutorial001.py new file mode 100644 index 000000000..cbcfd4146 --- /dev/null +++ b/tests/test_tutorial/test_middleware/test_tutorial001.py @@ -0,0 +1,24 @@ +from fastapi.testclient import TestClient + +from docs_src.middleware.tutorial001_py39 import app + +client = TestClient(app) + + +def test_response_headers(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert "X-Process-Time" in response.headers + + +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": {}, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py new file mode 100644 index 000000000..085d1f5e1 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py @@ -0,0 +1,186 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_operation_configuration.{request.param}" + ) + return TestClient(mod.app) + + +def test_post_items(client: TestClient): + response = client.post( + "/items/", + json={ + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": ["bar", "baz"], + }, + ) + assert response.status_code == 201, response.text + assert response.json() == { + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": IsList("bar", "baz", check_order=False), + } + + +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/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Description", + }, + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", + }, + "tags": { + "default": [], + "items": { + "type": "string", + }, + "title": "Tags", + "type": "array", + "uniqueItems": True, + }, + "tax": { + "anyOf": [ + { + "type": "number", + }, + { + "type": "null", + }, + ], + "title": "Tax", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py new file mode 100644 index 000000000..c7414d756 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py @@ -0,0 +1,223 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_operation_configuration.{request.param}" + ) + return TestClient(mod.app) + + +def test_post_items(client: TestClient): + response = client.post( + "/items/", + json={ + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": ["bar", "baz"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": IsList("bar", "baz", check_order=False), + } + + +def test_get_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [{"name": "Foo", "price": 42}] + + +def test_get_users(client: TestClient): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == [{"username": "johndoe"}] + + +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": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + }, + "post": { + "tags": ["items"], + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Description", + }, + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", + }, + "tags": { + "default": [], + "items": { + "type": "string", + }, + "title": "Tags", + "type": "array", + "uniqueItems": True, + }, + "tax": { + "anyOf": [ + { + "type": "number", + }, + { + "type": "null", + }, + ], + "title": "Tax", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py new file mode 100644 index 000000000..791db2462 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py @@ -0,0 +1,208 @@ +import importlib +from textwrap import dedent + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +DESCRIPTIONS = { + "tutorial003": "Create an item with all the information, name, description, price, tax and a set of unique tags", + "tutorial004": dedent(""" + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + """).strip(), +} + + +@pytest.fixture( + name="mod_name", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_mod_name(request: pytest.FixtureRequest) -> str: + return request.param + + +@pytest.fixture(name="client") +def get_client(mod_name: str) -> TestClient: + mod = importlib.import_module(f"docs_src.path_operation_configuration.{mod_name}") + return TestClient(mod.app) + + +def test_post_items(client: TestClient): + response = client.post( + "/items/", + json={ + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": ["bar", "baz"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": IsList("bar", "baz", check_order=False), + } + + +def test_openapi_schema(client: TestClient, mod_name: str): + mod_name = mod_name[:11] + + 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/": { + "post": { + "summary": "Create an item", + "description": DESCRIPTIONS[mod_name], + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Description", + }, + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", + }, + "tags": { + "default": [], + "items": { + "type": "string", + }, + "title": "Tags", + "type": "array", + "uniqueItems": True, + }, + "tax": { + "anyOf": [ + { + "type": "number", + }, + { + "type": "null", + }, + ], + "title": "Tax", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial001.py b/tests/test_tutorial/test_path_params/test_tutorial001.py new file mode 100644 index 000000000..a898e386f --- /dev/null +++ b/tests/test_tutorial/test_path_params/test_tutorial001.py @@ -0,0 +1,116 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.path_params.tutorial001_py39 import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + ("item_id", "expected_response"), + [ + (1, {"item_id": "1"}), + ("alice", {"item_id": "alice"}), + ], +) +def test_get_items(item_id, expected_response): + response = client.get(f"/items/{item_id}") + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +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/{item_id}": { + "get": { + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Read Item", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial002.py b/tests/test_tutorial/test_path_params/test_tutorial002.py new file mode 100644 index 000000000..0bfc9f807 --- /dev/null +++ b/tests/test_tutorial/test_path_params/test_tutorial002.py @@ -0,0 +1,124 @@ +from fastapi.testclient import TestClient + +from docs_src.path_params.tutorial002_py39 import app + +client = TestClient(app) + + +def test_get_items(): + response = client.get("/items/1") + assert response.status_code == 200, response.text + assert response.json() == {"item_id": 1} + + +def test_get_items_invalid_id(): + response = client.get("/items/item1") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "input": "item1", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +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/{item_id}": { + "get": { + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Read Item", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial003.py b/tests/test_tutorial/test_path_params/test_tutorial003.py new file mode 100644 index 000000000..cd2c39ab0 --- /dev/null +++ b/tests/test_tutorial/test_path_params/test_tutorial003.py @@ -0,0 +1,133 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.path_params.tutorial003_py39 import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + ("user_id", "expected_response"), + [ + ("me", {"user_id": "the current user"}), + ("alice", {"user_id": "alice"}), + ], +) +def test_get_users(user_id: str, expected_response: dict): + response = client.get(f"/users/{user_id}") + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +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": { + "/users/me": { + "get": { + "operationId": "read_user_me_users_me_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Read User Me", + }, + }, + "/users/{user_id}": { + "get": { + "operationId": "read_user_users__user_id__get", + "parameters": [ + { + "in": "path", + "name": "user_id", + "required": True, + "schema": { + "title": "User Id", + "type": "string", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Read User", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial003b.py b/tests/test_tutorial/test_path_params/test_tutorial003b.py new file mode 100644 index 000000000..8e4a26a1c --- /dev/null +++ b/tests/test_tutorial/test_path_params/test_tutorial003b.py @@ -0,0 +1,44 @@ +import asyncio + +from fastapi.testclient import TestClient + +from docs_src.path_params.tutorial003b_py39 import app, read_users2 + +client = TestClient(app) + + +def test_get_users(): + response = client.get("/users") + assert response.status_code == 200, response.text + assert response.json() == ["Rick", "Morty"] + + +def test_read_users2(): # Just for coverage + assert asyncio.run(read_users2()) == ["Bean", "Elfo"] + + +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": { + "/users": { + "get": { + "operationId": "read_users2_users_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Read Users2", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/__init__.py b/tests/test_tutorial/test_path_params_numeric_validations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py new file mode 100644 index 000000000..f1e304103 --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py @@ -0,0 +1,164 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_an_py39"), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ("/items/42", {"item_id": 42}), + ("/items/123?item-query=somequery", {"item_id": 123, "q": "somequery"}), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_invalid_item_id(client: TestClient): + response = client.get("/items/invalid_id") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "invalid_id", + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +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}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Item-Query", + }, + "name": "item-query", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py new file mode 100644 index 000000000..467c915dc --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py @@ -0,0 +1,170 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ("/items/42?q=", {"item_id": 42}), + ("/items/123?q=somequery", {"item_id": 123, "q": "somequery"}), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_invalid_item_id(client: TestClient): + response = client.get("/items/invalid_id?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "invalid_id", + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +def test_read_items_missing_q(client: TestClient): + response = client.get("/items/42") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "q"], + "input": None, + "msg": "Field required", + "type": "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}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py new file mode 100644 index 000000000..d3593c984 --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py @@ -0,0 +1,185 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ("/items/42?q=", {"item_id": 42}), + ("/items/1?q=somequery", {"item_id": 1, "q": "somequery"}), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_non_int_item_id(client: TestClient): + response = client.get("/items/invalid_id?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "invalid_id", + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +def test_read_items_item_id_less_than_one(client: TestClient): + response = client.get("/items/0?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "0", + "msg": "Input should be greater than or equal to 1", + "type": "greater_than_equal", + "ctx": {"ge": 1}, + } + ] + } + + +def test_read_items_missing_q(client: TestClient): + response = client.get("/items/42") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "q"], + "input": None, + "msg": "Field required", + "type": "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}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + "minimum": 1, + }, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py new file mode 100644 index 000000000..296192593 --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py @@ -0,0 +1,202 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ("/items/1?q=", {"item_id": 1}), + ("/items/1000?q=somequery", {"item_id": 1000, "q": "somequery"}), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_non_int_item_id(client: TestClient): + response = client.get("/items/invalid_id?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "invalid_id", + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +def test_read_items_item_id_less_than_one(client: TestClient): + response = client.get("/items/0?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "0", + "msg": "Input should be greater than 0", + "type": "greater_than", + "ctx": {"gt": 0}, + } + ] + } + + +def test_read_items_item_id_greater_than_one_thousand(client: TestClient): + response = client.get("/items/1001?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "1001", + "msg": "Input should be less than or equal to 1000", + "type": "less_than_equal", + "ctx": {"le": 1000}, + } + ] + } + + +def test_read_items_missing_q(client: TestClient): + response = client.get("/items/42") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "q"], + "input": None, + "msg": "Field required", + "type": "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}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 1000, + }, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py new file mode 100644 index 000000000..9dc7d7aac --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py @@ -0,0 +1,221 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial006_py39"), + pytest.param("tutorial006_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ( + "/items/0?q=&size=0.1", + {"item_id": 0, "size": 0.1}, + ), + ( + "/items/1000?q=somequery&size=10.4", + {"item_id": 1000, "q": "somequery", "size": 10.4}, + ), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_item_id_less_than_zero(client: TestClient): + response = client.get("/items/-1?q=somequery&size=5") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "-1", + "msg": "Input should be greater than or equal to 0", + "type": "greater_than_equal", + "ctx": {"ge": 0}, + } + ] + } + + +def test_read_items_item_id_greater_than_one_thousand(client: TestClient): + response = client.get("/items/1001?q=somequery&size=5") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "1001", + "msg": "Input should be less than or equal to 1000", + "type": "less_than_equal", + "ctx": {"le": 1000}, + } + ] + } + + +def test_read_items_size_too_small(client: TestClient): + response = client.get("/items/1?q=somequery&size=0.0") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "size"], + "input": "0.0", + "msg": "Input should be greater than 0", + "type": "greater_than", + "ctx": {"gt": 0.0}, + } + ] + } + + +def test_read_items_size_too_large(client: TestClient): + response = client.get("/items/1?q=somequery&size=10.5") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "size"], + "input": "10.5", + "msg": "Input should be less than 10.5", + "type": "less_than", + "ctx": {"lt": 10.5}, + } + ] + } + + +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}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + "minimum": 0, + "maximum": 1000, + }, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + { + "in": "query", + "name": "size", + "required": True, + "schema": { + "exclusiveMaximum": 10.5, + "exclusiveMinimum": 0, + "title": "Size", + "type": "number", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_python_types/__init__.py b/tests/test_tutorial/test_python_types/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py new file mode 100644 index 000000000..ccb096857 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py @@ -0,0 +1,18 @@ +import runpy +from unittest.mock import patch + +import pytest + + +@pytest.mark.parametrize( + "module_name", + [ + "tutorial001_py39", + "tutorial002_py39", + ], +) +def test_run_module(module_name: str): + with patch("builtins.print") as mock_print: + runpy.run_module(f"docs_src.python_types.{module_name}", run_name="__main__") + + mock_print.assert_called_with("John Doe") diff --git a/tests/test_tutorial/test_python_types/test_tutorial003.py b/tests/test_tutorial/test_python_types/test_tutorial003.py new file mode 100644 index 000000000..34d264917 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial003.py @@ -0,0 +1,12 @@ +import pytest + +from docs_src.python_types.tutorial003_py39 import get_name_with_age + + +def test_get_name_with_age_pass_int(): + with pytest.raises(TypeError): + get_name_with_age("John", 30) + + +def test_get_name_with_age_pass_str(): + assert get_name_with_age("John", "30") == "John is this old: 30" diff --git a/tests/test_tutorial/test_python_types/test_tutorial004.py b/tests/test_tutorial/test_python_types/test_tutorial004.py new file mode 100644 index 000000000..24af32883 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial004.py @@ -0,0 +1,5 @@ +from docs_src.python_types.tutorial004_py39 import get_name_with_age + + +def test_get_name_with_age_pass_int(): + assert get_name_with_age("John", 30) == "John is this old: 30" diff --git a/tests/test_tutorial/test_python_types/test_tutorial005.py b/tests/test_tutorial/test_python_types/test_tutorial005.py new file mode 100644 index 000000000..6d67ec471 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial005.py @@ -0,0 +1,12 @@ +from docs_src.python_types.tutorial005_py39 import get_items + + +def test_get_items(): + res = get_items( + "item_a", + "item_b", + "item_c", + "item_d", + "item_e", + ) + assert res == ("item_a", "item_b", "item_c", "item_d", "item_e") diff --git a/tests/test_tutorial/test_python_types/test_tutorial006.py b/tests/test_tutorial/test_python_types/test_tutorial006.py new file mode 100644 index 000000000..50976926e --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial006.py @@ -0,0 +1,16 @@ +from unittest.mock import patch + +from docs_src.python_types.tutorial006_py39 import process_items + + +def test_process_items(): + with patch("builtins.print") as mock_print: + process_items(["item_a", "item_b", "item_c"]) + + assert mock_print.call_count == 3 + call_args = [arg.args for arg in mock_print.call_args_list] + assert call_args == [ + ("item_a",), + ("item_b",), + ("item_c",), + ] diff --git a/tests/test_tutorial/test_python_types/test_tutorial007.py b/tests/test_tutorial/test_python_types/test_tutorial007.py new file mode 100644 index 000000000..c04529465 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial007.py @@ -0,0 +1,8 @@ +from docs_src.python_types.tutorial007_py39 import process_items + + +def test_process_items(): + items_t = (1, 2, "foo") + items_s = {b"a", b"b", b"c"} + + assert process_items(items_t, items_s) == (items_t, items_s) diff --git a/tests/test_tutorial/test_python_types/test_tutorial008.py b/tests/test_tutorial/test_python_types/test_tutorial008.py new file mode 100644 index 000000000..33cf6cbfb --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial008.py @@ -0,0 +1,17 @@ +from unittest.mock import patch + +from docs_src.python_types.tutorial008_py39 import process_items + + +def test_process_items(): + with patch("builtins.print") as mock_print: + process_items({"a": 1.0, "b": 2.5}) + + assert mock_print.call_count == 4 + call_args = [arg.args for arg in mock_print.call_args_list] + assert call_args == [ + ("a",), + (1.0,), + ("b",), + (2.5,), + ] diff --git a/tests/test_tutorial/test_python_types/test_tutorial008b.py b/tests/test_tutorial/test_python_types/test_tutorial008b.py new file mode 100644 index 000000000..1ef0d4ea1 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial008b.py @@ -0,0 +1,27 @@ +import importlib +from types import ModuleType +from unittest.mock import patch + +import pytest + +from ...utils import needs_py310 + + +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial008b_py39"), + pytest.param("tutorial008b_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.python_types.{request.param}") + return mod + + +def test_process_items(module: ModuleType): + with patch("builtins.print") as mock_print: + module.process_item("a") + + assert mock_print.call_count == 1 + mock_print.assert_called_with("a") diff --git a/tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py b/tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py new file mode 100644 index 000000000..34046c5c4 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py @@ -0,0 +1,33 @@ +import importlib +from types import ModuleType +from unittest.mock import patch + +import pytest + +from ...utils import needs_py310 + + +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial009_py39"), + pytest.param("tutorial009_py310", marks=needs_py310), + pytest.param("tutorial009b_py39"), + ], +) +def get_module(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.python_types.{request.param}") + return mod + + +def test_say_hi(module: ModuleType): + with patch("builtins.print") as mock_print: + module.say_hi("FastAPI") + module.say_hi() + + assert mock_print.call_count == 2 + call_args = [arg.args for arg in mock_print.call_args_list] + assert call_args == [ + ("Hey FastAPI!",), + ("Hello World",), + ] diff --git a/tests/test_tutorial/test_python_types/test_tutorial009c.py b/tests/test_tutorial/test_python_types/test_tutorial009c.py new file mode 100644 index 000000000..7bd404911 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial009c.py @@ -0,0 +1,33 @@ +import importlib +import re +from types import ModuleType +from unittest.mock import patch + +import pytest + +from ...utils import needs_py310 + + +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial009c_py39"), + pytest.param("tutorial009c_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.python_types.{request.param}") + return mod + + +def test_say_hi(module: ModuleType): + with patch("builtins.print") as mock_print: + module.say_hi("FastAPI") + + mock_print.assert_called_once_with("Hey FastAPI!") + + with pytest.raises( + TypeError, + match=re.escape("say_hi() missing 1 required positional argument: 'name'"), + ): + module.say_hi() diff --git a/tests/test_tutorial/test_python_types/test_tutorial010.py b/tests/test_tutorial/test_python_types/test_tutorial010.py new file mode 100644 index 000000000..9e4d2e36b --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial010.py @@ -0,0 +1,5 @@ +from docs_src.python_types.tutorial010_py39 import Person, get_person_name + + +def test_get_person_name(): + assert get_person_name(Person("John Doe")) == "John Doe" diff --git a/tests/test_tutorial/test_python_types/test_tutorial011.py b/tests/test_tutorial/test_python_types/test_tutorial011.py new file mode 100644 index 000000000..a05751b97 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial011.py @@ -0,0 +1,25 @@ +import runpy +from unittest.mock import patch + +import pytest + +from ...utils import needs_py310 + + +@pytest.mark.parametrize( + "module_name", + [ + pytest.param("tutorial011_py39"), + pytest.param("tutorial011_py310", marks=needs_py310), + ], +) +def test_run_module(module_name: str): + with patch("builtins.print") as mock_print: + runpy.run_module(f"docs_src.python_types.{module_name}", run_name="__main__") + + assert mock_print.call_count == 2 + call_args = [str(arg.args[0]) for arg in mock_print.call_args_list] + assert call_args == [ + "id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]", + "123", + ] diff --git a/tests/test_tutorial/test_python_types/test_tutorial012.py b/tests/test_tutorial/test_python_types/test_tutorial012.py new file mode 100644 index 000000000..e57804820 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial012.py @@ -0,0 +1,7 @@ +from docs_src.python_types.tutorial012_py39 import User + + +def test_user(): + user = User(name="John Doe", age=30) + assert user.name == "John Doe" + assert user.age == 30 diff --git a/tests/test_tutorial/test_python_types/test_tutorial013.py b/tests/test_tutorial/test_python_types/test_tutorial013.py new file mode 100644 index 000000000..5602ef76f --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial013.py @@ -0,0 +1,5 @@ +from docs_src.python_types.tutorial013_py39 import say_hello + + +def test_say_hello(): + assert say_hello("FastAPI") == "Hello FastAPI" diff --git a/tests/test_tutorial/test_query_params/test_tutorial001.py b/tests/test_tutorial/test_query_params/test_tutorial001.py new file mode 100644 index 000000000..4c92b57b8 --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial001.py @@ -0,0 +1,126 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + ("path", "expected_json"), + [ + ( + "/items/", + [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}], + ), + ( + "/items/?skip=1", + [{"item_name": "Bar"}, {"item_name": "Baz"}], + ), + ( + "/items/?skip=1&limit=1", + [{"item_name": "Bar"}], + ), + ], +) +def test_read_user_item(client: TestClient, path, expected_json): + response = client.get(path) + assert response.status_code == 200 + assert response.json() == expected_json + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Item", + "operationId": "read_item_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 10, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "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/test_tutorial002.py b/tests/test_tutorial/test_query_params/test_tutorial002.py new file mode 100644 index 000000000..ae3ee7613 --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial002.py @@ -0,0 +1,127 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + ("path", "expected_json"), + [ + ( + "/items/foo", + {"item_id": "foo"}, + ), + ( + "/items/bar?q=somequery", + {"item_id": "bar", "q": "somequery"}, + ), + ], +) +def test_read_user_item(client: TestClient, path, expected_json): + response = client.get(path) + assert response.status_code == 200 + assert response.json() == expected_json + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "title": "Q", + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "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/test_tutorial003.py b/tests/test_tutorial/test_query_params/test_tutorial003.py new file mode 100644 index 000000000..c0b7e3b13 --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial003.py @@ -0,0 +1,148 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + ("path", "expected_json"), + [ + ( + "/items/foo", + { + "item_id": "foo", + "description": "This is an amazing item that has a long description", + }, + ), + ( + "/items/bar?q=somequery", + { + "item_id": "bar", + "q": "somequery", + "description": "This is an amazing item that has a long description", + }, + ), + ( + "/items/baz?short=true", + {"item_id": "baz"}, + ), + ], +) +def test_read_user_item(client: TestClient, path, expected_json): + response = client.get(path) + assert response.status_code == 200 + assert response.json() == expected_json + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "title": "Q", + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Short", + "type": "boolean", + "default": False, + }, + "name": "short", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "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/test_tutorial004.py b/tests/test_tutorial/test_query_params/test_tutorial004.py new file mode 100644 index 000000000..9be18b74d --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial004.py @@ -0,0 +1,156 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + ("path", "expected_json"), + [ + ( + "/users/123/items/foo", + { + "item_id": "foo", + "owner_id": 123, + "description": "This is an amazing item that has a long description", + }, + ), + ( + "/users/1/items/bar?q=somequery", + { + "item_id": "bar", + "owner_id": 1, + "q": "somequery", + "description": "This is an amazing item that has a long description", + }, + ), + ( + "/users/42/items/baz?short=true", + {"item_id": "baz", "owner_id": 42}, + ), + ], +) +def test_read_user_item(client: TestClient, path, expected_json): + response = client.get(path) + assert response.status_code == 200 + assert response.json() == expected_json + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/{user_id}/items/{item_id}": { + "get": { + "summary": "Read User Item", + "operationId": "read_user_item_users__user_id__items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "title": "Q", + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Short", + "type": "boolean", + "default": False, + }, + "name": "short", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "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_tutorial001.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py new file mode 100644 index 000000000..f1af7e08c --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py @@ -0,0 +1,121 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_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(mod.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_q_empty_str(client: TestClient): + response = client.get("/items/", params={"q": ""}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +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": { + "anyOf": [ + {"type": "string"}, + {"type": "null"}, + ], + "title": "Q", + }, + "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_tutorial002.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py new file mode 100644 index 000000000..62018b80b --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py @@ -0,0 +1,142 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_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(mod.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_q_empty_str(client: TestClient): + response = client.get("/items/", params={"q": ""}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_query_params_str_validations_q_too_long(client: TestClient): + response = client.get("/items/", params={"q": "q" * 51}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_long", + "loc": ["query", "q"], + "msg": "String should have at most 50 characters", + "input": "q" * 51, + "ctx": {"max_length": 50}, + } + ] + } + + +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": { + "anyOf": [ + { + "type": "string", + "maxLength": 50, + }, + {"type": "null"}, + ], + "title": "Q", + }, + "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_tutorial003.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py new file mode 100644 index 000000000..a4ad7a63b --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py @@ -0,0 +1,153 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + pytest.param("tutorial003_an_py39"), + pytest.param("tutorial003_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(mod.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_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_query_params_str_validations_q_too_short(client: TestClient): + response = client.get("/items/", params={"q": "qu"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "qu", + "ctx": {"min_length": 3}, + } + ] + } + + +def test_query_params_str_validations_q_too_long(client: TestClient): + response = client.get("/items/", params={"q": "q" * 51}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_long", + "loc": ["query", "q"], + "msg": "String should have at most 50 characters", + "input": "q" * 51, + "ctx": {"max_length": 50}, + } + ] + } + + +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": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + }, + {"type": "null"}, + ], + "title": "Q", + }, + "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_tutorial004.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py new file mode 100644 index 000000000..95efab2dc --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py @@ -0,0 +1,147 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + pytest.param("tutorial004_an_py39"), + pytest.param("tutorial004_an_py310", marks=needs_py310), + pytest.param( + "tutorial004_regex_an_py310", + marks=( + needs_py310, + pytest.mark.filterwarnings( + "ignore:`regex` has been deprecated, please use `pattern` instead:DeprecationWarning" + ), + ), + ), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.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_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"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_nonregexquery(client: TestClient): + response = client.get("/items/", params={"q": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + } + ] + } + + +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": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Q", + }, + "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_tutorial005.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py new file mode 100644 index 000000000..52462fe33 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py @@ -0,0 +1,131 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.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"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_query_params_str_validations_q_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +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": { + "type": "string", + "default": "fixedquery", + "minLength": 3, + "title": "Q", + }, + "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_tutorial006.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py new file mode 100644 index 000000000..640cedce1 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py @@ -0,0 +1,136 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial006_py39"), + pytest.param("tutorial006_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } + + +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"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery_too_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +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": True, + "schema": { + "type": "string", + "minLength": 3, + "title": "Q", + }, + "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_tutorial006c.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py new file mode 100644 index 000000000..f287b5dcd --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py @@ -0,0 +1,148 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial006c_py39"), + pytest.param("tutorial006c_py310", marks=needs_py310), + pytest.param("tutorial006c_an_py39"), + pytest.param("tutorial006c_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(mod.app) + return client + + +@pytest.mark.xfail( + reason="Code example is not valid. See https://github.com/fastapi/fastapi/issues/12419" +) +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { # pragma: no cover + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + } + + +@pytest.mark.xfail( + reason="Code example is not valid. See https://github.com/fastapi/fastapi/issues/12419" +) +def test_query_params_str_validations_empty_str(client: TestClient): + response = client.get("/items/?q=") + assert response.status_code == 200 + assert response.json() == { # pragma: no cover + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + } + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_query_params_str_validations_q_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +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": True, + "schema": { + "anyOf": [ + {"type": "string", "minLength": 3}, + {"type": "null"}, + ], + "title": "Q", + }, + "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_tutorial007.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py new file mode 100644 index 000000000..b17bc2771 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py @@ -0,0 +1,136 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial007_py39"), + pytest.param("tutorial007_py310", marks=needs_py310), + pytest.param("tutorial007_an_py39"), + pytest.param("tutorial007_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(mod.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_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"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery_too_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +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": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + }, + {"type": "null"}, + ], + "title": "Query 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_tutorial008.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py new file mode 100644 index 000000000..c63111574 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py @@ -0,0 +1,138 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial008_py39"), + pytest.param("tutorial008_py310", marks=needs_py310), + pytest.param("tutorial008_an_py39"), + pytest.param("tutorial008_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(mod.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_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"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery_too_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +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, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + }, + "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_tutorial009.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py new file mode 100644 index 000000000..7e9d69d41 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py @@ -0,0 +1,123 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial009_py39"), + pytest.param("tutorial009_py310", marks=needs_py310), + pytest.param("tutorial009_an_py39"), + pytest.param("tutorial009_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(mod.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_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": [ + { + "schema": { + "anyOf": [ + {"type": "string"}, + {"type": "null"}, + ], + "title": "Item-Query", + }, + "required": False, + "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_response_directly/test_tutorial002.py b/tests/test_tutorial/test_response_directly/test_tutorial002.py new file mode 100644 index 000000000..ef8457572 --- /dev/null +++ b/tests/test_tutorial/test_response_directly/test_tutorial002.py @@ -0,0 +1,65 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_directly.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_path_operation(client: TestClient): + expected_content = """ + +
+ Apply shampoo here. +
+ + You'll have to use soap here. + +
+ """ + + response = client.get("/legacy/") + assert response.status_code == 200, response.text + assert response.headers["content-type"] == "application/xml" + assert response.text == expected_content + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/legacy/": { + "get": { + "operationId": "get_legacy_data_legacy__get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Get Legacy Data", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py b/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py new file mode 100644 index 000000000..10692f990 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py @@ -0,0 +1,193 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_01_py39"), + pytest.param("tutorial001_01_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_model.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_read_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": None, + "price": 42.0, + "tags": [], + "tax": None, + }, + { + "name": "Plumbus", + "description": None, + "price": 32.0, + "tags": [], + "tax": None, + }, + ] + + +def test_create_item(client: TestClient): + item_data = { + "name": "Test Item", + "description": "A test item", + "price": 10.5, + "tax": 1.5, + "tags": ["test", "item"], + } + response = client.post("/items/", json=item_data) + assert response.status_code == 200, response.text + assert response.json() == item_data + + +def test_create_item_only_required(client: TestClient): + response = client.post( + "/items/", + json={ + "name": "Test Item", + "price": 10.5, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Test Item", + "price": 10.5, + "description": None, + "tax": None, + "tags": [], + } + + +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": { + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + "title": "Response Read Items Items Get", + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item", + "operationId": "create_item_items__post", + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "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_response_model/test_tutorial002.py b/tests/test_tutorial/test_response_model/test_tutorial002.py new file mode 100644 index 000000000..216d4c420 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial002.py @@ -0,0 +1,129 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_model.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_user(client: TestClient): + user_data = { + "username": "foo", + "password": "fighter", + "email": "foo@example.com", + "full_name": "Grave Dohl", + } + response = client.post( + "/user/", + json=user_data, + ) + assert response.status_code == 200, response.text + assert response.json() == user_data + + +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": { + "/user/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "UserIn": { + "title": "UserIn", + "required": ["username", "password", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "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_response_status_code/__init__.py b/tests/test_tutorial/test_response_status_code/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py new file mode 100644 index 000000000..ddf55a045 --- /dev/null +++ b/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py @@ -0,0 +1,96 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial002_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_status_code.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_create_item(client: TestClient): + response = client.post("/items/", params={"name": "Test Item"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Test Item"} + + +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/": { + "post": { + "parameters": [ + { + "name": "name", + "in": "query", + "required": True, + "schema": {"title": "Name", "type": "string"}, + } + ], + "summary": "Create Item", + "operationId": "create_item_items__post", + "responses": { + "201": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "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_schema_extra_example/test_tutorial002.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py new file mode 100644 index 000000000..4f5240860 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py @@ -0,0 +1,141 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "examples": ["Foo"], + }, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + "examples": ["A very nice Item"], + }, + "price": { + "type": "number", + "title": "Price", + "examples": [35.4], + }, + "tax": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Tax", + "examples": [3.2], + }, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py new file mode 100644 index 000000000..3529a9bf0 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py @@ -0,0 +1,143 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + pytest.param("tutorial003_an_py39"), + pytest.param("tutorial003_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "description": "A very nice Item", + "name": "Foo", + "price": 35.4, + "tax": 3.2, + } + ], + }, + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"type": "number", "title": "Price"}, + "tax": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Tax", + }, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial002.py b/tests/test_tutorial/test_security/test_tutorial002.py new file mode 100644 index 000000000..85c076b1d --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial002.py @@ -0,0 +1,71 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") + client = TestClient(mod.app) + return client + + +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "testtokenfakedecoded", + "email": "john@example.com", + "full_name": "John Doe", + "disabled": None, + } + + +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": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial004.py b/tests/test_tutorial/test_security/test_tutorial004.py new file mode 100644 index 000000000..b5e3d39ef --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial004.py @@ -0,0 +1,363 @@ +import importlib +from types import ModuleType +from unittest.mock import patch + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + pytest.param("tutorial004_an_py39"), + pytest.param("tutorial004_an_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") + + return mod + + +def get_access_token(*, username="johndoe", password="secret", client: TestClient): + data = {"username": username, "password": password} + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +def test_login(mod: ModuleType): + client = TestClient(mod.app) + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +def test_login_incorrect_password(mod: ModuleType): + client = TestClient(mod.app) + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_login_incorrect_username(mod: ModuleType): + client = TestClient(mod.app) + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_no_token(mod: ModuleType): + client = TestClient(mod.app) + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token(client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +def test_incorrect_token(mod: ModuleType): + client = TestClient(mod.app) + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_incorrect_token_type(mod: ModuleType): + client = TestClient(mod.app) + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_verify_password(mod: ModuleType): + assert mod.verify_password( + "secret", mod.fake_users_db["johndoe"]["hashed_password"] + ) + + +def test_get_password_hash(mod: ModuleType): + assert mod.get_password_hash("johndoe") + + +def test_create_access_token(mod: ModuleType): + access_token = mod.create_access_token(data={"data": "foo"}) + assert access_token + + +def test_token_no_sub(mod: ModuleType): + client = TestClient(mod.app) + + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token_no_username(mod: ModuleType): + client = TestClient(mod.app) + + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token_nonexistent_user(mod: ModuleType): + client = TestClient(mod.app) + + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token_inactive_user(mod: ModuleType): + client = TestClient(mod.app) + alice_user_data = { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": mod.get_password_hash("secretalice"), + "disabled": True, + } + with patch.dict(f"{mod.__name__}.fake_users_db", {"alice": alice_user_data}): + access_token = get_access_token( + username="alice", password="secretalice", client=client + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +def test_read_items(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token(client=client) + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +def test_openapi_schema(mod: ModuleType): + client = TestClient(mod.app) + 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": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "disabled": { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + }, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "anyOf": [ + {"pattern": "^password$", "type": "string"}, + {"type": "null"}, + ], + }, + "username": {"title": "Username", "type": "string"}, + "password": { + "title": "Password", + "type": "string", + "format": "password", + }, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + "format": "password", + }, + }, + }, + "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"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": {}, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial007.py b/tests/test_tutorial/test_security/test_tutorial007.py new file mode 100644 index 000000000..28b70a2d4 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial007.py @@ -0,0 +1,89 @@ +import importlib +from base64 import b64encode + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial007_py39"), + pytest.param("tutorial007_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") + return TestClient(mod.app) + + +def test_security_http_basic(client: TestClient): + response = client.get("/users/me", auth=("stanleyjobson", "swordfish")) + assert response.status_code == 200, response.text + assert response.json() == {"username": "stanleyjobson"} + + +def test_security_http_basic_no_credentials(client: TestClient): + response = client.get("/users/me") + assert response.json() == {"detail": "Not authenticated"} + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + + +def test_security_http_basic_invalid_credentials(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Basic notabase64token"} + ) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Not authenticated"} + + +def test_security_http_basic_non_basic_credentials(client: TestClient): + payload = b64encode(b"johnsecret").decode("ascii") + auth_header = f"Basic {payload}" + response = client.get("/users/me", headers={"Authorization": auth_header}) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Not authenticated"} + + +def test_security_http_basic_invalid_username(client: TestClient): + response = client.get("/users/me", auth=("alice", "swordfish")) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Incorrect username or password"} + assert response.headers["WWW-Authenticate"] == "Basic" + + +def test_security_http_basic_invalid_password(client: TestClient): + response = client.get("/users/me", auth=("stanleyjobson", "wrongpassword")) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Incorrect username or password"} + assert response.headers["WWW-Authenticate"] == "Basic" + + +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": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } diff --git a/tests/test_tutorial/test_settings/test_app01.py b/tests/test_tutorial/test_settings/test_app01.py new file mode 100644 index 000000000..0c5e440f1 --- /dev/null +++ b/tests/test_tutorial/test_settings/test_app01.py @@ -0,0 +1,78 @@ +import importlib +import sys + +import pytest +from dirty_equals import IsAnyStr +from fastapi.testclient import TestClient +from pydantic import ValidationError +from pytest import MonkeyPatch + + +@pytest.fixture( + name="mod_name", + params=[ + pytest.param("app01_py39"), + ], +) +def get_mod_name(request: pytest.FixtureRequest): + return f"docs_src.settings.{request.param}.main" + + +@pytest.fixture(name="client") +def get_test_client(mod_name: str, monkeypatch: MonkeyPatch) -> TestClient: + if mod_name in sys.modules: + del sys.modules[mod_name] + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + main_mod = importlib.import_module(mod_name) + return TestClient(main_mod.app) + + +def test_settings_validation_error(mod_name: str, monkeypatch: MonkeyPatch): + monkeypatch.delenv("ADMIN_EMAIL", raising=False) + if mod_name in sys.modules: + del sys.modules[mod_name] # pragma: no cover + + with pytest.raises(ValidationError) as exc_info: + importlib.import_module(mod_name) + assert exc_info.value.errors() == [ + { + "loc": ("admin_email",), + "msg": "Field required", + "type": "missing", + "input": {}, + "url": IsAnyStr, + } + ] + + +def test_app(client: TestClient): + response = client.get("/info") + data = response.json() + assert data == { + "app_name": "Awesome API", + "admin_email": "admin@example.com", + "items_per_user": 50, + } + + +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": { + "/info": { + "get": { + "operationId": "info_info_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Info", + } + } + }, + } diff --git a/tests/test_tutorial/test_static_files/__init__.py b/tests/test_tutorial/test_static_files/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_static_files/test_tutorial001.py b/tests/test_tutorial/test_static_files/test_tutorial001.py new file mode 100644 index 000000000..4fbf19ae8 --- /dev/null +++ b/tests/test_tutorial/test_static_files/test_tutorial001.py @@ -0,0 +1,40 @@ +import os +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture(scope="module") +def client(): + static_dir: Path = Path(os.getcwd()) / "static" + static_dir.mkdir(exist_ok=True) + sample_file = static_dir / "sample.txt" + sample_file.write_text("This is a sample static file.") + from docs_src.static_files.tutorial001_py39 import app + + with TestClient(app) as client: + yield client + sample_file.unlink() + static_dir.rmdir() + + +def test_static_files(client: TestClient): + response = client.get("/static/sample.txt") + assert response.status_code == 200, response.text + assert response.text == "This is a sample static file." + + +def test_static_files_not_found(client: TestClient): + response = client.get("/static/non_existent_file.txt") + assert response.status_code == 404, response.text + + +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": {}, + } From 8cefc4b7cc9acff703f3e7327aba7274d83022c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 10:43:27 +0000 Subject: [PATCH 012/243] =?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 bc2441f7f..11dae3333 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Internal +* ✅ Add missing tests for code examples. PR [#14569](https://github.com/fastapi/fastapi/pull/14569) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Remove `lint` job from `test` CI workflow. PR [#14593](https://github.com/fastapi/fastapi/pull/14593) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo). * 👷 Run CodSpeed tests in parallel to other tests to speed up CI. PR [#14586](https://github.com/fastapi/fastapi/pull/14586) by [@tiangolo](https://github.com/tiangolo). From d98f4eb56eae3155034fe631d9639d95171fb9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 03:36:58 -0800 Subject: [PATCH 013/243] =?UTF-8?q?=F0=9F=94=A7=20Update=20pre-commit=20to?= =?UTF-8?q?=20use=20local=20Ruff=20instead=20of=20hook=20(#14604)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77e06bd96..10a0949e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,15 +12,23 @@ repos: - --unsafe - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.3 - hooks: - - id: ruff - args: - - --fix - - id: ruff-format + - repo: local hooks: + - id: local-ruff-check + name: ruff check + entry: uv run ruff check --force-exclude --fix --exit-non-zero-on-fix + require_serial: true + language: unsupported + types: [python] + + - id: local-ruff-format + name: ruff format + entry: uv run ruff format --force-exclude --exit-non-zero-on-format + require_serial: true + language: unsupported + types: [python] + - id: add-permalinks-pages language: unsupported name: add-permalinks-pages @@ -28,18 +36,21 @@ repos: args: - --update-existing files: ^docs/en/docs/.*\.md$ + - id: generate-readme language: unsupported name: generate README.md from index.md entry: uv run ./scripts/docs.py generate-readme files: ^docs/en/docs/index\.md|docs/en/data/sponsors\.yml|scripts/docs\.py$ pass_filenames: false + - id: update-languages language: unsupported name: update languages entry: uv run ./scripts/docs.py update-languages files: ^docs/.*|scripts/docs\.py$ pass_filenames: false + - id: ensure-non-translated language: unsupported name: ensure non-translated files are not modified From 6b53786f626db95a42fd0f66afd360bab161f4b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 11:37:18 +0000 Subject: [PATCH 014/243] =?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 11dae3333..98fd67da2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Internal +* 🔧 Update pre-commit to use local Ruff instead of hook. PR [#14604](https://github.com/fastapi/fastapi/pull/14604) by [@tiangolo](https://github.com/tiangolo). * ✅ Add missing tests for code examples. PR [#14569](https://github.com/fastapi/fastapi/pull/14569) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Remove `lint` job from `test` CI workflow. PR [#14593](https://github.com/fastapi/fastapi/pull/14593) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo). From 535b5daa317a9d1d1f9a1058e57650a7beefa861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 04:45:20 -0800 Subject: [PATCH 015/243] =?UTF-8?q?=F0=9F=94=8A=20Add=20a=20custom=20`Fast?= =?UTF-8?q?APIDeprecationWarning`=20(#14605)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 4 ++-- fastapi/exceptions.py | 7 +++++++ fastapi/openapi/utils.py | 5 +++-- fastapi/params.py | 9 ++++---- fastapi/routing.py | 5 +++-- fastapi/temp_pydantic_v1_params.py | 9 ++++---- fastapi/utils.py | 5 +++-- tests/benchmarks/test_general_performance.py | 3 ++- tests/test_compat_params_v1.py | 9 ++++---- .../test_pydantic_v1_deprecation_warnings.py | 9 ++++---- tests/test_regex_deprecated_body.py | 3 ++- tests/test_regex_deprecated_params.py | 3 ++- tests/test_schema_extra_examples.py | 21 ++++++++++--------- .../test_tutorial002.py | 3 ++- .../test_tutorial003.py | 3 ++- .../test_tutorial004.py | 3 ++- .../test_tutorial004.py | 2 +- .../test_tutorial002_pv1.py | 3 ++- .../test_tutorial001_pv1.py | 3 ++- 19 files changed, 66 insertions(+), 43 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 39d0bd89c..af2bed9ad 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -51,7 +51,7 @@ from fastapi.concurrency import ( contextmanager_in_threadpool, ) from fastapi.dependencies.models import Dependant -from fastapi.exceptions import DependencyScopeError +from fastapi.exceptions import DependencyScopeError, FastAPIDeprecationWarning from fastapi.logger import logger from fastapi.security.oauth2 import SecurityScopes from fastapi.types import DependencyCacheKey @@ -327,7 +327,7 @@ def get_dependant( warnings.warn( "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." f" Please update the param {param_name}: {param_details.type_annotation!r}.", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=5, ) if isinstance( diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 8e0c55902..53e505281 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -231,3 +231,10 @@ class ResponseValidationError(ValidationException): ) -> None: super().__init__(errors, endpoint_ctx=endpoint_ctx) self.body = body + + +class FastAPIDeprecationWarning(UserWarning): + """ + A custom deprecation warning as DeprecationWarning is ignored + Ref: https://sethmlarson.dev/deprecations-via-warnings-dont-work-for-python-libraries + """ diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index a99d4188e..6180fcde6 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -23,6 +23,7 @@ from fastapi.dependencies.utils import ( get_validation_alias, ) from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX from fastapi.openapi.models import OpenAPI from fastapi.params import Body, ParamTypes @@ -215,9 +216,9 @@ def generate_operation_id( *, route: routing.APIRoute, method: str ) -> str: # pragma: nocover warnings.warn( - "fastapi.openapi.utils.generate_operation_id() was deprecated, " + message="fastapi.openapi.utils.generate_operation_id() was deprecated, " "it is not used internally, and will be removed soon", - DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=2, ) if route.operation_id: diff --git a/fastapi/params.py b/fastapi/params.py index c776c4a59..cc2934f44 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from enum import Enum from typing import Annotated, Any, Callable, Optional, Union +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.openapi.models import Example from pydantic.fields import FieldInfo from typing_extensions import Literal, deprecated @@ -75,7 +76,7 @@ class Param(FieldInfo): # type: ignore[misc] if example is not _Unset: warnings.warn( "`example` has been deprecated, please use `examples` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) self.example = example @@ -105,7 +106,7 @@ class Param(FieldInfo): # type: ignore[misc] if regex is not None: warnings.warn( "`regex` has been deprecated, please use `pattern` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra @@ -530,7 +531,7 @@ class Body(FieldInfo): # type: ignore[misc] if example is not _Unset: warnings.warn( "`example` has been deprecated, please use `examples` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) self.example = example @@ -560,7 +561,7 @@ class Body(FieldInfo): # type: ignore[misc] if regex is not None: warnings.warn( "`regex` has been deprecated, please use `pattern` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra diff --git a/fastapi/routing.py b/fastapi/routing.py index 2770e3253..3f78e93e8 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -47,6 +47,7 @@ from fastapi.dependencies.utils import ( from fastapi.encoders import jsonable_encoder from fastapi.exceptions import ( EndpointContext, + FastAPIDeprecationWarning, FastAPIError, RequestValidationError, ResponseValidationError, @@ -640,7 +641,7 @@ class APIRoute(routing.Route): warnings.warn( "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." f" Please update the response model {self.response_model!r}.", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) self.response_field = create_model_field( @@ -680,7 +681,7 @@ class APIRoute(routing.Route): warnings.warn( "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." f" In responses={{}}, please update {model}.", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) response_field = create_model_field( diff --git a/fastapi/temp_pydantic_v1_params.py b/fastapi/temp_pydantic_v1_params.py index 1bda0ea9b..62230e42c 100644 --- a/fastapi/temp_pydantic_v1_params.py +++ b/fastapi/temp_pydantic_v1_params.py @@ -1,6 +1,7 @@ import warnings from typing import Annotated, Any, Callable, Optional, Union +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.openapi.models import Example from fastapi.params import ParamTypes from typing_extensions import deprecated @@ -63,7 +64,7 @@ class Param(FieldInfo): if example is not _Unset: warnings.warn( "`example` has been deprecated, please use `examples` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) self.example = example @@ -93,7 +94,7 @@ class Param(FieldInfo): if regex is not None: warnings.warn( "`regex` has been deprecated, please use `pattern` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra @@ -503,7 +504,7 @@ class Body(FieldInfo): if example is not _Unset: warnings.warn( "`example` has been deprecated, please use `examples` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) self.example = example @@ -533,7 +534,7 @@ class Body(FieldInfo): if regex is not None: warnings.warn( "`regex` has been deprecated, please use `pattern` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra diff --git a/fastapi/utils.py b/fastapi/utils.py index c4631d7ed..8ae50aa14 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -23,6 +23,7 @@ from fastapi._compat import ( may_v1, ) from fastapi.datastructures import DefaultPlaceholder, DefaultType +from fastapi.exceptions import FastAPIDeprecationWarning from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Literal @@ -195,9 +196,9 @@ def generate_operation_id_for_path( *, name: str, path: str, method: str ) -> str: # pragma: nocover warnings.warn( - "fastapi.utils.generate_operation_id_for_path() was deprecated, " + message="fastapi.utils.generate_operation_id_for_path() was deprecated, " "it is not used internally, and will be removed soon", - DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=2, ) operation_id = f"{name}{path}" diff --git a/tests/benchmarks/test_general_performance.py b/tests/benchmarks/test_general_performance.py index 2da74b95c..dac297e4e 100644 --- a/tests/benchmarks/test_general_performance.py +++ b/tests/benchmarks/test_general_performance.py @@ -6,6 +6,7 @@ from typing import Annotated, Any import pytest from fastapi import Depends, FastAPI +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient if "--codspeed" not in sys.argv: @@ -89,7 +90,7 @@ def app(basemodel_class: type[Any]) -> FastAPI: warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, ) @app.post("/sync/validated", response_model=ItemOut) diff --git a/tests/test_compat_params_v1.py b/tests/test_compat_params_v1.py index 2ac96993a..704b3f77a 100644 --- a/tests/test_compat_params_v1.py +++ b/tests/test_compat_params_v1.py @@ -3,6 +3,7 @@ import warnings from typing import Optional import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from tests.utils import skip_module_if_py_gte_314 @@ -504,23 +505,23 @@ def test_body_repr(): # Deprecation warning tests for regex parameter def test_query_regex_deprecation_warning(): - with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"): + with pytest.warns(FastAPIDeprecationWarning, match="`regex` has been deprecated"): Query(regex="^test$") def test_body_regex_deprecation_warning(): - with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"): + with pytest.warns(FastAPIDeprecationWarning, match="`regex` has been deprecated"): Body(regex="^test$") # Deprecation warning tests for example parameter def test_query_example_deprecation_warning(): - with pytest.warns(DeprecationWarning, match="`example` has been deprecated"): + with pytest.warns(FastAPIDeprecationWarning, match="`example` has been deprecated"): Query(example="test example") def test_body_example_deprecation_warning(): - with pytest.warns(DeprecationWarning, match="`example` has been deprecated"): + with pytest.warns(FastAPIDeprecationWarning, match="`example` has been deprecated"): Body(example={"test": "example"}) diff --git a/tests/test_pydantic_v1_deprecation_warnings.py b/tests/test_pydantic_v1_deprecation_warnings.py index e0008e218..89ca6a865 100644 --- a/tests/test_pydantic_v1_deprecation_warnings.py +++ b/tests/test_pydantic_v1_deprecation_warnings.py @@ -1,6 +1,7 @@ import sys import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from tests.utils import skip_module_if_py_gte_314 @@ -19,7 +20,7 @@ def test_warns_pydantic_v1_model_in_endpoint_param() -> None: app = FastAPI() with pytest.warns( - DeprecationWarning, + FastAPIDeprecationWarning, match=r"pydantic\.v1 is deprecated.*Please update the param data:", ): @@ -40,7 +41,7 @@ def test_warns_pydantic_v1_model_in_return_type() -> None: app = FastAPI() with pytest.warns( - DeprecationWarning, + FastAPIDeprecationWarning, match=r"pydantic\.v1 is deprecated.*Please update the response model", ): @@ -61,7 +62,7 @@ def test_warns_pydantic_v1_model_in_response_model() -> None: app = FastAPI() with pytest.warns( - DeprecationWarning, + FastAPIDeprecationWarning, match=r"pydantic\.v1 is deprecated.*Please update the response model", ): @@ -82,7 +83,7 @@ def test_warns_pydantic_v1_model_in_additional_responses_model() -> None: app = FastAPI() with pytest.warns( - DeprecationWarning, + FastAPIDeprecationWarning, match=r"pydantic\.v1 is deprecated.*In responses=\{\}, please update", ): diff --git a/tests/test_regex_deprecated_body.py b/tests/test_regex_deprecated_body.py index cfbff19c8..9d58c5dae 100644 --- a/tests/test_regex_deprecated_body.py +++ b/tests/test_regex_deprecated_body.py @@ -3,6 +3,7 @@ from typing import Annotated import pytest from dirty_equals import IsDict from fastapi import FastAPI, Form +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from .utils import needs_py310 @@ -10,7 +11,7 @@ from .utils import needs_py310 def get_client(): app = FastAPI() - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.post("/items/") async def read_items( diff --git a/tests/test_regex_deprecated_params.py b/tests/test_regex_deprecated_params.py index 7d9988f9f..8973657a9 100644 --- a/tests/test_regex_deprecated_params.py +++ b/tests/test_regex_deprecated_params.py @@ -3,6 +3,7 @@ from typing import Annotated import pytest from dirty_equals import IsDict from fastapi import FastAPI, Query +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from .utils import needs_py310 @@ -10,7 +11,7 @@ from .utils import needs_py310 def get_client(): app = FastAPI() - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/items/") async def read_items( diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index 176b5588d..8f5195ba1 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -3,6 +3,7 @@ from typing import Union import pytest from dirty_equals import IsDict from fastapi import Body, Cookie, FastAPI, Header, Path, Query +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict @@ -21,7 +22,7 @@ def create_app(): def schema_extra(item: Item): return item - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.post("/example/") def example(item: Item = Body(example={"data": "Data in Body example"})): @@ -38,7 +39,7 @@ def create_app(): ): return item - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.post("/example_examples/") def example_examples( @@ -83,7 +84,7 @@ def create_app(): # ): # return lastname - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/path_example/{item_id}") def path_example( @@ -101,7 +102,7 @@ def create_app(): ): return item_id - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/path_example_examples/{item_id}") def path_example_examples( @@ -112,7 +113,7 @@ def create_app(): ): return item_id - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/query_example/") def query_example( @@ -132,7 +133,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/query_example_examples/") def query_example_examples( @@ -144,7 +145,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/header_example/") def header_example( @@ -167,7 +168,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/header_example_examples/") def header_example_examples( @@ -179,7 +180,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/cookie_example/") def cookie_example( @@ -199,7 +200,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/cookie_example_examples/") def cookie_example_examples( diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py index ab7e1d8a7..9d1baf853 100644 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py @@ -2,6 +2,7 @@ import sys import warnings import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from inline_snapshot import snapshot from tests.utils import skip_module_if_py_gte_314 @@ -29,7 +30,7 @@ def get_client(request: pytest.FixtureRequest): warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, ) mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py index c45e04248..23b236888 100644 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py @@ -2,6 +2,7 @@ import sys import warnings import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from inline_snapshot import snapshot from tests.utils import skip_module_if_py_gte_314 @@ -29,7 +30,7 @@ def get_client(request: pytest.FixtureRequest): warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, ) mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py index f3da849e0..61c0f6357 100644 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py @@ -2,6 +2,7 @@ import sys import warnings import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from inline_snapshot import snapshot from tests.utils import skip_module_if_py_gte_314 @@ -29,7 +30,7 @@ def get_client(request: pytest.FixtureRequest): warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, ) mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py index 95efab2dc..585989a82 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py @@ -18,7 +18,7 @@ from ...utils import needs_py310 marks=( needs_py310, pytest.mark.filterwarnings( - "ignore:`regex` has been deprecated, please use `pattern` instead:DeprecationWarning" + "ignore:`regex` has been deprecated, please use `pattern` instead:fastapi.exceptions.FastAPIDeprecationWarning" ), ), ), diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py index 515a5a8d7..50be45896 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py @@ -2,6 +2,7 @@ import importlib import warnings import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from ...utils import needs_pydanticv1 @@ -19,7 +20,7 @@ def get_client(request: pytest.FixtureRequest): warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, ) mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py index c5526b19c..83c717656 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py @@ -2,6 +2,7 @@ import importlib import warnings import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -20,7 +21,7 @@ def get_client(request: pytest.FixtureRequest): warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, ) mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") From 93f4dfd88b7d62bda89eb521ebd9322e357e6fff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 12:46:00 +0000 Subject: [PATCH 016/243] =?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 98fd67da2..2af8abdfd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* 🔊 Add a custom `FastAPIDeprecationWarning`. PR [#14605](https://github.com/fastapi/fastapi/pull/14605) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Add documentary to website. PR [#14600](https://github.com/fastapi/fastapi/pull/14600) by [@tiangolo](https://github.com/tiangolo). From cd90c78391f86f3fd06d5b327bffad9666b05b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 14:02:41 +0100 Subject: [PATCH 017/243] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?7.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2af8abdfd..3e9084d47 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.127.1 + ### Refactors * 🔊 Add a custom `FastAPIDeprecationWarning`. PR [#14605](https://github.com/fastapi/fastapi/pull/14605) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 73df6dc6c..dc447c8bf 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.127.0" +__version__ = "0.127.1" from starlette import status as status From 34e884156f38dff4094a7764ff895c4d4b872060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 12:40:26 -0800 Subject: [PATCH 018/243] =?UTF-8?q?=E2=9C=85=20Run=20performance=20tests?= =?UTF-8?q?=20only=20on=20Pydantic=20v2=20(#14608)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/benchmarks/test_general_performance.py | 272 +++++++++---------- 1 file changed, 133 insertions(+), 139 deletions(-) diff --git a/tests/benchmarks/test_general_performance.py b/tests/benchmarks/test_general_performance.py index dac297e4e..87add6d17 100644 --- a/tests/benchmarks/test_general_performance.py +++ b/tests/benchmarks/test_general_performance.py @@ -1,13 +1,12 @@ import json import sys -import warnings from collections.abc import Iterator from typing import Annotated, Any import pytest from fastapi import Depends, FastAPI -from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient +from pydantic import BaseModel if "--codspeed" not in sys.argv: pytest.skip( @@ -47,148 +46,143 @@ def dep_b(a: Annotated[int, Depends(dep_a)]): return a + 2 -@pytest.fixture( - scope="module", - params=[ - "pydantic-v2", - "pydantic-v1", - ], -) -def basemodel_class(request: pytest.FixtureRequest) -> type[Any]: - if request.param == "pydantic-v2": - from pydantic import BaseModel +class ItemIn(BaseModel): + name: str + value: int - return BaseModel - else: - from pydantic.v1 import BaseModel - return BaseModel +class ItemOut(BaseModel): + name: str + value: int + dep: int + + +class LargeIn(BaseModel): + items: list[dict[str, Any]] + metadata: dict[str, Any] + + +class LargeOut(BaseModel): + items: list[dict[str, Any]] + metadata: dict[str, Any] + + +app = FastAPI() + + +@app.post("/sync/validated", response_model=ItemOut) +def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name=item.name, value=item.value, dep=dep) + + +@app.get("/sync/dict-no-response-model") +def sync_dict_no_response_model(): + return {"name": "foo", "value": 123} + + +@app.get("/sync/dict-with-response-model", response_model=ItemOut) +def sync_dict_with_response_model( + dep: Annotated[int, Depends(dep_b)], +): + return {"name": "foo", "value": 123, "dep": dep} + + +@app.get("/sync/model-no-response-model") +def sync_model_no_response_model(dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name="foo", value=123, dep=dep) + + +@app.get("/sync/model-with-response-model", response_model=ItemOut) +def sync_model_with_response_model(dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name="foo", value=123, dep=dep) + + +@app.post("/async/validated", response_model=ItemOut) +async def async_validated( + item: ItemIn, + dep: Annotated[int, Depends(dep_b)], +): + return ItemOut(name=item.name, value=item.value, dep=dep) + + +@app.post("/sync/large-receive") +def sync_large_receive(payload: LargeIn): + return {"received": len(payload.items)} + + +@app.post("/async/large-receive") +async def async_large_receive(payload: LargeIn): + return {"received": len(payload.items)} + + +@app.get("/sync/large-dict-no-response-model") +def sync_large_dict_no_response_model(): + return LARGE_PAYLOAD + + +@app.get("/sync/large-dict-with-response-model", response_model=LargeOut) +def sync_large_dict_with_response_model(): + return LARGE_PAYLOAD + + +@app.get("/sync/large-model-no-response-model") +def sync_large_model_no_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + +@app.get("/sync/large-model-with-response-model", response_model=LargeOut) +def sync_large_model_with_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + +@app.get("/async/large-dict-no-response-model") +async def async_large_dict_no_response_model(): + return LARGE_PAYLOAD + + +@app.get("/async/large-dict-with-response-model", response_model=LargeOut) +async def async_large_dict_with_response_model(): + return LARGE_PAYLOAD + + +@app.get("/async/large-model-no-response-model") +async def async_large_model_no_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + +@app.get("/async/large-model-with-response-model", response_model=LargeOut) +async def async_large_model_with_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + +@app.get("/async/dict-no-response-model") +async def async_dict_no_response_model(): + return {"name": "foo", "value": 123} + + +@app.get("/async/dict-with-response-model", response_model=ItemOut) +async def async_dict_with_response_model( + dep: Annotated[int, Depends(dep_b)], +): + return {"name": "foo", "value": 123, "dep": dep} + + +@app.get("/async/model-no-response-model") +async def async_model_no_response_model( + dep: Annotated[int, Depends(dep_b)], +): + return ItemOut(name="foo", value=123, dep=dep) + + +@app.get("/async/model-with-response-model", response_model=ItemOut) +async def async_model_with_response_model( + dep: Annotated[int, Depends(dep_b)], +): + return ItemOut(name="foo", value=123, dep=dep) @pytest.fixture(scope="module") -def app(basemodel_class: type[Any]) -> FastAPI: - class ItemIn(basemodel_class): - name: str - value: int - - class ItemOut(basemodel_class): - name: str - value: int - dep: int - - class LargeIn(basemodel_class): - items: list[dict[str, Any]] - metadata: dict[str, Any] - - class LargeOut(basemodel_class): - items: list[dict[str, Any]] - metadata: dict[str, Any] - - app = FastAPI() - - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - - @app.post("/sync/validated", response_model=ItemOut) - def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]): - return ItemOut(name=item.name, value=item.value, dep=dep) - - @app.get("/sync/dict-no-response-model") - def sync_dict_no_response_model(): - return {"name": "foo", "value": 123} - - @app.get("/sync/dict-with-response-model", response_model=ItemOut) - def sync_dict_with_response_model( - dep: Annotated[int, Depends(dep_b)], - ): - return {"name": "foo", "value": 123, "dep": dep} - - @app.get("/sync/model-no-response-model") - def sync_model_no_response_model(dep: Annotated[int, Depends(dep_b)]): - return ItemOut(name="foo", value=123, dep=dep) - - @app.get("/sync/model-with-response-model", response_model=ItemOut) - def sync_model_with_response_model(dep: Annotated[int, Depends(dep_b)]): - return ItemOut(name="foo", value=123, dep=dep) - - @app.post("/async/validated", response_model=ItemOut) - async def async_validated( - item: ItemIn, - dep: Annotated[int, Depends(dep_b)], - ): - return ItemOut(name=item.name, value=item.value, dep=dep) - - @app.post("/sync/large-receive") - def sync_large_receive(payload: LargeIn): - return {"received": len(payload.items)} - - @app.post("/async/large-receive") - async def async_large_receive(payload: LargeIn): - return {"received": len(payload.items)} - - @app.get("/sync/large-dict-no-response-model") - def sync_large_dict_no_response_model(): - return LARGE_PAYLOAD - - @app.get("/sync/large-dict-with-response-model", response_model=LargeOut) - def sync_large_dict_with_response_model(): - return LARGE_PAYLOAD - - @app.get("/sync/large-model-no-response-model") - def sync_large_model_no_response_model(): - return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) - - @app.get("/sync/large-model-with-response-model", response_model=LargeOut) - def sync_large_model_with_response_model(): - return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) - - @app.get("/async/large-dict-no-response-model") - async def async_large_dict_no_response_model(): - return LARGE_PAYLOAD - - @app.get("/async/large-dict-with-response-model", response_model=LargeOut) - async def async_large_dict_with_response_model(): - return LARGE_PAYLOAD - - @app.get("/async/large-model-no-response-model") - async def async_large_model_no_response_model(): - return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) - - @app.get("/async/large-model-with-response-model", response_model=LargeOut) - async def async_large_model_with_response_model(): - return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) - - @app.get("/async/dict-no-response-model") - async def async_dict_no_response_model(): - return {"name": "foo", "value": 123} - - @app.get("/async/dict-with-response-model", response_model=ItemOut) - async def async_dict_with_response_model( - dep: Annotated[int, Depends(dep_b)], - ): - return {"name": "foo", "value": 123, "dep": dep} - - @app.get("/async/model-no-response-model") - async def async_model_no_response_model( - dep: Annotated[int, Depends(dep_b)], - ): - return ItemOut(name="foo", value=123, dep=dep) - - @app.get("/async/model-with-response-model", response_model=ItemOut) - async def async_model_with_response_model( - dep: Annotated[int, Depends(dep_b)], - ): - return ItemOut(name="foo", value=123, dep=dep) - - return app - - -@pytest.fixture(scope="module") -def client(app: FastAPI) -> Iterator[TestClient]: +def client() -> Iterator[TestClient]: with TestClient(app) as client: yield client From 1b3bea8b6be2220cc1544653ea785d2134fcb35b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 20:40:51 +0000 Subject: [PATCH 019/243] =?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 3e9084d47..10b262b48 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* ✅ Run performance tests only on Pydantic v2. PR [#14608](https://github.com/fastapi/fastapi/pull/14608) by [@tiangolo](https://github.com/tiangolo). + ## 0.127.1 ### Refactors From e3006305518a56ea35f62a31748ad26fe4356fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 04:54:56 -0800 Subject: [PATCH 020/243] =?UTF-8?q?=E2=9E=96=20Drop=20support=20for=20`pyd?= =?UTF-8?q?antic.v1`=20(#14609)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial002_pv1_an_py39.py | 20 - .../tutorial002_pv1_py39.py | 18 - fastapi/_compat/__init__.py | 64 +- fastapi/_compat/main.py | 264 ---- fastapi/_compat/may_v1.py | 124 -- fastapi/_compat/model_field.py | 50 - fastapi/_compat/shared.py | 24 +- fastapi/_compat/v1.py | 222 --- fastapi/_compat/v2.py | 59 +- fastapi/datastructures.py | 15 +- fastapi/dependencies/utils.py | 116 +- fastapi/encoders.py | 37 +- fastapi/exceptions.py | 6 + fastapi/openapi/models.py | 24 +- fastapi/openapi/utils.py | 13 +- fastapi/param_functions.py | 29 +- fastapi/params.py | 33 +- fastapi/routing.py | 104 +- fastapi/temp_pydantic_v1_params.py | 718 --------- fastapi/utils.py | 109 +- pyproject.toml | 22 +- tests/test_compat.py | 67 +- tests/test_compat_params_v1.py | 1060 ------------- tests/test_datetime_custom_encoder.py | 34 - .../__init__.py | 0 .../test_filter_pydantic_sub_model/app_pv1.py | 45 - .../test_filter_pydantic_sub_model_pv1.py | 146 -- ...t_get_model_definitions_formfeed_escape.py | 47 +- tests/test_inherited_custom_class.py | 45 - tests/test_jsonable_encoder.py | 63 +- .../test_pydantic_v1_deprecation_warnings.py | 99 -- tests/test_pydantic_v1_error.py | 97 ++ tests/test_pydantic_v1_v2_01.py | 439 ----- tests/test_pydantic_v1_v2_list.py | 682 -------- tests/test_pydantic_v1_v2_mixed.py | 1408 ----------------- .../test_pydantic_v1_v2_multifile/__init__.py | 0 tests/test_pydantic_v1_v2_multifile/main.py | 137 -- .../test_pydantic_v1_v2_multifile/modelsv1.py | 19 - .../test_pydantic_v1_v2_multifile/modelsv2.py | 19 - .../modelsv2b.py | 19 - .../test_multifile.py | 951 ----------- tests/test_pydantic_v1_v2_noneable.py | 692 -------- tests/test_read_with_orm_mode.py | 50 - ...est_response_model_as_return_annotation.py | 26 - .../test_tutorial007_pv1.py | 115 -- .../test_pydantic_v1_in_v2/__init__.py | 0 .../test_tutorial001.py | 31 - .../test_tutorial002.py | 143 -- .../test_tutorial003.py | 158 -- .../test_tutorial004.py | 156 -- .../test_tutorial002_pv1.py | 128 -- .../test_tutorial001_pv1.py | 152 -- .../test_tutorial/test_settings/test_app03.py | 12 - .../test_settings/test_tutorial001.py | 10 +- tests/utils.py | 2 - 55 files changed, 338 insertions(+), 8785 deletions(-) delete mode 100644 docs_src/request_form_models/tutorial002_pv1_an_py39.py delete mode 100644 docs_src/request_form_models/tutorial002_pv1_py39.py delete mode 100644 fastapi/_compat/main.py delete mode 100644 fastapi/_compat/may_v1.py delete mode 100644 fastapi/_compat/model_field.py delete mode 100644 fastapi/_compat/v1.py delete mode 100644 fastapi/temp_pydantic_v1_params.py delete mode 100644 tests/test_compat_params_v1.py delete mode 100644 tests/test_filter_pydantic_sub_model/__init__.py delete mode 100644 tests/test_filter_pydantic_sub_model/app_pv1.py delete mode 100644 tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py delete mode 100644 tests/test_pydantic_v1_deprecation_warnings.py create mode 100644 tests/test_pydantic_v1_error.py delete mode 100644 tests/test_pydantic_v1_v2_01.py delete mode 100644 tests/test_pydantic_v1_v2_list.py delete mode 100644 tests/test_pydantic_v1_v2_mixed.py delete mode 100644 tests/test_pydantic_v1_v2_multifile/__init__.py delete mode 100644 tests/test_pydantic_v1_v2_multifile/main.py delete mode 100644 tests/test_pydantic_v1_v2_multifile/modelsv1.py delete mode 100644 tests/test_pydantic_v1_v2_multifile/modelsv2.py delete mode 100644 tests/test_pydantic_v1_v2_multifile/modelsv2b.py delete mode 100644 tests/test_pydantic_v1_v2_multifile/test_multifile.py delete mode 100644 tests/test_pydantic_v1_v2_noneable.py delete mode 100644 tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py delete mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py delete mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py delete mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py delete mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py delete mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py delete mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py delete mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py diff --git a/docs_src/request_form_models/tutorial002_pv1_an_py39.py b/docs_src/request_form_models/tutorial002_pv1_an_py39.py deleted file mode 100644 index 392e6873c..000000000 --- a/docs_src/request_form_models/tutorial002_pv1_an_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI -from fastapi.temp_pydantic_v1_params import Form -from pydantic.v1 import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - class Config: - extra = "forbid" - - -@app.post("/login/") -async def login(data: Annotated[FormData, Form()]): - return data diff --git a/docs_src/request_form_models/tutorial002_pv1_py39.py b/docs_src/request_form_models/tutorial002_pv1_py39.py deleted file mode 100644 index da160b3a5..000000000 --- a/docs_src/request_form_models/tutorial002_pv1_py39.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import FastAPI -from fastapi.temp_pydantic_v1_params import Form -from pydantic.v1 import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - class Config: - extra = "forbid" - - -@app.post("/login/") -async def login(data: FormData = Form()): - return data diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py index fd1df8c6a..3dfaf9b71 100644 --- a/fastapi/_compat/__init__.py +++ b/fastapi/_compat/__init__.py @@ -1,43 +1,9 @@ -from .main import BaseConfig as BaseConfig -from .main import PydanticSchemaGenerationError as PydanticSchemaGenerationError -from .main import RequiredParam as RequiredParam -from .main import Undefined as Undefined -from .main import UndefinedType as UndefinedType -from .main import Url as Url -from .main import Validator as Validator -from .main import _get_model_config as _get_model_config -from .main import _is_error_wrapper as _is_error_wrapper -from .main import _is_model_class as _is_model_class -from .main import _is_model_field as _is_model_field -from .main import _is_undefined as _is_undefined -from .main import _model_dump as _model_dump -from .main import copy_field_info as copy_field_info -from .main import create_body_model as create_body_model -from .main import evaluate_forwardref as evaluate_forwardref -from .main import get_annotation_from_field_info as get_annotation_from_field_info -from .main import get_cached_model_fields as get_cached_model_fields -from .main import get_compat_model_name_map as get_compat_model_name_map -from .main import get_definitions as get_definitions -from .main import get_missing_field_error as get_missing_field_error -from .main import get_schema_from_model_field as get_schema_from_model_field -from .main import is_bytes_field as is_bytes_field -from .main import is_bytes_sequence_field as is_bytes_sequence_field -from .main import is_scalar_field as is_scalar_field -from .main import is_scalar_sequence_field as is_scalar_sequence_field -from .main import is_sequence_field as is_sequence_field -from .main import serialize_sequence_value as serialize_sequence_value -from .main import ( - with_info_plain_validator_function as with_info_plain_validator_function, -) -from .may_v1 import CoreSchema as CoreSchema -from .may_v1 import GetJsonSchemaHandler as GetJsonSchemaHandler -from .may_v1 import JsonSchemaValue as JsonSchemaValue -from .may_v1 import _normalize_errors as _normalize_errors -from .model_field import ModelField as ModelField from .shared import PYDANTIC_V2 as PYDANTIC_V2 from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1 from .shared import field_annotation_is_scalar as field_annotation_is_scalar +from .shared import is_pydantic_v1_model_class as is_pydantic_v1_model_class +from .shared import is_pydantic_v1_model_instance as is_pydantic_v1_model_instance from .shared import ( is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation, ) @@ -47,3 +13,29 @@ from .shared import ( from .shared import lenient_issubclass as lenient_issubclass from .shared import sequence_types as sequence_types from .shared import value_is_sequence as value_is_sequence +from .v2 import BaseConfig as BaseConfig +from .v2 import ModelField as ModelField +from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError +from .v2 import RequiredParam as RequiredParam +from .v2 import Undefined as Undefined +from .v2 import UndefinedType as UndefinedType +from .v2 import Url as Url +from .v2 import Validator as Validator +from .v2 import _regenerate_error_with_loc as _regenerate_error_with_loc +from .v2 import copy_field_info as copy_field_info +from .v2 import create_body_model as create_body_model +from .v2 import evaluate_forwardref as evaluate_forwardref +from .v2 import get_cached_model_fields as get_cached_model_fields +from .v2 import get_compat_model_name_map as get_compat_model_name_map +from .v2 import get_definitions as get_definitions +from .v2 import get_missing_field_error as get_missing_field_error +from .v2 import get_schema_from_model_field as get_schema_from_model_field +from .v2 import is_bytes_field as is_bytes_field +from .v2 import is_bytes_sequence_field as is_bytes_sequence_field +from .v2 import is_scalar_field as is_scalar_field +from .v2 import is_scalar_sequence_field as is_scalar_sequence_field +from .v2 import is_sequence_field as is_sequence_field +from .v2 import serialize_sequence_value as serialize_sequence_value +from .v2 import ( + with_info_plain_validator_function as with_info_plain_validator_function, +) diff --git a/fastapi/_compat/main.py b/fastapi/_compat/main.py deleted file mode 100644 index 95053a237..000000000 --- a/fastapi/_compat/main.py +++ /dev/null @@ -1,264 +0,0 @@ -import sys -from collections.abc import Sequence -from functools import lru_cache -from typing import ( - Any, -) - -from fastapi._compat import may_v1 -from fastapi._compat.shared import lenient_issubclass -from fastapi.types import ModelNameMap -from pydantic import BaseModel -from typing_extensions import Literal - -from . import v2 -from .model_field import ModelField -from .v2 import BaseConfig as BaseConfig -from .v2 import FieldInfo as FieldInfo -from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError -from .v2 import RequiredParam as RequiredParam -from .v2 import Undefined as Undefined -from .v2 import UndefinedType as UndefinedType -from .v2 import Url as Url -from .v2 import Validator as Validator -from .v2 import evaluate_forwardref as evaluate_forwardref -from .v2 import get_missing_field_error as get_missing_field_error -from .v2 import ( - with_info_plain_validator_function as with_info_plain_validator_function, -) - - -@lru_cache -def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]: - if lenient_issubclass(model, may_v1.BaseModel): - from fastapi._compat import v1 - - return v1.get_model_fields(model) # type: ignore[arg-type,return-value] - else: - from . import v2 - - return v2.get_model_fields(model) # type: ignore[return-value] - - -def _is_undefined(value: object) -> bool: - if isinstance(value, may_v1.UndefinedType): - return True - - return isinstance(value, v2.UndefinedType) - - -def _get_model_config(model: BaseModel) -> Any: - if isinstance(model, may_v1.BaseModel): - from fastapi._compat import v1 - - return v1._get_model_config(model) - - return v2._get_model_config(model) - - -def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any -) -> Any: - if isinstance(model, may_v1.BaseModel): - from fastapi._compat import v1 - - return v1._model_dump(model, mode=mode, **kwargs) - - return v2._model_dump(model, mode=mode, **kwargs) - - -def _is_error_wrapper(exc: Exception) -> bool: - if isinstance(exc, may_v1.ErrorWrapper): - return True - - return isinstance(exc, v2.ErrorWrapper) - - -def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - if isinstance(field_info, may_v1.FieldInfo): - from fastapi._compat import v1 - - return v1.copy_field_info(field_info=field_info, annotation=annotation) - - return v2.copy_field_info(field_info=field_info, annotation=annotation) - - -def create_body_model( - *, fields: Sequence[ModelField], model_name: str -) -> type[BaseModel]: - if fields and isinstance(fields[0], may_v1.ModelField): - from fastapi._compat import v1 - - return v1.create_body_model(fields=fields, model_name=model_name) - - return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type] - - -def get_annotation_from_field_info( - annotation: Any, field_info: FieldInfo, field_name: str -) -> Any: - if isinstance(field_info, may_v1.FieldInfo): - from fastapi._compat import v1 - - return v1.get_annotation_from_field_info( - annotation=annotation, field_info=field_info, field_name=field_name - ) - - return v2.get_annotation_from_field_info( - annotation=annotation, field_info=field_info, field_name=field_name - ) - - -def is_bytes_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_bytes_field(field) - - return v2.is_bytes_field(field) # type: ignore[arg-type] - - -def is_bytes_sequence_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_bytes_sequence_field(field) - - return v2.is_bytes_sequence_field(field) # type: ignore[arg-type] - - -def is_scalar_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_scalar_field(field) - - return v2.is_scalar_field(field) # type: ignore[arg-type] - - -def is_scalar_sequence_field(field: ModelField) -> bool: - return v2.is_scalar_sequence_field(field) # type: ignore[arg-type] - - -def is_sequence_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_sequence_field(field) - - return v2.is_sequence_field(field) # type: ignore[arg-type] - - -def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.serialize_sequence_value(field=field, value=value) - - return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type] - - -def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: - v1_model_fields = [ - field for field in fields if isinstance(field, may_v1.ModelField) - ] - if v1_model_fields: - from fastapi._compat import v1 - - v1_flat_models = v1.get_flat_models_from_fields( - v1_model_fields, # type: ignore[arg-type] - known_models=set(), - ) - all_flat_models = v1_flat_models - else: - all_flat_models = set() - - v2_model_fields = [field for field in fields if isinstance(field, v2.ModelField)] - v2_flat_models = v2.get_flat_models_from_fields(v2_model_fields, known_models=set()) - all_flat_models = all_flat_models.union(v2_flat_models) # type: ignore[arg-type] - - model_name_map = v2.get_model_name_map(all_flat_models) # type: ignore[arg-type] - return model_name_map - - -def get_definitions( - *, - fields: list[ModelField], - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, -) -> tuple[ - dict[ - tuple[ModelField, Literal["validation", "serialization"]], - may_v1.JsonSchemaValue, - ], - dict[str, dict[str, Any]], -]: - if sys.version_info < (3, 14): - v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)] - v1_field_maps, v1_definitions = may_v1.get_definitions( - fields=v1_fields, # type: ignore[arg-type] - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - - v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] - v2_field_maps, v2_definitions = v2.get_definitions( - fields=v2_fields, - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - all_definitions = {**v1_definitions, **v2_definitions} - all_field_maps = {**v1_field_maps, **v2_field_maps} # type: ignore[misc] - return all_field_maps, all_definitions - - # Pydantic v1 is not supported since Python 3.14 - else: - v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] - v2_field_maps, v2_definitions = v2.get_definitions( - fields=v2_fields, - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - return v2_field_maps, v2_definitions - - -def get_schema_from_model_field( - *, - field: ModelField, - model_name_map: ModelNameMap, - field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], - may_v1.JsonSchemaValue, - ], - separate_input_output_schemas: bool = True, -) -> dict[str, Any]: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.get_schema_from_model_field( - field=field, - model_name_map=model_name_map, - field_mapping=field_mapping, - separate_input_output_schemas=separate_input_output_schemas, - ) - - return v2.get_schema_from_model_field( - field=field, # type: ignore[arg-type] - model_name_map=model_name_map, - field_mapping=field_mapping, # type: ignore[arg-type] - separate_input_output_schemas=separate_input_output_schemas, - ) - - -def _is_model_field(value: Any) -> bool: - if isinstance(value, may_v1.ModelField): - return True - - return isinstance(value, v2.ModelField) - - -def _is_model_class(value: Any) -> bool: - if lenient_issubclass(value, may_v1.BaseModel): - return True - - return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined] diff --git a/fastapi/_compat/may_v1.py b/fastapi/_compat/may_v1.py deleted file mode 100644 index 3ac86aa98..000000000 --- a/fastapi/_compat/may_v1.py +++ /dev/null @@ -1,124 +0,0 @@ -import sys -from collections.abc import Sequence -from typing import Any, Literal, Union - -from fastapi.types import ModelNameMap - -if sys.version_info >= (3, 14): - - class AnyUrl: - pass - - class BaseConfig: - pass - - class BaseModel: - pass - - class Color: - pass - - class CoreSchema: - pass - - class ErrorWrapper: - pass - - class FieldInfo: - pass - - class GetJsonSchemaHandler: - pass - - class JsonSchemaValue: - pass - - class ModelField: - pass - - class NameEmail: - pass - - class RequiredParam: - pass - - class SecretBytes: - pass - - class SecretStr: - pass - - class Undefined: - pass - - class UndefinedType: - pass - - class Url: - pass - - from .v2 import ValidationError, create_model - - def get_definitions( - *, - fields: list[ModelField], - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, - ) -> tuple[ - dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - dict[str, dict[str, Any]], - ]: - return {}, {} # pragma: no cover - - -else: - from .v1 import AnyUrl as AnyUrl - from .v1 import BaseConfig as BaseConfig - from .v1 import BaseModel as BaseModel - from .v1 import Color as Color - from .v1 import CoreSchema as CoreSchema - from .v1 import ErrorWrapper as ErrorWrapper - from .v1 import FieldInfo as FieldInfo - from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler - from .v1 import JsonSchemaValue as JsonSchemaValue - from .v1 import ModelField as ModelField - from .v1 import NameEmail as NameEmail - from .v1 import RequiredParam as RequiredParam - from .v1 import SecretBytes as SecretBytes - from .v1 import SecretStr as SecretStr - from .v1 import Undefined as Undefined - from .v1 import UndefinedType as UndefinedType - from .v1 import Url as Url - from .v1 import ValidationError, create_model - from .v1 import get_definitions as get_definitions - - -RequestErrorModel: type[BaseModel] = create_model("Request") - - -def _normalize_errors(errors: Sequence[Any]) -> list[dict[str, Any]]: - use_errors: list[Any] = [] - for error in errors: - if isinstance(error, ErrorWrapper): - new_errors = ValidationError( - errors=[error], model=RequestErrorModel - ).errors() - use_errors.extend(new_errors) - elif isinstance(error, list): - use_errors.extend(_normalize_errors(error)) - else: - use_errors.append(error) - return use_errors - - -def _regenerate_error_with_loc( - *, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...] -) -> list[dict[str, Any]]: - updated_loc_errors: list[Any] = [ - {**err, "loc": loc_prefix + err.get("loc", ())} - for err in _normalize_errors(errors) - ] - - return updated_loc_errors diff --git a/fastapi/_compat/model_field.py b/fastapi/_compat/model_field.py deleted file mode 100644 index 47d05cb94..000000000 --- a/fastapi/_compat/model_field.py +++ /dev/null @@ -1,50 +0,0 @@ -from typing import ( - Any, - Union, -) - -from fastapi.types import IncEx -from pydantic.fields import FieldInfo -from typing_extensions import Literal, Protocol - - -class ModelField(Protocol): - field_info: "FieldInfo" - name: str - mode: Literal["validation", "serialization"] = "validation" - _version: Literal["v1", "v2"] = "v1" - - @property - def alias(self) -> str: ... - - @property - def required(self) -> bool: ... - - @property - def default(self) -> Any: ... - - @property - def type_(self) -> Any: ... - - def get_default(self) -> Any: ... - - def validate( - self, - value: Any, - values: dict[str, Any] = {}, # noqa: B006 - *, - loc: tuple[Union[int, str], ...] = (), - ) -> tuple[Any, Union[list[dict[str, Any]], None]]: ... - - def serialize( - self, - value: Any, - *, - mode: Literal["json", "python"] = "json", - include: Union[IncEx, None] = None, - exclude: Union[IncEx, None] = None, - by_alias: bool = True, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - ) -> Any: ... diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py index 3a11e88ac..419b58f7f 100644 --- a/fastapi/_compat/shared.py +++ b/fastapi/_compat/shared.py @@ -1,6 +1,7 @@ import sys import types import typing +import warnings from collections import deque from collections.abc import Mapping, Sequence from dataclasses import is_dataclass @@ -10,7 +11,6 @@ from typing import ( Union, ) -from fastapi._compat import may_v1 from fastapi.types import UnionType from pydantic import BaseModel from pydantic.version import VERSION as PYDANTIC_VERSION @@ -81,9 +81,7 @@ def value_is_sequence(value: Any) -> bool: def _annotation_is_complex(annotation: Union[type[Any], None]) -> bool: return ( - lenient_issubclass( - annotation, (BaseModel, may_v1.BaseModel, Mapping, UploadFile) - ) + lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile)) or _annotation_is_sequence(annotation) or is_dataclass(annotation) ) @@ -179,13 +177,27 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool: ) +def is_pydantic_v1_model_instance(obj: Any) -> bool: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 + return isinstance(obj, v1.BaseModel) + + +def is_pydantic_v1_model_class(cls: Any) -> bool: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 + return lenient_issubclass(cls, v1.BaseModel) + + def annotation_is_pydantic_v1(annotation: Any) -> bool: - if lenient_issubclass(annotation, may_v1.BaseModel): + if is_pydantic_v1_model_class(annotation): return True origin = get_origin(annotation) if origin is Union or origin is UnionType: for arg in get_args(annotation): - if lenient_issubclass(arg, may_v1.BaseModel): + if is_pydantic_v1_model_class(arg): return True if field_annotation_is_sequence(annotation): for sub_annotation in get_args(annotation): diff --git a/fastapi/_compat/v1.py b/fastapi/_compat/v1.py deleted file mode 100644 index b0a9dd35f..000000000 --- a/fastapi/_compat/v1.py +++ /dev/null @@ -1,222 +0,0 @@ -from collections.abc import Sequence -from copy import copy -from dataclasses import dataclass, is_dataclass -from enum import Enum -from typing import ( - Any, - Callable, - Union, -) - -from fastapi._compat import shared -from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX -from fastapi.types import ModelNameMap -from pydantic.v1 import BaseConfig as BaseConfig -from pydantic.v1 import BaseModel as BaseModel -from pydantic.v1 import ValidationError as ValidationError -from pydantic.v1 import create_model as create_model -from pydantic.v1.class_validators import Validator as Validator -from pydantic.v1.color import Color as Color -from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper -from pydantic.v1.fields import ( - SHAPE_FROZENSET, - SHAPE_LIST, - SHAPE_SEQUENCE, - SHAPE_SET, - SHAPE_SINGLETON, - SHAPE_TUPLE, - SHAPE_TUPLE_ELLIPSIS, -) -from pydantic.v1.fields import FieldInfo as FieldInfo -from pydantic.v1.fields import ModelField as ModelField -from pydantic.v1.fields import Undefined as Undefined -from pydantic.v1.fields import UndefinedType as UndefinedType -from pydantic.v1.networks import AnyUrl as AnyUrl -from pydantic.v1.networks import NameEmail as NameEmail -from pydantic.v1.schema import TypeModelSet as TypeModelSet -from pydantic.v1.schema import field_schema, model_process_schema -from pydantic.v1.schema import ( - get_annotation_from_field_info as get_annotation_from_field_info, -) -from pydantic.v1.schema import ( - get_flat_models_from_field as get_flat_models_from_field, -) -from pydantic.v1.schema import ( - get_flat_models_from_fields as get_flat_models_from_fields, -) -from pydantic.v1.schema import get_model_name_map as get_model_name_map -from pydantic.v1.types import SecretBytes as SecretBytes -from pydantic.v1.types import SecretStr as SecretStr -from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref -from pydantic.v1.utils import lenient_issubclass as lenient_issubclass -from pydantic.version import VERSION as PYDANTIC_VERSION -from typing_extensions import Literal - -PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) -PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 -# Keeping old "Required" functionality from Pydantic V1, without -# shadowing typing.Required. -RequiredParam: Any = Ellipsis - - -GetJsonSchemaHandler = Any -JsonSchemaValue = dict[str, Any] -CoreSchema = Any -Url = AnyUrl - -sequence_shapes = { - SHAPE_LIST, - SHAPE_SET, - SHAPE_FROZENSET, - SHAPE_TUPLE, - SHAPE_SEQUENCE, - SHAPE_TUPLE_ELLIPSIS, -} -sequence_shape_to_type = { - SHAPE_LIST: list, - SHAPE_SET: set, - SHAPE_TUPLE: tuple, - SHAPE_SEQUENCE: list, - SHAPE_TUPLE_ELLIPSIS: list, -} - - -@dataclass -class GenerateJsonSchema: - ref_template: str - - -class PydanticSchemaGenerationError(Exception): - pass - - -RequestErrorModel: type[BaseModel] = create_model("Request") - - -def with_info_plain_validator_function( - function: Callable[..., Any], - *, - ref: Union[str, None] = None, - metadata: Any = None, - serialization: Any = None, -) -> Any: - return {} - - -def get_model_definitions( - *, - flat_models: set[Union[type[BaseModel], type[Enum]]], - model_name_map: dict[Union[type[BaseModel], type[Enum]], str], -) -> dict[str, Any]: - definitions: dict[str, dict[str, Any]] = {} - for model in flat_models: - m_schema, m_definitions, m_nested_models = model_process_schema( - model, model_name_map=model_name_map, ref_prefix=REF_PREFIX - ) - definitions.update(m_definitions) - model_name = model_name_map[model] - definitions[model_name] = m_schema - for m_schema in definitions.values(): - if "description" in m_schema: - m_schema["description"] = m_schema["description"].split("\f")[0] - return definitions - - -def is_pv1_scalar_field(field: ModelField) -> bool: - from fastapi import params - - field_info = field.field_info - if not ( - field.shape == SHAPE_SINGLETON - and not lenient_issubclass(field.type_, BaseModel) - and not lenient_issubclass(field.type_, dict) - and not shared.field_annotation_is_sequence(field.type_) - and not is_dataclass(field.type_) - and not isinstance(field_info, params.Body) - ): - return False - if field.sub_fields: - if not all(is_pv1_scalar_field(f) for f in field.sub_fields): - return False - return True - - -def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any -) -> Any: - return model.dict(**kwargs) - - -def _get_model_config(model: BaseModel) -> Any: - return model.__config__ - - -def get_schema_from_model_field( - *, - field: ModelField, - model_name_map: ModelNameMap, - field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - separate_input_output_schemas: bool = True, -) -> dict[str, Any]: - return field_schema( - field, - model_name_map=model_name_map, # type: ignore[arg-type] - ref_prefix=REF_PREFIX, - )[0] - - -# def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: -# models = get_flat_models_from_fields(fields, known_models=set()) -# return get_model_name_map(models) # type: ignore[no-any-return] - - -def get_definitions( - *, - fields: list[ModelField], - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, -) -> tuple[ - dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], - dict[str, dict[str, Any]], -]: - models = get_flat_models_from_fields(fields, known_models=set()) - return {}, get_model_definitions(flat_models=models, model_name_map=model_name_map) # type: ignore[arg-type] - - -def is_scalar_field(field: ModelField) -> bool: - return is_pv1_scalar_field(field) - - -def is_sequence_field(field: ModelField) -> bool: - return field.shape in sequence_shapes or shared._annotation_is_sequence(field.type_) - - -def is_bytes_field(field: ModelField) -> bool: - return lenient_issubclass(field.type_, bytes) - - -def is_bytes_sequence_field(field: ModelField) -> bool: - return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) - - -def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - return copy(field_info) - - -def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return] - - -def create_body_model( - *, fields: Sequence[ModelField], model_name: str -) -> type[BaseModel]: - BodyModel = create_model(model_name) - for f in fields: - BodyModel.__fields__[f.name] = f - return BodyModel - - -def get_model_fields(model: type[BaseModel]) -> list[ModelField]: - return list(model.__fields__.values()) diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index cbcb98e1a..25b681453 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -4,6 +4,7 @@ from collections.abc import Sequence from copy import copy, deepcopy from dataclasses import dataclass, is_dataclass from enum import Enum +from functools import lru_cache from typing import ( Annotated, Any, @@ -11,7 +12,7 @@ from typing import ( cast, ) -from fastapi._compat import may_v1, shared +from fastapi._compat import shared from fastapi.openapi.constants import REF_TEMPLATE from fastapi.types import IncEx, ModelNameMap, UnionType from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model @@ -175,7 +176,7 @@ class ModelField: None, ) except ValidationError as exc: - return None, may_v1._regenerate_error_with_loc( + return None, _regenerate_error_with_loc( errors=exc.errors(include_url=False), loc_prefix=loc ) @@ -210,22 +211,6 @@ class ModelField: return id(self) -def get_annotation_from_field_info( - annotation: Any, field_info: FieldInfo, field_name: str -) -> Any: - return annotation - - -def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any -) -> Any: - return model.model_dump(mode=mode, **kwargs) - - -def _get_model_config(model: BaseModel) -> Any: - return model.model_config - - def _has_computed_fields(field: ModelField) -> bool: computed_fields = field._type_adapter.core_schema.get("schema", {}).get( "computed_fields", [] @@ -490,6 +475,11 @@ def get_model_fields(model: type[BaseModel]) -> list[ModelField]: return model_fields +@lru_cache +def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]: + return get_model_fields(model) # type: ignore[return-value] + + # Duplicate of several schema functions from Pydantic v1 to make them compatible with # Pydantic v2 and allow mixing the models @@ -503,22 +493,23 @@ def normalize_name(name: str) -> str: def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str]: name_model_map = {} - conflicting_names: set[str] = set() for model in unique_models: model_name = normalize_name(model.__name__) - if model_name in conflicting_names: - model_name = get_long_model_name(model) - name_model_map[model_name] = model - elif model_name in name_model_map: - conflicting_names.add(model_name) - conflicting_model = name_model_map.pop(model_name) - name_model_map[get_long_model_name(conflicting_model)] = conflicting_model - name_model_map[get_long_model_name(model)] = model - else: - name_model_map[model_name] = model + name_model_map[model_name] = model return {v: k for k, v in name_model_map.items()} +def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: + all_flat_models = set() + + v2_model_fields = [field for field in fields if isinstance(field, ModelField)] + v2_flat_models = get_flat_models_from_fields(v2_model_fields, known_models=set()) + all_flat_models = all_flat_models.union(v2_flat_models) # type: ignore[arg-type] + + model_name_map = get_model_name_map(all_flat_models) # type: ignore[arg-type] + return model_name_map + + def get_flat_models_from_model( model: type["BaseModel"], known_models: Union[TypeModelSet, None] = None ) -> TypeModelSet: @@ -567,5 +558,11 @@ def get_flat_models_from_fields( return known_models -def get_long_model_name(model: TypeModelOrEnum) -> str: - return f"{model.__module__}__{model.__qualname__}".replace(".", "__") +def _regenerate_error_with_loc( + *, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...] +) -> list[dict[str, Any]]: + updated_loc_errors: list[Any] = [ + {**err, "loc": loc_prefix + err.get("loc", ())} for err in errors + ] + + return updated_loc_errors diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index 492cbfccc..2bf5fdb26 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -1,3 +1,4 @@ +from collections.abc import Mapping from typing import ( Annotated, Any, @@ -9,11 +10,7 @@ from typing import ( ) from annotated_doc import Doc -from fastapi._compat import ( - CoreSchema, - GetJsonSchemaHandler, - JsonSchemaValue, -) +from pydantic import GetJsonSchemaHandler from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401 from starlette.datastructures import FormData as FormData # noqa: F401 @@ -142,14 +139,14 @@ class UploadFile(StarletteUploadFile): @classmethod def __get_pydantic_json_schema__( - cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler - ) -> JsonSchemaValue: + cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler + ) -> dict[str, Any]: return {"type": "string", "format": "binary"} @classmethod def __get_pydantic_core_schema__( - cls, source: type[Any], handler: Callable[[Any], CoreSchema] - ) -> CoreSchema: + cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]] + ) -> Mapping[str, Any]: from ._compat.v2 import with_info_plain_validator_function return with_info_plain_validator_function(cls._validate) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index af2bed9ad..45e1ff3ed 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,7 +1,6 @@ import dataclasses import inspect import sys -import warnings from collections.abc import Coroutine, Mapping, Sequence from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy @@ -22,13 +21,11 @@ from fastapi._compat import ( ModelField, RequiredParam, Undefined, - _is_error_wrapper, - _is_model_class, + _regenerate_error_with_loc, copy_field_info, create_body_model, evaluate_forwardref, field_annotation_is_scalar, - get_annotation_from_field_info, get_cached_model_fields, get_missing_field_error, is_bytes_field, @@ -39,19 +36,17 @@ from fastapi._compat import ( is_uploadfile_or_nonable_uploadfile_annotation, is_uploadfile_sequence_annotation, lenient_issubclass, - may_v1, sequence_types, serialize_sequence_value, value_is_sequence, ) -from fastapi._compat.shared import annotation_is_pydantic_v1 from fastapi.background import BackgroundTasks from fastapi.concurrency import ( asynccontextmanager, contextmanager_in_threadpool, ) from fastapi.dependencies.models import Dependant -from fastapi.exceptions import DependencyScopeError, FastAPIDeprecationWarning +from fastapi.exceptions import DependencyScopeError from fastapi.logger import logger from fastapi.security.oauth2 import SecurityScopes from fastapi.types import DependencyCacheKey @@ -72,8 +67,6 @@ from starlette.responses import Response from starlette.websockets import WebSocket from typing_extensions import Literal, get_args, get_origin -from .. import temp_pydantic_v1_params - multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' 'You can install "python-multipart" with: \n\n' @@ -189,7 +182,7 @@ def _get_flat_fields_from_params(fields: list[ModelField]) -> list[ModelField]: if not fields: return fields first_field = fields[0] - if len(fields) == 1 and _is_model_class(first_field.type_): + if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel): fields_to_extract = get_cached_model_fields(first_field.type_) return fields_to_extract return fields @@ -323,16 +316,7 @@ def get_dependant( ) continue assert param_details.field is not None - if isinstance(param_details.field, may_v1.ModelField): - warnings.warn( - "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." - f" Please update the param {param_name}: {param_details.type_annotation!r}.", - category=FastAPIDeprecationWarning, - stacklevel=5, - ) - if isinstance( - param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body) - ): + if isinstance(param_details.field.field_info, params.Body): dependant.body_params.append(param_details.field) else: add_param_to_fields(field=param_details.field, dependant=dependant) @@ -391,7 +375,7 @@ def analyze_param( fastapi_annotations = [ arg for arg in annotated_args[1:] - if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends)) + if isinstance(arg, (FieldInfo, params.Depends)) ] fastapi_specific_annotations = [ arg @@ -400,30 +384,27 @@ def analyze_param( arg, ( params.Param, - temp_pydantic_v1_params.Param, params.Body, - temp_pydantic_v1_params.Body, params.Depends, ), ) ] if fastapi_specific_annotations: - fastapi_annotation: Union[ - FieldInfo, may_v1.FieldInfo, params.Depends, None - ] = fastapi_specific_annotations[-1] + fastapi_annotation: Union[FieldInfo, params.Depends, None] = ( + fastapi_specific_annotations[-1] + ) else: fastapi_annotation = None # Set default for Annotated FieldInfo - if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)): + if isinstance(fastapi_annotation, FieldInfo): # Copy `field_info` because we mutate `field_info.default` below. field_info = copy_field_info( field_info=fastapi_annotation, # type: ignore[arg-type] annotation=use_annotation, ) - assert field_info.default in { - Undefined, - may_v1.Undefined, - } or field_info.default in {RequiredParam, may_v1.RequiredParam}, ( + assert ( + field_info.default == Undefined or field_info.default == RequiredParam + ), ( f"`{field_info.__class__.__name__}` default value cannot be set in" f" `Annotated` for {param_name!r}. Set the default value with `=` instead." ) @@ -447,7 +428,7 @@ def analyze_param( ) depends = value # Get FieldInfo from default value - elif isinstance(value, (FieldInfo, may_v1.FieldInfo)): + elif isinstance(value, FieldInfo): assert field_info is None, ( "Cannot specify FastAPI annotations in `Annotated` and default value" f" together for {param_name!r}" @@ -491,14 +472,7 @@ def analyze_param( ) or is_uploadfile_sequence_annotation(type_annotation): field_info = params.File(annotation=use_annotation, default=default_value) elif not field_annotation_is_scalar(annotation=type_annotation): - if annotation_is_pydantic_v1(use_annotation): - field_info = temp_pydantic_v1_params.Body( # type: ignore[assignment] - annotation=use_annotation, default=default_value - ) - else: - field_info = params.Body( - annotation=use_annotation, default=default_value - ) + field_info = params.Body(annotation=use_annotation, default=default_value) else: field_info = params.Query(annotation=use_annotation, default=default_value) @@ -507,23 +481,17 @@ def analyze_param( if field_info is not None: # Handle field_info.in_ if is_path_param: - assert isinstance( - field_info, (params.Path, temp_pydantic_v1_params.Path) - ), ( + assert isinstance(field_info, params.Path), ( f"Cannot use `{field_info.__class__.__name__}` for path param" f" {param_name!r}" ) elif ( - isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)) + isinstance(field_info, params.Param) and getattr(field_info, "in_", None) is None ): field_info.in_ = params.ParamTypes.query - use_annotation_from_field_info = get_annotation_from_field_info( - use_annotation, - field_info, - param_name, - ) - if isinstance(field_info, (params.Form, temp_pydantic_v1_params.Form)): + use_annotation_from_field_info = use_annotation + if isinstance(field_info, params.Form): ensure_multipart_is_installed() if not field_info.alias and getattr(field_info, "convert_underscores", None): alias = param_name.replace("_", "-") @@ -535,20 +503,19 @@ def analyze_param( type_=use_annotation_from_field_info, default=field_info.default, alias=alias, - required=field_info.default - in (RequiredParam, may_v1.RequiredParam, Undefined), + required=field_info.default in (RequiredParam, Undefined), field_info=field_info, ) if is_path_param: assert is_scalar_field(field=field), ( "Path params must be of one of the supported types" ) - elif isinstance(field_info, (params.Query, temp_pydantic_v1_params.Query)): + elif isinstance(field_info, params.Query): assert ( is_scalar_field(field) or is_scalar_sequence_field(field) or ( - _is_model_class(field.type_) + lenient_issubclass(field.type_, BaseModel) # For Pydantic v1 and getattr(field, "shape", 1) == 1 ) @@ -742,10 +709,8 @@ def _validate_value_with_model_field( else: return deepcopy(field.default), [] v_, errors_ = field.validate(value, values, loc=loc) - if _is_error_wrapper(errors_): # type: ignore[arg-type] - return None, [errors_] - elif isinstance(errors_, list): - new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=()) + if isinstance(errors_, list): + new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) return None, new_errors else: return v_, [] @@ -762,7 +727,7 @@ def _get_multidict_value( if ( value is None or ( - isinstance(field.field_info, (params.Form, temp_pydantic_v1_params.Form)) + isinstance(field.field_info, params.Form) and isinstance(value, str) # For type checks and value == "" ) @@ -832,7 +797,7 @@ def request_params_to_args( if single_not_embedded_field: field_info = first_field.field_info - assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( + assert isinstance(field_info, params.Param), ( "Params must be subclasses of Param" ) loc: tuple[str, ...] = (field_info.in_.value,) @@ -844,7 +809,7 @@ def request_params_to_args( for field in fields: value = _get_multidict_value(field, received_params) field_info = field.field_info - assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( + assert isinstance(field_info, params.Param), ( "Params must be subclasses of Param" ) loc = (field_info.in_.value, get_validation_alias(field)) @@ -871,7 +836,7 @@ def is_union_of_base_models(field_type: Any) -> bool: union_args = get_args(field_type) for arg in union_args: - if not _is_model_class(arg): + if not lenient_issubclass(arg, BaseModel): return False return True @@ -893,8 +858,8 @@ def _should_embed_body_fields(fields: list[ModelField]) -> bool: # If it's a Form (or File) field, it has to be a BaseModel (or a union of BaseModels) to be top level # otherwise it has to be embedded, so that the key value pair can be extracted if ( - isinstance(first_field.field_info, (params.Form, temp_pydantic_v1_params.Form)) - and not _is_model_class(first_field.type_) + isinstance(first_field.field_info, params.Form) + and not lenient_issubclass(first_field.type_, BaseModel) and not is_union_of_base_models(first_field.type_) ): return True @@ -911,14 +876,14 @@ async def _extract_form_body( value = _get_multidict_value(field, received_body) field_info = field.field_info if ( - isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) + isinstance(field_info, params.File) and is_bytes_field(field) and isinstance(value, UploadFile) ): value = await value.read() elif ( is_bytes_sequence_field(field) - and isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) + and isinstance(field_info, params.File) and value_is_sequence(value) ): # For types @@ -964,7 +929,7 @@ async def request_body_to_args( if ( single_not_embedded_field - and _is_model_class(first_field.type_) + and lenient_issubclass(first_field.type_, BaseModel) and isinstance(received_body, FormData) ): fields_to_extract = get_cached_model_fields(first_field.type_) @@ -1029,28 +994,15 @@ def get_body_field( BodyFieldInfo_kwargs["default"] = None if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params): BodyFieldInfo: type[params.Body] = params.File - elif any( - isinstance(f.field_info, temp_pydantic_v1_params.File) - for f in flat_dependant.body_params - ): - BodyFieldInfo: type[temp_pydantic_v1_params.Body] = temp_pydantic_v1_params.File # type: ignore[no-redef] elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params): BodyFieldInfo = params.Form - elif any( - isinstance(f.field_info, temp_pydantic_v1_params.Form) - for f in flat_dependant.body_params - ): - BodyFieldInfo = temp_pydantic_v1_params.Form # type: ignore[assignment] else: - if annotation_is_pydantic_v1(BodyModel): - BodyFieldInfo = temp_pydantic_v1_params.Body # type: ignore[assignment] - else: - BodyFieldInfo = params.Body + BodyFieldInfo = params.Body body_param_media_types = [ f.field_info.media_type for f in flat_dependant.body_params - if isinstance(f.field_info, (params.Body, temp_pydantic_v1_params.Body)) + if isinstance(f.field_info, params.Body) ] if len(set(body_param_media_types)) == 1: BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0] diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 549da3279..e8610c983 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -18,14 +18,18 @@ from typing import Annotated, Any, Callable, Optional, Union from uuid import UUID from annotated_doc import Doc -from fastapi._compat import may_v1 +from fastapi.exceptions import PydanticV1NotSupportedError from fastapi.types import IncEx from pydantic import BaseModel from pydantic.color import Color from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr +from pydantic_core import PydanticUndefinedType -from ._compat import Url, _is_undefined, _model_dump +from ._compat import ( + Url, + is_pydantic_v1_model_instance, +) # Taken from Pydantic v1 as is @@ -63,7 +67,6 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]: ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = { bytes: lambda o: o.decode(), Color: str, - may_v1.Color: str, datetime.date: isoformat, datetime.datetime: isoformat, datetime.time: isoformat, @@ -80,19 +83,14 @@ ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = { IPv6Interface: str, IPv6Network: str, NameEmail: str, - may_v1.NameEmail: str, Path: str, Pattern: lambda o: o.pattern, SecretBytes: str, - may_v1.SecretBytes: str, SecretStr: str, - may_v1.SecretStr: str, set: list, UUID: str, Url: str, - may_v1.Url: str, AnyUrl: str, - may_v1.AnyUrl: str, } @@ -224,15 +222,8 @@ def jsonable_encoder( include = set(include) if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) - if isinstance(obj, (BaseModel, may_v1.BaseModel)): - # TODO: remove when deprecating Pydantic v1 - encoders: dict[Any, Any] = {} - if isinstance(obj, may_v1.BaseModel): - encoders = getattr(obj.__config__, "json_encoders", {}) - if custom_encoder: - encoders = {**encoders, **custom_encoder} - obj_dict = _model_dump( - obj, # type: ignore[arg-type] + if isinstance(obj, BaseModel): + obj_dict = obj.model_dump( mode="json", include=include, exclude=exclude, @@ -241,14 +232,10 @@ def jsonable_encoder( exclude_none=exclude_none, exclude_defaults=exclude_defaults, ) - if "__root__" in obj_dict: - obj_dict = obj_dict["__root__"] return jsonable_encoder( obj_dict, exclude_none=exclude_none, exclude_defaults=exclude_defaults, - # TODO: remove when deprecating Pydantic v1 - custom_encoder=encoders, sqlalchemy_safe=sqlalchemy_safe, ) if dataclasses.is_dataclass(obj): @@ -271,7 +258,7 @@ def jsonable_encoder( return str(obj) if isinstance(obj, (str, int, float, type(None))): return obj - if _is_undefined(obj): + if isinstance(obj, PydanticUndefinedType): return None if isinstance(obj, dict): encoded_dict = {} @@ -331,7 +318,11 @@ def jsonable_encoder( for encoder, classes_tuple in encoders_by_class_tuples.items(): if isinstance(obj, classes_tuple): return encoder(obj) - + if is_pydantic_v1_model_instance(obj): + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" Please update the model {obj!r}." + ) try: data = dict(obj) except Exception as e: diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 53e505281..1a3abd80c 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -233,6 +233,12 @@ class ResponseValidationError(ValidationException): self.body = body +class PydanticV1NotSupportedError(FastAPIError): + """ + A pydantic.v1 model is used, which is no longer supported. + """ + + class FastAPIDeprecationWarning(UserWarning): """ A custom deprecation warning as DeprecationWarning is ignored diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 680f67832..ac6a6d52c 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -1,15 +1,15 @@ -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from enum import Enum from typing import Annotated, Any, Callable, Optional, Union -from fastapi._compat import ( - CoreSchema, - GetJsonSchemaHandler, - JsonSchemaValue, - with_info_plain_validator_function, -) +from fastapi._compat import with_info_plain_validator_function from fastapi.logger import logger -from pydantic import AnyUrl, BaseModel, Field +from pydantic import ( + AnyUrl, + BaseModel, + Field, + GetJsonSchemaHandler, +) from typing_extensions import Literal, TypedDict from typing_extensions import deprecated as typing_deprecated @@ -43,14 +43,14 @@ except ImportError: # pragma: no cover @classmethod def __get_pydantic_json_schema__( - cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler - ) -> JsonSchemaValue: + cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler + ) -> dict[str, Any]: return {"type": "string", "format": "email"} @classmethod def __get_pydantic_core_schema__( - cls, source: type[Any], handler: Callable[[Any], CoreSchema] - ) -> CoreSchema: + cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]] + ) -> Mapping[str, Any]: return with_info_plain_validator_function(cls._validate) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 6180fcde6..75ff26102 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -6,7 +6,6 @@ from typing import Any, Optional, Union, cast from fastapi import routing from fastapi._compat import ( - JsonSchemaValue, ModelField, Undefined, get_compat_model_name_map, @@ -39,8 +38,6 @@ from starlette.responses import JSONResponse from starlette.routing import BaseRoute from typing_extensions import Literal -from .._compat import _is_model_field - validation_error_definition = { "title": "ValidationError", "type": "object", @@ -109,7 +106,7 @@ def _get_openapi_operation_parameters( dependant: Dependant, model_name_map: ModelNameMap, field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, ) -> list[dict[str, Any]]: @@ -182,13 +179,13 @@ def get_openapi_operation_request_body( body_field: Optional[ModelField], model_name_map: ModelNameMap, field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, ) -> Optional[dict[str, Any]]: if not body_field: return None - assert _is_model_field(body_field) + assert isinstance(body_field, ModelField) body_schema = get_schema_from_model_field( field=body_field, model_name_map=model_name_map, @@ -265,7 +262,7 @@ def get_openapi_path( operation_ids: set[str], model_name_map: ModelNameMap, field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]: @@ -457,7 +454,7 @@ def get_fields_from_routes( route, routing.APIRoute ): if route.body_field: - assert _is_model_field(route.body_field), ( + assert isinstance(route.body_field, ModelField), ( "A request body must be a Pydantic Field" ) body_fields_from_routes.append(route.body_field) diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 844542594..0834fd741 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -5,6 +5,7 @@ from annotated_doc import Doc from fastapi import params from fastapi._compat import Undefined from fastapi.openapi.models import Example +from pydantic import AliasChoices, AliasPath from typing_extensions import Literal, deprecated _Unset: Any = Undefined @@ -54,10 +55,8 @@ def Path( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -379,10 +378,8 @@ def Query( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -683,10 +680,8 @@ def Header( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -999,10 +994,8 @@ def Cookie( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -1326,10 +1319,8 @@ def Body( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -1641,10 +1632,8 @@ def Form( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -1955,10 +1944,8 @@ def File( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one diff --git a/fastapi/params.py b/fastapi/params.py index cc2934f44..72e797f83 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -6,6 +6,7 @@ from typing import Annotated, Any, Callable, Optional, Union from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.openapi.models import Example +from pydantic import AliasChoices, AliasPath from pydantic.fields import FieldInfo from typing_extensions import Literal, deprecated @@ -34,9 +35,7 @@ class Param(FieldInfo): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -147,9 +146,7 @@ class Path(Param): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -233,9 +230,7 @@ class Query(Param): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -317,9 +312,7 @@ class Header(Param): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, convert_underscores: bool = True, title: Optional[str] = None, @@ -403,9 +396,7 @@ class Cookie(Param): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -487,9 +478,7 @@ class Body(FieldInfo): # type: ignore[misc] media_type: str = "application/json", alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -600,9 +589,7 @@ class Form(Body): # type: ignore[misc] media_type: str = "application/x-www-form-urlencoded", alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -684,9 +671,7 @@ class File(Form): # type: ignore[misc] media_type: str = "multipart/form-data", alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, diff --git a/fastapi/routing.py b/fastapi/routing.py index 3f78e93e8..9ca2f4673 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -2,7 +2,6 @@ import email.message import functools import inspect import json -import warnings from collections.abc import ( AsyncIterator, Awaitable, @@ -22,16 +21,12 @@ from typing import ( ) from annotated_doc import Doc -from fastapi import params, temp_pydantic_v1_params +from fastapi import params from fastapi._compat import ( ModelField, Undefined, - _get_model_config, - _model_dump, - _normalize_errors, annotation_is_pydantic_v1, lenient_issubclass, - may_v1, ) from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.dependencies.models import Dependant @@ -47,8 +42,8 @@ from fastapi.dependencies.utils import ( from fastapi.encoders import jsonable_encoder from fastapi.exceptions import ( EndpointContext, - FastAPIDeprecationWarning, FastAPIError, + PydanticV1NotSupportedError, RequestValidationError, ResponseValidationError, WebSocketRequestValidationError, @@ -148,51 +143,6 @@ def websocket_session( return app -def _prepare_response_content( - res: Any, - *, - exclude_unset: bool, - exclude_defaults: bool = False, - exclude_none: bool = False, -) -> Any: - if isinstance(res, may_v1.BaseModel): - read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) # type: ignore[arg-type] - if read_with_orm_mode: - # Let from_orm extract the data from this model instead of converting - # it now to a dict. - # Otherwise, there's no way to extract lazy data that requires attribute - # access instead of dict iteration, e.g. lazy relationships. - return res - return _model_dump( - res, # type: ignore[arg-type] - by_alias=True, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - elif isinstance(res, list): - return [ - _prepare_response_content( - item, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - for item in res - ] - elif isinstance(res, dict): - return { - k: _prepare_response_content( - v, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - for k, v in res.items() - } - return res - - def _merge_lifespan_context( original_context: Lifespan[Any], nested_context: Lifespan[Any] ) -> Lifespan[Any]: @@ -252,14 +202,6 @@ async def serialize_response( ) -> Any: if field: errors = [] - if not hasattr(field, "serialize"): - # pydantic v1 - response_content = _prepare_response_content( - response_content, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) if is_coroutine: value, errors_ = field.validate(response_content, {}, loc=("response",)) else: @@ -268,28 +210,15 @@ async def serialize_response( ) if isinstance(errors_, list): errors.extend(errors_) - elif errors_: - errors.append(errors_) if errors: ctx = endpoint_ctx or EndpointContext() raise ResponseValidationError( - errors=_normalize_errors(errors), + errors=errors, body=response_content, endpoint_ctx=ctx, ) - if hasattr(field, "serialize"): - return field.serialize( - value, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - - return jsonable_encoder( + return field.serialize( value, include=include, exclude=exclude, @@ -298,6 +227,7 @@ async def serialize_response( exclude_defaults=exclude_defaults, exclude_none=exclude_none, ) + else: return jsonable_encoder(response_content) @@ -332,9 +262,7 @@ def get_request_handler( ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" is_coroutine = dependant.is_coroutine_callable - is_body_form = body_field and isinstance( - body_field.field_info, (params.Form, temp_pydantic_v1_params.Form) - ) + is_body_form = body_field and isinstance(body_field.field_info, params.Form) if isinstance(response_class, DefaultPlaceholder): actual_response_class: type[Response] = response_class.value else: @@ -464,7 +392,7 @@ def get_request_handler( response.headers.raw.extend(solved_result.response.headers.raw) if errors: validation_error = RequestValidationError( - _normalize_errors(errors), body=body, endpoint_ctx=endpoint_ctx + errors, body=body, endpoint_ctx=endpoint_ctx ) raise validation_error @@ -503,7 +431,7 @@ def get_websocket_app( ) if solved_result.errors: raise WebSocketRequestValidationError( - _normalize_errors(solved_result.errors), + solved_result.errors, endpoint_ctx=endpoint_ctx, ) assert dependant.call is not None, "dependant.call must be a function" @@ -638,11 +566,9 @@ class APIRoute(routing.Route): ) response_name = "Response_" + self.unique_id if annotation_is_pydantic_v1(self.response_model): - warnings.warn( - "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." - f" Please update the response model {self.response_model!r}.", - category=FastAPIDeprecationWarning, - stacklevel=4, + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" Please update the response model {self.response_model!r}." ) self.response_field = create_model_field( name=response_name, @@ -678,11 +604,9 @@ class APIRoute(routing.Route): ) response_name = f"Response_{additional_status_code}_{self.unique_id}" if annotation_is_pydantic_v1(model): - warnings.warn( - "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." - f" In responses={{}}, please update {model}.", - category=FastAPIDeprecationWarning, - stacklevel=4, + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" In responses={{}}, please update {model}." ) response_field = create_model_field( name=response_name, type_=model, mode="serialization" diff --git a/fastapi/temp_pydantic_v1_params.py b/fastapi/temp_pydantic_v1_params.py deleted file mode 100644 index 62230e42c..000000000 --- a/fastapi/temp_pydantic_v1_params.py +++ /dev/null @@ -1,718 +0,0 @@ -import warnings -from typing import Annotated, Any, Callable, Optional, Union - -from fastapi.exceptions import FastAPIDeprecationWarning -from fastapi.openapi.models import Example -from fastapi.params import ParamTypes -from typing_extensions import deprecated - -from ._compat.may_v1 import FieldInfo, Undefined - -_Unset: Any = Undefined - - -class Param(FieldInfo): - in_: ParamTypes - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - if example is not _Unset: - warnings.warn( - "`example` has been deprecated, please use `examples` instead", - category=FastAPIDeprecationWarning, - stacklevel=4, - ) - self.example = example - self.include_in_schema = include_in_schema - self.openapi_examples = openapi_examples - kwargs = dict( - default=default, - default_factory=default_factory, - alias=alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - discriminator=discriminator, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - **extra, - ) - if examples is not None: - kwargs["examples"] = examples - if regex is not None: - warnings.warn( - "`regex` has been deprecated, please use `pattern` instead", - category=FastAPIDeprecationWarning, - stacklevel=4, - ) - current_json_schema_extra = json_schema_extra or extra - kwargs["deprecated"] = deprecated - kwargs["regex"] = pattern or regex - kwargs.update(**current_json_schema_extra) - use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} - - super().__init__(**use_kwargs) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.default})" - - -class Path(Param): - in_ = ParamTypes.path - - def __init__( - self, - default: Any = ..., - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - assert default is ..., "Path parameters cannot have a default value" - self.in_ = self.in_ - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Query(Param): - in_ = ParamTypes.query - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Header(Param): - in_ = ParamTypes.header - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - convert_underscores: bool = True, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - self.convert_underscores = convert_underscores - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Cookie(Param): - in_ = ParamTypes.cookie - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Body(FieldInfo): - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - embed: Union[bool, None] = None, - media_type: str = "application/json", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - self.embed = embed - self.media_type = media_type - if example is not _Unset: - warnings.warn( - "`example` has been deprecated, please use `examples` instead", - category=FastAPIDeprecationWarning, - stacklevel=4, - ) - self.example = example - self.include_in_schema = include_in_schema - self.openapi_examples = openapi_examples - kwargs = dict( - default=default, - default_factory=default_factory, - alias=alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - discriminator=discriminator, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - **extra, - ) - if examples is not None: - kwargs["examples"] = examples - if regex is not None: - warnings.warn( - "`regex` has been deprecated, please use `pattern` instead", - category=FastAPIDeprecationWarning, - stacklevel=4, - ) - current_json_schema_extra = json_schema_extra or extra - kwargs["deprecated"] = deprecated - kwargs["regex"] = pattern or regex - kwargs.update(**current_json_schema_extra) - - use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} - - super().__init__(**use_kwargs) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.default})" - - -class Form(Body): - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - media_type: str = "application/x-www-form-urlencoded", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - media_type=media_type, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class File(Form): - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - media_type: str = "multipart/form-data", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - media_type=media_type, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) diff --git a/fastapi/utils.py b/fastapi/utils.py index 8ae50aa14..78fdcbb5b 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -6,7 +6,6 @@ from typing import ( Any, Optional, Union, - cast, ) from weakref import WeakKeyDictionary @@ -19,11 +18,9 @@ from fastapi._compat import ( UndefinedType, Validator, annotation_is_pydantic_v1, - lenient_issubclass, - may_v1, ) from fastapi.datastructures import DefaultPlaceholder, DefaultType -from fastapi.exceptions import FastAPIDeprecationWarning +from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Literal @@ -83,52 +80,18 @@ def create_model_field( mode: Literal["validation", "serialization"] = "validation", version: Literal["1", "auto"] = "auto", ) -> ModelField: + if annotation_is_pydantic_v1(type_): + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" Please update the response model {type_!r}." + ) class_validators = class_validators or {} - v1_model_config = may_v1.BaseConfig - v1_field_info = field_info or may_v1.FieldInfo() - v1_kwargs = { - "name": name, - "field_info": v1_field_info, - "type_": type_, - "class_validators": class_validators, - "default": default, - "required": required, - "model_config": v1_model_config, - "alias": alias, - } - - if ( - annotation_is_pydantic_v1(type_) - or isinstance(field_info, may_v1.FieldInfo) - or version == "1" - ): - from fastapi._compat import v1 - - try: - return v1.ModelField(**v1_kwargs) # type: ignore[return-value] - except RuntimeError: - raise fastapi.exceptions.FastAPIError( - _invalid_args_message.format(type_=type_) - ) from None - else: - field_info = field_info or FieldInfo( - annotation=type_, default=default, alias=alias - ) - kwargs = {"mode": mode, "name": name, "field_info": field_info} - try: - return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type] - except PydanticSchemaGenerationError: - raise fastapi.exceptions.FastAPIError( - _invalid_args_message.format(type_=type_) - ) from None - # Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be - # a Pydantic v1 type, like a constrained int - from fastapi._compat import v1 - + field_info = field_info or FieldInfo(annotation=type_, default=default, alias=alias) + kwargs = {"mode": mode, "name": name, "field_info": field_info} try: - return v1.ModelField(**v1_kwargs) - except RuntimeError: + return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type] + except PydanticSchemaGenerationError: raise fastapi.exceptions.FastAPIError( _invalid_args_message.format(type_=type_) ) from None @@ -139,57 +102,7 @@ def create_cloned_field( *, cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None, ) -> ModelField: - if isinstance(field, v2.ModelField): - return field - - from fastapi._compat import v1 - - # cloned_types caches already cloned types to support recursive models and improve - # performance by avoiding unnecessary cloning - if cloned_types is None: - cloned_types = _CLONED_TYPES_CACHE - - original_type = field.type_ - use_type = original_type - if lenient_issubclass(original_type, v1.BaseModel): - original_type = cast(type[v1.BaseModel], original_type) - use_type = cloned_types.get(original_type) - if use_type is None: - use_type = v1.create_model(original_type.__name__, __base__=original_type) - cloned_types[original_type] = use_type - for f in original_type.__fields__.values(): - use_type.__fields__[f.name] = create_cloned_field( - f, - cloned_types=cloned_types, - ) - new_field = create_model_field(name=field.name, type_=use_type, version="1") - new_field.has_alias = field.has_alias # type: ignore[attr-defined] - new_field.alias = field.alias # type: ignore[misc] - new_field.class_validators = field.class_validators # type: ignore[attr-defined] - new_field.default = field.default # type: ignore[misc] - new_field.default_factory = field.default_factory # type: ignore[attr-defined] - new_field.required = field.required # type: ignore[misc] - new_field.model_config = field.model_config # type: ignore[attr-defined] - new_field.field_info = field.field_info - new_field.allow_none = field.allow_none # type: ignore[attr-defined] - new_field.validate_always = field.validate_always # type: ignore[attr-defined] - if field.sub_fields: # type: ignore[attr-defined] - new_field.sub_fields = [ # type: ignore[attr-defined] - create_cloned_field(sub_field, cloned_types=cloned_types) - for sub_field in field.sub_fields # type: ignore[attr-defined] - ] - if field.key_field: # type: ignore[attr-defined] - new_field.key_field = create_cloned_field( # type: ignore[attr-defined] - field.key_field, # type: ignore[attr-defined] - cloned_types=cloned_types, - ) - new_field.validators = field.validators # type: ignore[attr-defined] - new_field.pre_validators = field.pre_validators # type: ignore[attr-defined] - new_field.post_validators = field.post_validators # type: ignore[attr-defined] - new_field.parse_json = field.parse_json # type: ignore[attr-defined] - new_field.shape = field.shape # type: ignore[attr-defined] - new_field.populate_validators() # type: ignore[attr-defined] - return new_field + return field def generate_operation_id_for_path( diff --git a/pyproject.toml b/pyproject.toml index 8f824af5d..9c2c35a9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -199,14 +199,22 @@ omit = [ "docs_src/dependencies/tutorial008_an_py39.py", # difficult to mock "docs_src/dependencies/tutorial013_an_py310.py", # temporary code example? "docs_src/dependencies/tutorial014_an_py310.py", # temporary code example? - # Pydantic V1 + # Pydantic v1 migration, no longer tested + "docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py", + "docs_src/pydantic_v1_in_v2/tutorial001_an_py39.py", + "docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py", + "docs_src/pydantic_v1_in_v2/tutorial002_an_py39.py", + "docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py", + "docs_src/pydantic_v1_in_v2/tutorial003_an_py39.py", + "docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py", + "docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py", + # TODO: remove when removing this file, after updating translations, Pydantic v1 "docs_src/schema_extra_example/tutorial001_pv1_py310.py", - "docs_src/query_param_models/tutorial002_pv1_py310.py", - "docs_src/query_param_models/tutorial002_pv1_an_py310.py", - "docs_src/header_param_models/tutorial002_pv1_py310.py", - "docs_src/header_param_models/tutorial002_pv1_an_py310.py", - "docs_src/cookie_param_models/tutorial002_pv1_py310.py", - "docs_src/cookie_param_models/tutorial002_pv1_an_py310.py", + "docs_src/schema_extra_example/tutorial001_pv1_py39.py", + "docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py", + "docs_src/settings/app03_py39/config_pv1.py", + "docs_src/settings/app03_an_py39/config_pv1.py", + "docs_src/settings/tutorial001_pv1_py39.py", ] [tool.coverage.report] diff --git a/tests/test_compat.py b/tests/test_compat.py index 8d2071030..0b5600f8f 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,20 +1,16 @@ -from typing import Any, Union +from typing import Union from fastapi import FastAPI, UploadFile from fastapi._compat import ( Undefined, - _get_model_config, - get_cached_model_fields, - is_scalar_field, is_uploadfile_sequence_annotation, - may_v1, ) from fastapi._compat.shared import is_bytes_sequence_annotation from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict from pydantic.fields import FieldInfo -from .utils import needs_py310, needs_py_lt_314 +from .utils import needs_py310 def test_model_field_default_required(): @@ -26,35 +22,6 @@ def test_model_field_default_required(): assert field.default is Undefined -@needs_py_lt_314 -def test_v1_plain_validator_function(): - from fastapi._compat import v1 - - # For coverage - def func(v): # pragma: no cover - return v - - result = v1.with_info_plain_validator_function(func) - assert result == {} - - -def test_is_model_field(): - # For coverage - from fastapi._compat import _is_model_field - - assert not _is_model_field(str) - - -def test_get_model_config(): - # For coverage in Pydantic v2 - class Foo(BaseModel): - model_config = ConfigDict(from_attributes=True) - - foo = Foo() - config = _get_model_config(foo) - assert config == {"from_attributes": True} - - def test_complex(): app = FastAPI() @@ -165,33 +132,3 @@ def test_serialize_sequence_value_with_none_first_in_union(): result = v2.serialize_sequence_value(field=field, value=["x", "y"]) assert result == ["x", "y"] assert isinstance(result, list) - - -@needs_py_lt_314 -def test_is_pv1_scalar_field(): - from fastapi._compat import v1 - - # For coverage - class Model(v1.BaseModel): - foo: Union[str, dict[str, Any]] - - fields = v1.get_model_fields(Model) - assert not is_scalar_field(fields[0]) - - -@needs_py_lt_314 -def test_get_model_fields_cached(): - from fastapi._compat import v1 - - class Model(may_v1.BaseModel): - foo: str - - non_cached_fields = v1.get_model_fields(Model) - non_cached_fields2 = v1.get_model_fields(Model) - cached_fields = get_cached_model_fields(Model) - cached_fields2 = get_cached_model_fields(Model) - for f1, f2 in zip(cached_fields, cached_fields2): - assert f1 is f2 - - assert non_cached_fields is not non_cached_fields2 - assert cached_fields is cached_fields2 diff --git a/tests/test_compat_params_v1.py b/tests/test_compat_params_v1.py deleted file mode 100644 index 704b3f77a..000000000 --- a/tests/test_compat_params_v1.py +++ /dev/null @@ -1,1060 +0,0 @@ -import sys -import warnings -from typing import Optional - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from typing import Annotated - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.temp_pydantic_v1_params import ( - Body, - Cookie, - File, - Form, - Header, - Path, - Query, -) -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - - -class Item(BaseModel): - name: str - price: float - description: Optional[str] = None - - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.get("/items/{item_id}") - def get_item_with_path( - item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=1000)], - ): - return {"item_id": item_id} - - @app.get("/items/") - def get_items_with_query( - q: Annotated[ - Optional[str], - Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$"), - ] = None, - skip: Annotated[int, Query(ge=0)] = 0, - limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10, - ): - return {"q": q, "skip": skip, "limit": limit} - - @app.get("/users/") - def get_user_with_header( - x_custom: Annotated[Optional[str], Header()] = None, - x_token: Annotated[Optional[str], Header(convert_underscores=True)] = None, - ): - return {"x_custom": x_custom, "x_token": x_token} - - @app.get("/cookies/") - def get_cookies( - session_id: Annotated[Optional[str], Cookie()] = None, - tracking_id: Annotated[Optional[str], Cookie(min_length=10)] = None, - ): - return {"session_id": session_id, "tracking_id": tracking_id} - - @app.post("/items/") - def create_item( - item: Annotated[ - Item, - Body( - examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}] - ), - ], - ): - return {"item": item} - - @app.post("/items-embed/") - def create_item_embed( - item: Annotated[Item, Body(embed=True)], - ): - return {"item": item} - - @app.put("/items/{item_id}") - def update_item( - item_id: Annotated[int, Path(ge=1)], - item: Annotated[Item, Body()], - importance: Annotated[int, Body(gt=0, le=10)], - ): - return {"item": item, "importance": importance} - - @app.post("/form-data/") - def submit_form( - username: Annotated[str, Form(min_length=3, max_length=50)], - password: Annotated[str, Form(min_length=8)], - email: Annotated[Optional[str], Form()] = None, - ): - return {"username": username, "password": password, "email": email} - - @app.post("/upload/") - def upload_file( - file: Annotated[bytes, File()], - description: Annotated[Optional[str], Form()] = None, - ): - return {"file_size": len(file), "description": description} - - @app.post("/upload-multiple/") - def upload_multiple_files( - files: Annotated[list[bytes], File()], - note: Annotated[str, Form()] = "", - ): - return { - "file_count": len(files), - "total_size": sum(len(f) for f in files), - "note": note, - } - - -client = TestClient(app) - - -# Path parameter tests -def test_path_param_valid(): - response = client.get("/items/50") - assert response.status_code == 200 - assert response.json() == {"item_id": 50} - - -def test_path_param_too_large(): - response = client.get("/items/1001") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["path", "item_id"] - - -def test_path_param_too_small(): - response = client.get("/items/0") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["path", "item_id"] - - -# Query parameter tests -def test_query_params_valid(): - response = client.get("/items/?q=test search&skip=5&limit=20") - assert response.status_code == 200 - assert response.json() == {"q": "test search", "skip": 5, "limit": 20} - - -def test_query_params_defaults(): - response = client.get("/items/") - assert response.status_code == 200 - assert response.json() == {"q": None, "skip": 0, "limit": 10} - - -def test_query_param_too_short(): - response = client.get("/items/?q=ab") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["query", "q"] - - -def test_query_param_invalid_pattern(): - response = client.get("/items/?q=test@#$") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["query", "q"] - - -def test_query_param_limit_too_large(): - response = client.get("/items/?limit=101") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["query", "limit"] - - -# Header parameter tests -def test_header_params(): - response = client.get( - "/users/", - headers={"X-Custom": "Plumbus", "X-Token": "secret-token"}, - ) - assert response.status_code == 200 - assert response.json() == { - "x_custom": "Plumbus", - "x_token": "secret-token", - } - - -def test_header_underscore_conversion(): - response = client.get( - "/users/", - headers={"x-token": "secret-token-with-dash"}, - ) - assert response.status_code == 200 - assert response.json()["x_token"] == "secret-token-with-dash" - - -def test_header_params_none(): - response = client.get("/users/") - assert response.status_code == 200 - assert response.json() == {"x_custom": None, "x_token": None} - - -# Cookie parameter tests -def test_cookie_params(): - with TestClient(app) as test_client: - test_client.cookies.set("session_id", "abc123") - test_client.cookies.set("tracking_id", "1234567890abcdef") - response = test_client.get("/cookies/") - assert response.status_code == 200 - assert response.json() == { - "session_id": "abc123", - "tracking_id": "1234567890abcdef", - } - - -def test_cookie_tracking_id_too_short(): - with TestClient(app) as test_client: - test_client.cookies.set("tracking_id", "short") - response = test_client.get("/cookies/") - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["cookie", "tracking_id"], - "msg": "ensure this value has at least 10 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 10}, - } - ] - } - ) - - -def test_cookie_params_none(): - response = client.get("/cookies/") - assert response.status_code == 200 - assert response.json() == {"session_id": None, "tracking_id": None} - - -# Body parameter tests -def test_body_param(): - response = client.post( - "/items/", - json={"name": "Test Item", "price": 29.99, "description": "A test item"}, - ) - assert response.status_code == 200 - assert response.json() == { - "item": { - "name": "Test Item", - "price": 29.99, - "description": "A test item", - } - } - - -def test_body_param_minimal(): - response = client.post( - "/items/", - json={"name": "Minimal", "price": 9.99}, - ) - assert response.status_code == 200 - assert response.json() == { - "item": {"name": "Minimal", "price": 9.99, "description": None} - } - - -def test_body_param_missing_required(): - response = client.post( - "/items/", - json={"name": "Incomplete"}, - ) - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["body", "price"] - - -def test_body_embed(): - response = client.post( - "/items-embed/", - json={"item": {"name": "Embedded", "price": 15.0}}, - ) - assert response.status_code == 200 - assert response.json() == { - "item": {"name": "Embedded", "price": 15.0, "description": None} - } - - -def test_body_embed_wrong_structure(): - response = client.post( - "/items-embed/", - json={"name": "Not Embedded", "price": 15.0}, - ) - assert response.status_code == 422 - - -# Multiple body parameters test -def test_multiple_body_params(): - response = client.put( - "/items/5", - json={ - "item": {"name": "Updated Item", "price": 49.99}, - "importance": 8, - }, - ) - assert response.status_code == 200 - assert response.json() == snapshot( - { - "item": {"name": "Updated Item", "price": 49.99, "description": None}, - "importance": 8, - } - ) - - -def test_multiple_body_params_importance_too_large(): - response = client.put( - "/items/5", - json={ - "item": {"name": "Item", "price": 10.0}, - "importance": 11, - }, - ) - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "importance"], - "msg": "ensure this value is less than or equal to 10", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 10}, - } - ] - } - ) - - -def test_multiple_body_params_importance_too_small(): - response = client.put( - "/items/5", - json={ - "item": {"name": "Item", "price": 10.0}, - "importance": 0, - }, - ) - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "importance"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, - } - ] - } - ) - - -# Form parameter tests -def test_form_data_valid(): - response = client.post( - "/form-data/", - data={ - "username": "testuser", - "password": "password123", - "email": "test@example.com", - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "testuser", - "password": "password123", - "email": "test@example.com", - } - - -def test_form_data_optional_field(): - response = client.post( - "/form-data/", - data={"username": "testuser", "password": "password123"}, - ) - assert response.status_code == 200 - assert response.json() == { - "username": "testuser", - "password": "password123", - "email": None, - } - - -def test_form_data_username_too_short(): - response = client.post( - "/form-data/", - data={"username": "ab", "password": "password123"}, - ) - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "ensure this value has at least 3 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 3}, - } - ] - } - ) - - -def test_form_data_password_too_short(): - response = client.post( - "/form-data/", - data={"username": "testuser", "password": "short"}, - ) - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "ensure this value has at least 8 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 8}, - } - ] - } - ) - - -# File upload tests -def test_upload_file(): - response = client.post( - "/upload/", - files={"file": ("test.txt", b"Hello, World!", "text/plain")}, - data={"description": "A test file"}, - ) - assert response.status_code == 200 - assert response.json() == { - "file_size": 13, - "description": "A test file", - } - - -def test_upload_file_without_description(): - response = client.post( - "/upload/", - files={"file": ("test.txt", b"Hello!", "text/plain")}, - ) - assert response.status_code == 200 - assert response.json() == { - "file_size": 6, - "description": None, - } - - -def test_upload_multiple_files(): - response = client.post( - "/upload-multiple/", - files=[ - ("files", ("file1.txt", b"Content 1", "text/plain")), - ("files", ("file2.txt", b"Content 2", "text/plain")), - ("files", ("file3.txt", b"Content 3", "text/plain")), - ], - data={"note": "Multiple files uploaded"}, - ) - assert response.status_code == 200 - assert response.json() == { - "file_count": 3, - "total_size": 27, - "note": "Multiple files uploaded", - } - - -def test_upload_multiple_files_empty_note(): - response = client.post( - "/upload-multiple/", - files=[ - ("files", ("file1.txt", b"Test", "text/plain")), - ], - ) - assert response.status_code == 200 - assert response.json()["file_count"] == 1 - assert response.json()["note"] == "" - - -# __repr__ tests -def test_query_repr(): - query_param = Query(default=None, min_length=3) - assert repr(query_param) == "Query(None)" - - -def test_body_repr(): - body_param = Body(default=None) - assert repr(body_param) == "Body(None)" - - -# Deprecation warning tests for regex parameter -def test_query_regex_deprecation_warning(): - with pytest.warns(FastAPIDeprecationWarning, match="`regex` has been deprecated"): - Query(regex="^test$") - - -def test_body_regex_deprecation_warning(): - with pytest.warns(FastAPIDeprecationWarning, match="`regex` has been deprecated"): - Body(regex="^test$") - - -# Deprecation warning tests for example parameter -def test_query_example_deprecation_warning(): - with pytest.warns(FastAPIDeprecationWarning, match="`example` has been deprecated"): - Query(example="test example") - - -def test_body_example_deprecation_warning(): - with pytest.warns(FastAPIDeprecationWarning, match="`example` has been deprecated"): - Body(example={"test": "example"}) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "summary": "Get Item With Path", - "operationId": "get_item_with_path_items__item_id__get", - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": True, - "schema": { - "title": "The ID of the item", - "minimum": 1, - "maximum": 1000, - "type": "integer", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": True, - "schema": { - "title": "Item Id", - "minimum": 1, - "type": "integer", - }, - } - ], - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "title": "Body", - "allOf": [ - { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - ], - } - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/items/": { - "get": { - "summary": "Get Items With Query", - "operationId": "get_items_with_query_items__get", - "parameters": [ - { - "name": "q", - "in": "query", - "required": False, - "schema": { - "title": "Q", - "maxLength": 50, - "minLength": 3, - "pattern": "^[a-zA-Z0-9 ]+$", - "type": "string", - }, - }, - { - "name": "skip", - "in": "query", - "required": False, - "schema": { - "title": "Skip", - "default": 0, - "minimum": 0, - "type": "integer", - }, - }, - { - "name": "limit", - "in": "query", - "required": False, - "schema": { - "title": "Limit", - "default": 10, - "minimum": 1, - "maximum": 100, - "examples": [5], - "type": "integer", - }, - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "title": "Item", - "examples": [ - { - "name": "Foo", - "price": 35.4, - "description": "The Foo item", - } - ], - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - } - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/users/": { - "get": { - "summary": "Get User With Header", - "operationId": "get_user_with_header_users__get", - "parameters": [ - { - "name": "x-custom", - "in": "header", - "required": False, - "schema": {"title": "X-Custom", "type": "string"}, - }, - { - "name": "x-token", - "in": "header", - "required": False, - "schema": {"title": "X-Token", "type": "string"}, - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookies/": { - "get": { - "summary": "Get Cookies", - "operationId": "get_cookies_cookies__get", - "parameters": [ - { - "name": "session_id", - "in": "cookie", - "required": False, - "schema": {"title": "Session Id", "type": "string"}, - }, - { - "name": "tracking_id", - "in": "cookie", - "required": False, - "schema": { - "title": "Tracking Id", - "minLength": 10, - "type": "string", - }, - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items-embed/": { - "post": { - "summary": "Create Item Embed", - "operationId": "create_item_embed_items_embed__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post" - } - ], - "title": "Body", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/form-data/": { - "post": { - "summary": "Submit Form", - "operationId": "submit_form_form_data__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_submit_form_form_data__post" - } - ], - "title": "Body", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/upload/": { - "post": { - "summary": "Upload File", - "operationId": "upload_file_upload__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_upload_file_upload__post" - } - ], - "title": "Body", - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/upload-multiple/": { - "post": { - "summary": "Upload Multiple Files", - "operationId": "upload_multiple_files_upload_multiple__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post" - } - ], - "title": "Body", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_item_embed_items_embed__post": { - "properties": { - "item": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - } - }, - "type": "object", - "required": ["item"], - "title": "Body_create_item_embed_items_embed__post", - }, - "Body_submit_form_form_data__post": { - "properties": { - "username": { - "type": "string", - "maxLength": 50, - "minLength": 3, - "title": "Username", - }, - "password": { - "type": "string", - "minLength": 8, - "title": "Password", - }, - "email": {"type": "string", "title": "Email"}, - }, - "type": "object", - "required": ["username", "password"], - "title": "Body_submit_form_form_data__post", - }, - "Body_update_item_items__item_id__put": { - "properties": { - "item": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - }, - "importance": { - "type": "integer", - "maximum": 10.0, - "exclusiveMinimum": 0.0, - "title": "Importance", - }, - }, - "type": "object", - "required": ["item", "importance"], - "title": "Body_update_item_items__item_id__put", - }, - "Body_upload_file_upload__post": { - "properties": { - "file": { - "type": "string", - "format": "binary", - "title": "File", - }, - "description": {"type": "string", "title": "Description"}, - }, - "type": "object", - "required": ["file"], - "title": "Body_upload_file_upload__post", - }, - "Body_upload_multiple_files_upload_multiple__post": { - "properties": { - "files": { - "items": {"type": "string", "format": "binary"}, - "type": "array", - "title": "Files", - }, - "note": {"type": "string", "title": "Note", "default": ""}, - }, - "type": "object", - "required": ["files"], - "title": "Body_upload_multiple_files_upload_multiple__post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "price": {"type": "number", "title": "Price"}, - "description": {"type": "string", "title": "Description"}, - }, - "type": "object", - "required": ["name", "price"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_datetime_custom_encoder.py b/tests/test_datetime_custom_encoder.py index 56b6780f0..f154ede02 100644 --- a/tests/test_datetime_custom_encoder.py +++ b/tests/test_datetime_custom_encoder.py @@ -1,12 +1,9 @@ -import warnings from datetime import datetime, timezone from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -from .utils import needs_pydanticv1 - def test_pydanticv2(): from pydantic import field_serializer @@ -29,34 +26,3 @@ def test_pydanticv2(): with client: response = client.get("/model") assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_pydanticv1(): - from pydantic import v1 - - class ModelWithDatetimeField(v1.BaseModel): - dt_field: datetime - - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } - - app = FastAPI() - model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) - - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.get("/model", response_model=ModelWithDatetimeField) - def get_model(): - return model - - client = TestClient(app) - with client: - response = client.get("/model") - assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} diff --git a/tests/test_filter_pydantic_sub_model/__init__.py b/tests/test_filter_pydantic_sub_model/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_filter_pydantic_sub_model/app_pv1.py b/tests/test_filter_pydantic_sub_model/app_pv1.py deleted file mode 100644 index d6f2ce7d2..000000000 --- a/tests/test_filter_pydantic_sub_model/app_pv1.py +++ /dev/null @@ -1,45 +0,0 @@ -import warnings -from typing import Optional - -from fastapi import Depends, FastAPI -from pydantic.v1 import BaseModel, validator - -app = FastAPI() - - -class ModelB(BaseModel): - username: str - - -class ModelC(ModelB): - password: str - - -class ModelA(BaseModel): - name: str - description: Optional[str] = None - model_b: ModelB - tags: dict[str, str] = {} - - @validator("name") - def lower_username(cls, name: str, values): - if not name.endswith("A"): - raise ValueError("name must end in A") - return name - - -async def get_model_c() -> ModelC: - return ModelC(username="test-user", password="test-password") - - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.get("/model/{name}", response_model=ModelA) - async def get_model_a(name: str, model_c=Depends(get_model_c)): - return { - "name": name, - "description": "model-a-desc", - "model_b": model_c, - "tags": {"key1": "value1", "key2": "value2"}, - } diff --git a/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py b/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py deleted file mode 100644 index b464b4f57..000000000 --- a/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py +++ /dev/null @@ -1,146 +0,0 @@ -import pytest -from fastapi.exceptions import ResponseValidationError -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - -from ..utils import needs_pydanticv1 - - -@pytest.fixture(name="client") -def get_client(): - from .app_pv1 import app - - client = TestClient(app) - return client - - -@needs_pydanticv1 -def test_filter_sub_model(client: TestClient): - response = client.get("/model/modelA") - assert response.status_code == 200, response.text - assert response.json() == { - "name": "modelA", - "description": "model-a-desc", - "model_b": {"username": "test-user"}, - "tags": {"key1": "value1", "key2": "value2"}, - } - - -@needs_pydanticv1 -def test_validator_is_cloned(client: TestClient): - with pytest.raises(ResponseValidationError) as err: - client.get("/model/modelX") - assert err.value.errors() == [ - { - "loc": ("response", "name"), - "msg": "name must end in A", - "type": "value_error", - } - ] - - -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/model/{name}": { - "get": { - "summary": "Get Model A", - "operationId": "get_model_a_model__name__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Name", "type": "string"}, - "name": "name", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelA" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "ModelA": { - "title": "ModelA", - "required": ["name", "model_b"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "model_b": {"$ref": "#/components/schemas/ModelB"}, - "tags": { - "additionalProperties": {"type": "string"}, - "type": "object", - "title": "Tags", - "default": {}, - }, - }, - }, - "ModelB": { - "title": "ModelB", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"} - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } - ) diff --git a/tests/test_get_model_definitions_formfeed_escape.py b/tests/test_get_model_definitions_formfeed_escape.py index dee595554..eb7939b69 100644 --- a/tests/test_get_model_definitions_formfeed_escape.py +++ b/tests/test_get_model_definitions_formfeed_escape.py @@ -1,25 +1,12 @@ -import warnings - import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot -from .utils import needs_pydanticv1 - -@pytest.fixture( - name="client", - params=[ - pytest.param("pydantic-v1", marks=needs_pydanticv1), - "pydantic-v2", - ], -) -def client_fixture(request: pytest.FixtureRequest) -> TestClient: - if request.param == "pydantic-v1": - from pydantic.v1 import BaseModel - else: - from pydantic import BaseModel +@pytest.fixture(name="client") +def client_fixture() -> TestClient: + from pydantic import BaseModel class Address(BaseModel): """ @@ -38,28 +25,12 @@ def client_fixture(request: pytest.FixtureRequest) -> TestClient: app = FastAPI() - if request.param == "pydantic-v1": - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.get("/facilities/{facility_id}") - def get_facility(facility_id: str) -> Facility: - return Facility( - id=facility_id, - address=Address( - line_1="123 Main St", city="Anytown", state_province="CA" - ), - ) - else: - - @app.get("/facilities/{facility_id}") - def get_facility(facility_id: str) -> Facility: - return Facility( - id=facility_id, - address=Address( - line_1="123 Main St", city="Anytown", state_province="CA" - ), - ) + @app.get("/facilities/{facility_id}") + def get_facility(facility_id: str) -> Facility: + return Facility( + id=facility_id, + address=Address(line_1="123 Main St", city="Anytown", state_province="CA"), + ) client = TestClient(app) return client diff --git a/tests/test_inherited_custom_class.py b/tests/test_inherited_custom_class.py index 7f29fe33e..8cf8952f9 100644 --- a/tests/test_inherited_custom_class.py +++ b/tests/test_inherited_custom_class.py @@ -5,8 +5,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -from .utils import needs_pydanticv1 - class MyUuid: def __init__(self, uuid_string: str): @@ -67,46 +65,3 @@ def test_pydanticv2(): assert response_pydantic.json() == { "a_uuid": "b8799909-f914-42de-91bc-95c819218d01" } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_pydanticv1(): - from pydantic import v1 - - app = FastAPI() - - @app.get("/fast_uuid") - def return_fast_uuid(): - asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") - assert isinstance(asyncpg_uuid, uuid.UUID) - assert type(asyncpg_uuid) is not uuid.UUID - with pytest.raises(TypeError): - vars(asyncpg_uuid) - return {"fast_uuid": asyncpg_uuid} - - class SomeCustomClass(v1.BaseModel): - class Config: - arbitrary_types_allowed = True - json_encoders = {uuid.UUID: str} - - a_uuid: MyUuid - - @app.get("/get_custom_class") - def return_some_user(): - # Test that the fix also works for custom pydantic classes - return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) - - client = TestClient(app) - - with client: - response_simple = client.get("/fast_uuid") - response_pydantic = client.get("/get_custom_class") - - assert response_simple.json() == { - "fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51" - } - - assert response_pydantic.json() == { - "a_uuid": "b8799909-f914-42de-91bc-95c819218d01" - } diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index 81bf94ece..4528dff44 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -1,3 +1,4 @@ +import warnings from collections import deque from dataclasses import dataclass from datetime import datetime, timezone @@ -5,15 +6,14 @@ from decimal import Decimal from enum import Enum from math import isinf, isnan from pathlib import PurePath, PurePosixPath, PureWindowsPath -from typing import Optional +from typing import Optional, TypedDict import pytest from fastapi._compat import Undefined from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import PydanticV1NotSupportedError from pydantic import BaseModel, Field, ValidationError -from .utils import needs_pydanticv1 - class Person: def __init__(self, name: str): @@ -156,29 +156,17 @@ def test_encode_custom_json_encoders_model_pydanticv2(): assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_encode_custom_json_encoders_model_pydanticv1(): - from pydantic import v1 +def test_json_encoder_error_with_pydanticv1(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 - class ModelWithCustomEncoder(v1.BaseModel): - dt_field: datetime + class ModelV1(v1.BaseModel): + name: str - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } - - class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): - class Config: - pass - - model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) - assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} - subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) - assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} + data = ModelV1(name="test") + with pytest.raises(PydanticV1NotSupportedError): + jsonable_encoder(data) def test_encode_model_with_config(): @@ -214,25 +202,27 @@ def test_encode_model_with_default(): } -@needs_pydanticv1 def test_custom_encoders(): - from pydantic import v1 - class safe_datetime(datetime): pass - class MyModel(v1.BaseModel): + class MyDict(TypedDict): dt_field: safe_datetime - instance = MyModel(dt_field=safe_datetime.now()) + instance = MyDict(dt_field=safe_datetime.now()) encoded_instance = jsonable_encoder( instance, custom_encoder={safe_datetime: lambda o: o.strftime("%H:%M:%S")} ) - assert encoded_instance["dt_field"] == instance.dt_field.strftime("%H:%M:%S") + assert encoded_instance["dt_field"] == instance["dt_field"].strftime("%H:%M:%S") + + encoded_instance = jsonable_encoder( + instance, custom_encoder={datetime: lambda o: o.strftime("%H:%M:%S")} + ) + assert encoded_instance["dt_field"] == instance["dt_field"].strftime("%H:%M:%S") encoded_instance2 = jsonable_encoder(instance) - assert encoded_instance2["dt_field"] == instance.dt_field.isoformat() + assert encoded_instance2["dt_field"] == instance["dt_field"].isoformat() def test_custom_enum_encoders(): @@ -287,17 +277,6 @@ def test_encode_pure_path(): assert jsonable_encoder({"path": test_path}) == {"path": str(test_path)} -@needs_pydanticv1 -def test_encode_root(): - from pydantic import v1 - - class ModelWithRoot(v1.BaseModel): - __root__: str - - model = ModelWithRoot(__root__="Foo") - assert jsonable_encoder(model) == "Foo" - - def test_decimal_encoder_float(): data = {"value": Decimal(1.23)} assert jsonable_encoder(data) == {"value": 1.23} diff --git a/tests/test_pydantic_v1_deprecation_warnings.py b/tests/test_pydantic_v1_deprecation_warnings.py deleted file mode 100644 index 89ca6a865..000000000 --- a/tests/test_pydantic_v1_deprecation_warnings.py +++ /dev/null @@ -1,99 +0,0 @@ -import sys - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient - - -def test_warns_pydantic_v1_model_in_endpoint_param() -> None: - class ParamModelV1(BaseModel): - name: str - - app = FastAPI() - - with pytest.warns( - FastAPIDeprecationWarning, - match=r"pydantic\.v1 is deprecated.*Please update the param data:", - ): - - @app.post("/param") - def endpoint(data: ParamModelV1): - return data - - client = TestClient(app) - response = client.post("/param", json={"name": "test"}) - assert response.status_code == 200, response.text - assert response.json() == {"name": "test"} - - -def test_warns_pydantic_v1_model_in_return_type() -> None: - class ReturnModelV1(BaseModel): - name: str - - app = FastAPI() - - with pytest.warns( - FastAPIDeprecationWarning, - match=r"pydantic\.v1 is deprecated.*Please update the response model", - ): - - @app.get("/return") - def endpoint() -> ReturnModelV1: - return ReturnModelV1(name="test") - - client = TestClient(app) - response = client.get("/return") - assert response.status_code == 200, response.text - assert response.json() == {"name": "test"} - - -def test_warns_pydantic_v1_model_in_response_model() -> None: - class ResponseModelV1(BaseModel): - name: str - - app = FastAPI() - - with pytest.warns( - FastAPIDeprecationWarning, - match=r"pydantic\.v1 is deprecated.*Please update the response model", - ): - - @app.get("/response-model", response_model=ResponseModelV1) - def endpoint(): - return {"name": "test"} - - client = TestClient(app) - response = client.get("/response-model") - assert response.status_code == 200, response.text - assert response.json() == {"name": "test"} - - -def test_warns_pydantic_v1_model_in_additional_responses_model() -> None: - class ErrorModelV1(BaseModel): - detail: str - - app = FastAPI() - - with pytest.warns( - FastAPIDeprecationWarning, - match=r"pydantic\.v1 is deprecated.*In responses=\{\}, please update", - ): - - @app.get( - "/responses", response_model=None, responses={400: {"model": ErrorModelV1}} - ) - def endpoint(): - return {"ok": True} - - client = TestClient(app) - response = client.get("/responses") - assert response.status_code == 200, response.text - assert response.json() == {"ok": True} diff --git a/tests/test_pydantic_v1_error.py b/tests/test_pydantic_v1_error.py new file mode 100644 index 000000000..13229a313 --- /dev/null +++ b/tests/test_pydantic_v1_error.py @@ -0,0 +1,97 @@ +import sys +import warnings +from typing import Union + +import pytest + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi.exceptions import PydanticV1NotSupportedError + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic.v1 import BaseModel + + +def test_raises_pydantic_v1_model_in_endpoint_param() -> None: + class ParamModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.post("/param") + def endpoint(data: ParamModelV1): # pragma: no cover + return data + + +def test_raises_pydantic_v1_model_in_return_type() -> None: + class ReturnModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.get("/return") + def endpoint() -> ReturnModelV1: # pragma: no cover + return ReturnModelV1(name="test") + + +def test_raises_pydantic_v1_model_in_response_model() -> None: + class ResponseModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.get("/response-model", response_model=ResponseModelV1) + def endpoint(): # pragma: no cover + return {"name": "test"} + + +def test_raises_pydantic_v1_model_in_additional_responses_model() -> None: + class ErrorModelV1(BaseModel): + detail: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.get( + "/responses", response_model=None, responses={400: {"model": ErrorModelV1}} + ) + def endpoint(): # pragma: no cover + return {"ok": True} + + +def test_raises_pydantic_v1_model_in_union() -> None: + class ModelV1A(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.post("/union") + def endpoint(data: Union[dict, ModelV1A]): # pragma: no cover + return data + + +def test_raises_pydantic_v1_model_in_sequence() -> None: + class ModelV1A(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.post("/sequence") + def endpoint(data: list[ModelV1A]): # pragma: no cover + return data diff --git a/tests/test_pydantic_v1_v2_01.py b/tests/test_pydantic_v1_v2_01.py deleted file mode 100644 index 4868e5d22..000000000 --- a/tests/test_pydantic_v1_v2_01.py +++ /dev/null @@ -1,439 +0,0 @@ -import sys -import warnings -from typing import Any, Union - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/simple-model") - def handle_simple_model(data: SubItem) -> SubItem: - return data - - @app.post("/simple-model-filter", response_model=SubItem) - def handle_simple_model_filter(data: SubItem) -> Any: - extended_data = data.dict() - extended_data.update({"secret_price": 42}) - return extended_data - - @app.post("/item") - def handle_item(data: Item) -> Item: - return data - - @app.post("/item-filter", response_model=Item) - def handle_item_filter(data: Item) -> Any: - extended_data = data.dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return extended_data - - -client = TestClient(app) - - -def test_old_simple_model(): - response = client.post( - "/simple-model", - json={"name": "Foo"}, - ) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo"} - - -def test_old_simple_model_validation_error(): - response = client.post( - "/simple-model", - json={"wrong_name": "Foo"}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_old_simple_model_filter(): - response = client.post( - "/simple-model-filter", - json={"name": "Foo"}, - ) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo"} - - -def test_item_model(): - response = client.post( - "/item", - json={ - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - } - - -def test_item_model_minimal(): - response = client.post( - "/item", - json={"title": "Minimal Item", "size": 50, "sub": {"name": "SubMin"}}, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "Minimal Item", - "size": 50, - "description": None, - "sub": {"name": "SubMin"}, - "multi": [], - } - - -def test_item_model_validation_errors(): - response = client.post( - "/item", - json={"title": "Missing fields"}, - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 2 - assert { - "loc": ["body", "size"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - assert { - "loc": ["body", "sub"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - - -def test_item_model_nested_validation_error(): - response = client.post( - "/item", - json={"title": "Test Item", "size": 100, "sub": {"wrong_field": "test"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "sub", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_item_model_invalid_type(): - response = client.post( - "/item", - json={"title": "Test Item", "size": "not_a_number", "sub": {"name": "SubItem"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_item_filter(): - response = client.post( - "/item-filter", - json={ - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == { - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - } - assert "secret_data" not in result - assert "internal_id" not in result - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/simple-model": { - "post": { - "summary": "Handle Simple Model", - "operationId": "handle_simple_model_simple_model_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/SubItem"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/simple-model-filter": { - "post": { - "summary": "Handle Simple Model Filter", - "operationId": "handle_simple_model_filter_simple_model_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/SubItem"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item": { - "post": { - "summary": "Handle Item", - "operationId": "handle_item_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-filter": { - "post": { - "summary": "Handle Item Filter", - "operationId": "handle_item_filter_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_list.py b/tests/test_pydantic_v1_v2_list.py deleted file mode 100644 index 108f231fa..000000000 --- a/tests/test_pydantic_v1_v2_list.py +++ /dev/null @@ -1,682 +0,0 @@ -import sys -import warnings -from typing import Any, Union - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -app = FastAPI() - - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/item") - def handle_item(data: Item) -> list[Item]: - return [data, data] - - @app.post("/item-filter", response_model=list[Item]) - def handle_item_filter(data: Item) -> Any: - extended_data = data.dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return [extended_data, extended_data] - - @app.post("/item-list") - def handle_item_list(data: list[Item]) -> Item: - if data: - return data[0] - return Item(title="", size=0, sub=SubItem(name="")) - - @app.post("/item-list-filter", response_model=Item) - def handle_item_list_filter(data: list[Item]) -> Any: - if data: - extended_data = data[0].dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return extended_data - return Item(title="", size=0, sub=SubItem(name="")) - - @app.post("/item-list-to-list") - def handle_item_list_to_list(data: list[Item]) -> list[Item]: - return data - - @app.post("/item-list-to-list-filter", response_model=list[Item]) - def handle_item_list_to_list_filter(data: list[Item]) -> Any: - if data: - extended_data = data[0].dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return [extended_data, extended_data] - return [] - - -client = TestClient(app) - - -def test_item_to_list(): - response = client.post( - "/item", - json={ - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 2 - for item in result: - assert item == { - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - } - - -def test_item_to_list_filter(): - response = client.post( - "/item-filter", - json={ - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 2 - for item in result: - assert item == { - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - } - # Verify secret fields are filtered out - assert "secret_data" not in item - assert "internal_id" not in item - assert "internal_id" not in item["sub"] - - -def test_list_to_item(): - response = client.post( - "/item-list", - json=[ - {"title": "First Item", "size": 50, "sub": {"name": "First Sub"}}, - {"title": "Second Item", "size": 75, "sub": {"name": "Second Sub"}}, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "First Item", - "size": 50, - "description": None, - "sub": {"name": "First Sub"}, - "multi": [], - } - - -def test_list_to_item_empty(): - response = client.post( - "/item-list", - json=[], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_list_to_item_filter(): - response = client.post( - "/item-list-filter", - json=[ - { - "title": "First Item", - "size": 100, - "sub": {"name": "First Sub"}, - "multi": [{"name": "Multi1"}], - }, - {"title": "Second Item", "size": 200, "sub": {"name": "Second Sub"}}, - ], - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == { - "title": "First Item", - "size": 100, - "description": None, - "sub": {"name": "First Sub"}, - "multi": [{"name": "Multi1"}], - } - # Verify secret fields are filtered out - assert "secret_data" not in result - assert "internal_id" not in result - - -def test_list_to_item_filter_no_data(): - response = client.post("/item-list-filter", json=[]) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_list_to_list(): - input_items = [ - {"title": "Item 1", "size": 10, "sub": {"name": "Sub1"}}, - { - "title": "Item 2", - "size": 20, - "description": "Second item", - "sub": {"name": "Sub2"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - {"title": "Item 3", "size": 30, "sub": {"name": "Sub3"}}, - ] - response = client.post( - "/item-list-to-list", - json=input_items, - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 3 - assert result[0] == { - "title": "Item 1", - "size": 10, - "description": None, - "sub": {"name": "Sub1"}, - "multi": [], - } - assert result[1] == { - "title": "Item 2", - "size": 20, - "description": "Second item", - "sub": {"name": "Sub2"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - } - assert result[2] == { - "title": "Item 3", - "size": 30, - "description": None, - "sub": {"name": "Sub3"}, - "multi": [], - } - - -def test_list_to_list_filter(): - response = client.post( - "/item-list-to-list-filter", - json=[{"title": "Item 1", "size": 100, "sub": {"name": "Sub1"}}], - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 2 - for item in result: - assert item == { - "title": "Item 1", - "size": 100, - "description": None, - "sub": {"name": "Sub1"}, - "multi": [], - } - # Verify secret fields are filtered out - assert "secret_data" not in item - assert "internal_id" not in item - - -def test_list_to_list_filter_no_data(): - response = client.post( - "/item-list-to-list-filter", - json=[], - ) - assert response.status_code == 200, response.text - assert response.json() == [] - - -def test_list_validation_error(): - response = client.post( - "/item-list", - json=[ - {"title": "Valid Item", "size": 100, "sub": {"name": "Sub1"}}, - { - "title": "Invalid Item" - # Missing required fields: size and sub - }, - ], - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 2 - assert { - "loc": ["body", 1, "size"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - assert { - "loc": ["body", 1, "sub"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - - -def test_list_nested_validation_error(): - response = client.post( - "/item-list", - json=[ - {"title": "Item with bad sub", "size": 100, "sub": {"wrong_field": "value"}} - ], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", 0, "sub", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_list_type_validation_error(): - response = client.post( - "/item-list", - json=[{"title": "Item", "size": "not_a_number", "sub": {"name": "Sub"}}], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", 0, "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_invalid_list_structure(): - response = client.post( - "/item-list", - json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ] - } - ) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/item": { - "post": { - "summary": "Handle Item", - "operationId": "handle_item_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item Item Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-filter": { - "post": { - "summary": "Handle Item Filter", - "operationId": "handle_item_filter_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item Filter Item Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list": { - "post": { - "summary": "Handle Item List", - "operationId": "handle_item_list_item_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list-filter": { - "post": { - "summary": "Handle Item List Filter", - "operationId": "handle_item_list_filter_item_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list-to-list": { - "post": { - "summary": "Handle Item List To List", - "operationId": "handle_item_list_to_list_item_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item List To List Item List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list-to-list-filter": { - "post": { - "summary": "Handle Item List To List Filter", - "operationId": "handle_item_list_to_list_filter_item_list_to_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item List To List Filter Item List To List Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_mixed.py b/tests/test_pydantic_v1_v2_mixed.py deleted file mode 100644 index 895835a4c..000000000 --- a/tests/test_pydantic_v1_v2_mixed.py +++ /dev/null @@ -1,1408 +0,0 @@ -import sys -import warnings -from typing import Any, Union - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot -from pydantic import BaseModel as NewBaseModel - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -class NewSubItem(NewBaseModel): - new_sub_name: str - - -class NewItem(NewBaseModel): - new_title: str - new_size: int - new_description: Union[str, None] = None - new_sub: NewSubItem - new_multi: list[NewSubItem] = [] - - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/v1-to-v2/item") - def handle_v1_item_to_v2(data: Item) -> NewItem: - return NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) - - @app.post("/v1-to-v2/item-filter", response_model=NewItem) - def handle_v1_item_to_v2_filter(data: Item) -> Any: - result = { - "new_title": data.title, - "new_size": data.size, - "new_description": data.description, - "new_sub": { - "new_sub_name": data.sub.name, - "new_sub_secret": "sub_hidden", - }, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} - for s in data.multi - ], - "secret": "hidden_v1_to_v2", - } - return result - - @app.post("/v2-to-v1/item") - def handle_v2_item_to_v1(data: NewItem) -> Item: - return Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - - @app.post("/v2-to-v1/item-filter", response_model=Item) - def handle_v2_item_to_v1_filter(data: NewItem) -> Any: - result = { - "title": data.new_title, - "size": data.new_size, - "description": data.new_description, - "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} - for s in data.new_multi - ], - "secret": "hidden_v2_to_v1", - } - return result - - @app.post("/v1-to-v2/item-to-list") - def handle_v1_item_to_v2_list(data: Item) -> list[NewItem]: - converted = NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) - return [converted, converted] - - @app.post("/v1-to-v2/list-to-list") - def handle_v1_list_to_v2_list(data: list[Item]) -> list[NewItem]: - result = [] - for item in data: - result.append( - NewItem( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=NewSubItem(new_sub_name=item.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], - ) - ) - return result - - @app.post("/v1-to-v2/list-to-list-filter", response_model=list[NewItem]) - def handle_v1_list_to_v2_list_filter(data: list[Item]) -> Any: - result = [] - for item in data: - converted = { - "new_title": item.title, - "new_size": item.size, - "new_description": item.description, - "new_sub": { - "new_sub_name": item.sub.name, - "new_sub_secret": "sub_hidden", - }, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} - for s in item.multi - ], - "secret": "hidden_v2_to_v1", - } - result.append(converted) - return result - - @app.post("/v1-to-v2/list-to-item") - def handle_v1_list_to_v2_item(data: list[Item]) -> NewItem: - if data: - item = data[0] - return NewItem( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=NewSubItem(new_sub_name=item.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], - ) - return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name="")) - - @app.post("/v2-to-v1/item-to-list") - def handle_v2_item_to_v1_list(data: NewItem) -> list[Item]: - converted = Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - return [converted, converted] - - @app.post("/v2-to-v1/list-to-list") - def handle_v2_list_to_v1_list(data: list[NewItem]) -> list[Item]: - result = [] - for item in data: - result.append( - Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=SubItem(name=item.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], - ) - ) - return result - - @app.post("/v2-to-v1/list-to-list-filter", response_model=list[Item]) - def handle_v2_list_to_v1_list_filter(data: list[NewItem]) -> Any: - result = [] - for item in data: - converted = { - "title": item.new_title, - "size": item.new_size, - "description": item.new_description, - "sub": { - "name": item.new_sub.new_sub_name, - "sub_secret": "sub_hidden", - }, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} - for s in item.new_multi - ], - "secret": "hidden_v2_to_v1", - } - result.append(converted) - return result - - @app.post("/v2-to-v1/list-to-item") - def handle_v2_list_to_v1_item(data: list[NewItem]) -> Item: - if data: - item = data[0] - return Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=SubItem(name=item.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], - ) - return Item(title="", size=0, sub=SubItem(name="")) - - -client = TestClient(app) - - -def test_v1_to_v2_item(): - response = client.post( - "/v1-to-v2/item", - json={ - "title": "Old Item", - "size": 100, - "description": "V1 description", - "sub": {"name": "V1 Sub"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Old Item", - "new_size": 100, - "new_description": "V1 description", - "new_sub": {"new_sub_name": "V1 Sub"}, - "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], - } - - -def test_v1_to_v2_item_minimal(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}}, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Minimal", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "MinSub"}, - "new_multi": [], - } - - -def test_v1_to_v2_item_filter(): - response = client.post( - "/v1-to-v2/item-filter", - json={ - "title": "Filtered Item", - "size": 50, - "sub": {"name": "Sub"}, - "multi": [{"name": "Multi1"}], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == snapshot( - { - "new_title": "Filtered Item", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "Sub"}, - "new_multi": [{"new_sub_name": "Multi1"}], - } - ) - # Verify secret fields are filtered out - assert "secret" not in result - assert "new_sub_secret" not in result["new_sub"] - assert "new_sub_secret" not in result["new_multi"][0] - - -def test_v2_to_v1_item(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "New Item", - "new_size": 200, - "new_description": "V2 description", - "new_sub": {"new_sub_name": "V2 Sub"}, - "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "New Item", - "size": 200, - "description": "V2 description", - "sub": {"name": "V2 Sub"}, - "multi": [{"name": "N1"}, {"name": "N2"}], - } - - -def test_v2_to_v1_item_minimal(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "MinimalNew", - "new_size": 75, - "new_sub": {"new_sub_name": "MinNewSub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "MinimalNew", - "size": 75, - "description": None, - "sub": {"name": "MinNewSub"}, - "multi": [], - } - - -def test_v2_to_v1_item_filter(): - response = client.post( - "/v2-to-v1/item-filter", - json={ - "new_title": "Filtered New", - "new_size": 75, - "new_sub": {"new_sub_name": "NewSub"}, - "new_multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == snapshot( - { - "title": "Filtered New", - "size": 75, - "description": None, - "sub": {"name": "NewSub"}, - "multi": [], - } - ) - # Verify secret fields are filtered out - assert "secret" not in result - assert "sub_secret" not in result["sub"] - - -def test_v1_item_to_v2_list(): - response = client.post( - "/v1-to-v2/item-to-list", - json={ - "title": "Single to List", - "size": 150, - "description": "Convert to list", - "sub": {"name": "Sub1"}, - "multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == [ - { - "new_title": "Single to List", - "new_size": 150, - "new_description": "Convert to list", - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - { - "new_title": "Single to List", - "new_size": 150, - "new_description": "Convert to list", - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - ] - - -def test_v1_list_to_v2_list(): - response = client.post( - "/v1-to-v2/list-to-list", - json=[ - {"title": "Item1", "size": 10, "sub": {"name": "Sub1"}}, - { - "title": "Item2", - "size": 20, - "description": "Second item", - "sub": {"name": "Sub2"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - {"title": "Item3", "size": 30, "sub": {"name": "Sub3"}}, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == [ - { - "new_title": "Item1", - "new_size": 10, - "new_description": None, - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - { - "new_title": "Item2", - "new_size": 20, - "new_description": "Second item", - "new_sub": {"new_sub_name": "Sub2"}, - "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], - }, - { - "new_title": "Item3", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "Sub3"}, - "new_multi": [], - }, - ] - - -def test_v1_list_to_v2_list_filter(): - response = client.post( - "/v1-to-v2/list-to-list-filter", - json=[{"title": "FilterMe", "size": 30, "sub": {"name": "SubF"}}], - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == snapshot( - [ - { - "new_title": "FilterMe", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "SubF"}, - "new_multi": [], - } - ] - ) - # Verify secret fields are filtered out - assert "secret" not in result[0] - assert "new_sub_secret" not in result[0]["new_sub"] - - -def test_v1_list_to_v2_item(): - response = client.post( - "/v1-to-v2/list-to-item", - json=[ - {"title": "First", "size": 100, "sub": {"name": "FirstSub"}}, - {"title": "Second", "size": 200, "sub": {"name": "SecondSub"}}, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "First", - "new_size": 100, - "new_description": None, - "new_sub": {"new_sub_name": "FirstSub"}, - "new_multi": [], - } - - -def test_v1_list_to_v2_item_empty(): - response = client.post("/v1-to-v2/list-to-item", json=[]) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "", - "new_size": 0, - "new_description": None, - "new_sub": {"new_sub_name": ""}, - "new_multi": [], - } - - -def test_v2_item_to_v1_list(): - response = client.post( - "/v2-to-v1/item-to-list", - json={ - "new_title": "Single New", - "new_size": 250, - "new_description": "New to list", - "new_sub": {"new_sub_name": "NewSub"}, - "new_multi": [], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [ - { - "title": "Single New", - "size": 250, - "description": "New to list", - "sub": {"name": "NewSub"}, - "multi": [], - }, - { - "title": "Single New", - "size": 250, - "description": "New to list", - "sub": {"name": "NewSub"}, - "multi": [], - }, - ] - - -def test_v2_list_to_v1_list(): - response = client.post( - "/v2-to-v1/list-to-list", - json=[ - {"new_title": "New1", "new_size": 15, "new_sub": {"new_sub_name": "NS1"}}, - { - "new_title": "New2", - "new_size": 25, - "new_description": "Second new", - "new_sub": {"new_sub_name": "NS2"}, - "new_multi": [{"new_sub_name": "NM1"}], - }, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == [ - { - "title": "New1", - "size": 15, - "description": None, - "sub": {"name": "NS1"}, - "multi": [], - }, - { - "title": "New2", - "size": 25, - "description": "Second new", - "sub": {"name": "NS2"}, - "multi": [{"name": "NM1"}], - }, - ] - - -def test_v2_list_to_v1_list_filter(): - response = client.post( - "/v2-to-v1/list-to-list-filter", - json=[ - { - "new_title": "FilterNew", - "new_size": 35, - "new_sub": {"new_sub_name": "NSF"}, - } - ], - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == snapshot( - [ - { - "title": "FilterNew", - "size": 35, - "description": None, - "sub": {"name": "NSF"}, - "multi": [], - } - ] - ) - # Verify secret fields are filtered out - assert "secret" not in result[0] - assert "sub_secret" not in result[0]["sub"] - - -def test_v2_list_to_v1_item(): - response = client.post( - "/v2-to-v1/list-to-item", - json=[ - { - "new_title": "FirstNew", - "new_size": 300, - "new_sub": {"new_sub_name": "FNS"}, - }, - { - "new_title": "SecondNew", - "new_size": 400, - "new_sub": {"new_sub_name": "SNS"}, - }, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "FirstNew", - "size": 300, - "description": None, - "sub": {"name": "FNS"}, - "multi": [], - } - - -def test_v2_list_to_v1_item_empty(): - response = client.post("/v2-to-v1/list-to-item", json=[]) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_v1_to_v2_validation_error(): - response = client.post("/v1-to-v2/item", json={"title": "Missing fields"}) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_v1_to_v2_nested_validation_error(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "sub", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_v1_to_v2_type_validation_error(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_v2_to_v1_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={"new_title": "Missing fields"}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "new_size"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - { - "type": "missing", - "loc": ["body", "new_sub"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - ] - } - ) - - -def test_v2_to_v1_nested_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Bad sub", - "new_size": 200, - "new_sub": {"wrong_field": "value"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "Field required", - "input": {"wrong_field": "value"}, - } - ] - } - ) - - -def test_v1_list_validation_error(): - response = client.post( - "/v1-to-v2/list-to-list", - json=[ - {"title": "Valid", "size": 10, "sub": {"name": "S"}}, - {"title": "Invalid"}, - ], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", 1, "size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 1, "sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_v2_list_validation_error(): - response = client.post( - "/v2-to-v1/list-to-list", - json=[ - {"new_title": "Valid", "new_size": 10, "new_sub": {"new_sub_name": "NS"}}, - {"new_title": "Invalid"}, - ], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "missing", - "loc": ["body", 1, "new_size"], - "msg": "Field required", - "input": {"new_title": "Invalid"}, - }, - { - "type": "missing", - "loc": ["body", 1, "new_sub"], - "msg": "Field required", - "input": {"new_title": "Invalid"}, - }, - ] - } - ) - - -def test_invalid_list_structure_v1(): - response = client.post( - "/v1-to-v2/list-to-list", - json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ] - } - ) - - -def test_invalid_list_structure_v2(): - response = client.post( - "/v2-to-v1/list-to-list", - json={ - "new_title": "Not a list", - "new_size": 100, - "new_sub": {"new_sub_name": "Sub"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "list_type", - "loc": ["body"], - "msg": "Input should be a valid list", - "input": { - "new_title": "Not a list", - "new_size": 100, - "new_sub": {"new_sub_name": "Sub"}, - }, - } - ] - } - ) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/v1-to-v2/item": { - "post": { - "summary": "Handle V1 Item To V2", - "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-filter": { - "post": { - "summary": "Handle V1 Item To V2 Filter", - "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item": { - "post": { - "summary": "Handle V2 Item To V1", - "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-filter": { - "post": { - "summary": "Handle V2 Item To V1 Filter", - "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-to-list": { - "post": { - "summary": "Handle V1 Item To V2 List", - "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-list": { - "post": { - "summary": "Handle V1 List To V2 List", - "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-list-filter": { - "post": { - "summary": "Handle V1 List To V2 List Filter", - "operationId": "handle_v1_list_to_v2_list_filter_v1_to_v2_list_to_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Response Handle V1 List To V2 List Filter V1 To V2 List To List Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-item": { - "post": { - "summary": "Handle V1 List To V2 Item", - "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-to-list": { - "post": { - "summary": "Handle V2 Item To V1 List", - "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-list": { - "post": { - "summary": "Handle V2 List To V1 List", - "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-list-filter": { - "post": { - "summary": "Handle V2 List To V1 List Filter", - "operationId": "handle_v2_list_to_v1_list_filter_v2_to_v1_list_to_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle V2 List To V1 List Filter V2 To V1 List To List Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-item": { - "post": { - "summary": "Handle V2 List To V1 Item", - "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "NewItem": { - "properties": { - "new_title": {"type": "string", "title": "New Title"}, - "new_size": {"type": "integer", "title": "New Size"}, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, - "new_multi": { - "items": {"$ref": "#/components/schemas/NewSubItem"}, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "NewItem", - }, - "NewSubItem": { - "properties": { - "new_sub_name": {"type": "string", "title": "New Sub Name"} - }, - "type": "object", - "required": ["new_sub_name"], - "title": "NewSubItem", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_multifile/__init__.py b/tests/test_pydantic_v1_v2_multifile/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_pydantic_v1_v2_multifile/main.py b/tests/test_pydantic_v1_v2_multifile/main.py deleted file mode 100644 index 4180ec3bf..000000000 --- a/tests/test_pydantic_v1_v2_multifile/main.py +++ /dev/null @@ -1,137 +0,0 @@ -import warnings - -from fastapi import FastAPI - -from . import modelsv1, modelsv2, modelsv2b - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/v1-to-v2/item") - def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item: - return modelsv2.Item( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], - ) - - @app.post("/v2-to-v1/item") - def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item: - return modelsv1.Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - - @app.post("/v1-to-v2/item-to-list") - def handle_v1_item_to_v2_list(data: modelsv1.Item) -> list[modelsv2.Item]: - converted = modelsv2.Item( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], - ) - return [converted, converted] - - @app.post("/v1-to-v2/list-to-list") - def handle_v1_list_to_v2_list(data: list[modelsv1.Item]) -> list[modelsv2.Item]: - result = [] - for item in data: - result.append( - modelsv2.Item( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), - new_multi=[ - modelsv2.SubItem(new_sub_name=s.name) for s in item.multi - ], - ) - ) - return result - - @app.post("/v1-to-v2/list-to-item") - def handle_v1_list_to_v2_item(data: list[modelsv1.Item]) -> modelsv2.Item: - if data: - item = data[0] - return modelsv2.Item( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi], - ) - return modelsv2.Item( - new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="") - ) - - @app.post("/v2-to-v1/item-to-list") - def handle_v2_item_to_v1_list(data: modelsv2.Item) -> list[modelsv1.Item]: - converted = modelsv1.Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - return [converted, converted] - - @app.post("/v2-to-v1/list-to-list") - def handle_v2_list_to_v1_list(data: list[modelsv2.Item]) -> list[modelsv1.Item]: - result = [] - for item in data: - result.append( - modelsv1.Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), - multi=[ - modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi - ], - ) - ) - return result - - @app.post("/v2-to-v1/list-to-item") - def handle_v2_list_to_v1_item(data: list[modelsv2.Item]) -> modelsv1.Item: - if data: - item = data[0] - return modelsv1.Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi], - ) - return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name="")) - - @app.post("/v2-to-v1/same-name") - def handle_v2_same_name_to_v1( - item1: modelsv2.Item, item2: modelsv2b.Item - ) -> modelsv1.Item: - return modelsv1.Item( - title=item1.new_title, - size=item2.dup_size, - description=item1.new_description, - sub=modelsv1.SubItem(name=item1.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi], - ) - - @app.post("/v2-to-v1/list-of-items-to-list-of-items") - def handle_v2_items_in_list_to_v1_item_in_list( - data1: list[modelsv2.ItemInList], data2: list[modelsv2b.ItemInList] - ) -> list[modelsv1.ItemInList]: - item1 = data1[0] - item2 = data2[0] - return [ - modelsv1.ItemInList(name1=item1.name2), - modelsv1.ItemInList(name1=item2.dup_name2), - ] diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv1.py b/tests/test_pydantic_v1_v2_multifile/modelsv1.py deleted file mode 100644 index 0cc8de455..000000000 --- a/tests/test_pydantic_v1_v2_multifile/modelsv1.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Union - -from fastapi._compat.v1 import BaseModel - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -class ItemInList(BaseModel): - name1: str diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2.py b/tests/test_pydantic_v1_v2_multifile/modelsv2.py deleted file mode 100644 index d80b77e10..000000000 --- a/tests/test_pydantic_v1_v2_multifile/modelsv2.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Union - -from pydantic import BaseModel - - -class SubItem(BaseModel): - new_sub_name: str - - -class Item(BaseModel): - new_title: str - new_size: int - new_description: Union[str, None] = None - new_sub: SubItem - new_multi: list[SubItem] = [] - - -class ItemInList(BaseModel): - name2: str diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2b.py b/tests/test_pydantic_v1_v2_multifile/modelsv2b.py deleted file mode 100644 index e992bea2e..000000000 --- a/tests/test_pydantic_v1_v2_multifile/modelsv2b.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Union - -from pydantic import BaseModel - - -class SubItem(BaseModel): - dup_sub_name: str - - -class Item(BaseModel): - dup_title: str - dup_size: int - dup_description: Union[str, None] = None - dup_sub: SubItem - dup_multi: list[SubItem] = [] - - -class ItemInList(BaseModel): - dup_name2: str diff --git a/tests/test_pydantic_v1_v2_multifile/test_multifile.py b/tests/test_pydantic_v1_v2_multifile/test_multifile.py deleted file mode 100644 index 32d901961..000000000 --- a/tests/test_pydantic_v1_v2_multifile/test_multifile.py +++ /dev/null @@ -1,951 +0,0 @@ -import sys - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - -from .main import app - -client = TestClient(app) - - -def test_v1_to_v2_item(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Test", "size": 10, "sub": {"name": "SubTest"}}, - ) - assert response.status_code == 200 - assert response.json() == { - "new_title": "Test", - "new_size": 10, - "new_description": None, - "new_sub": {"new_sub_name": "SubTest"}, - "new_multi": [], - } - - -def test_v2_to_v1_item(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "NewTest", - "new_size": 20, - "new_sub": {"new_sub_name": "NewSubTest"}, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "title": "NewTest", - "size": 20, - "description": None, - "sub": {"name": "NewSubTest"}, - "multi": [], - } - - -def test_v1_to_v2_item_to_list(): - response = client.post( - "/v1-to-v2/item-to-list", - json={"title": "ListTest", "size": 30, "sub": {"name": "SubListTest"}}, - ) - assert response.status_code == 200 - assert response.json() == [ - { - "new_title": "ListTest", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "SubListTest"}, - "new_multi": [], - }, - { - "new_title": "ListTest", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "SubListTest"}, - "new_multi": [], - }, - ] - - -def test_v1_to_v2_list_to_list(): - response = client.post( - "/v1-to-v2/list-to-list", - json=[ - {"title": "Item1", "size": 40, "sub": {"name": "Sub1"}}, - {"title": "Item2", "size": 50, "sub": {"name": "Sub2"}}, - ], - ) - assert response.status_code == 200 - assert response.json() == [ - { - "new_title": "Item1", - "new_size": 40, - "new_description": None, - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - { - "new_title": "Item2", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "Sub2"}, - "new_multi": [], - }, - ] - - -def test_v1_to_v2_list_to_item(): - response = client.post( - "/v1-to-v2/list-to-item", - json=[ - {"title": "FirstItem", "size": 60, "sub": {"name": "FirstSub"}}, - {"title": "SecondItem", "size": 70, "sub": {"name": "SecondSub"}}, - ], - ) - assert response.status_code == 200 - assert response.json() == { - "new_title": "FirstItem", - "new_size": 60, - "new_description": None, - "new_sub": {"new_sub_name": "FirstSub"}, - "new_multi": [], - } - - -def test_v2_to_v1_item_to_list(): - response = client.post( - "/v2-to-v1/item-to-list", - json={ - "new_title": "ListNew", - "new_size": 80, - "new_sub": {"new_sub_name": "SubListNew"}, - }, - ) - assert response.status_code == 200 - assert response.json() == [ - { - "title": "ListNew", - "size": 80, - "description": None, - "sub": {"name": "SubListNew"}, - "multi": [], - }, - { - "title": "ListNew", - "size": 80, - "description": None, - "sub": {"name": "SubListNew"}, - "multi": [], - }, - ] - - -def test_v2_to_v1_list_to_list(): - response = client.post( - "/v2-to-v1/list-to-list", - json=[ - { - "new_title": "New1", - "new_size": 90, - "new_sub": {"new_sub_name": "NewSub1"}, - }, - { - "new_title": "New2", - "new_size": 100, - "new_sub": {"new_sub_name": "NewSub2"}, - }, - ], - ) - assert response.status_code == 200 - assert response.json() == [ - { - "title": "New1", - "size": 90, - "description": None, - "sub": {"name": "NewSub1"}, - "multi": [], - }, - { - "title": "New2", - "size": 100, - "description": None, - "sub": {"name": "NewSub2"}, - "multi": [], - }, - ] - - -def test_v2_to_v1_list_to_item(): - response = client.post( - "/v2-to-v1/list-to-item", - json=[ - { - "new_title": "FirstNew", - "new_size": 110, - "new_sub": {"new_sub_name": "FirstNewSub"}, - }, - { - "new_title": "SecondNew", - "new_size": 120, - "new_sub": {"new_sub_name": "SecondNewSub"}, - }, - ], - ) - assert response.status_code == 200 - assert response.json() == { - "title": "FirstNew", - "size": 110, - "description": None, - "sub": {"name": "FirstNewSub"}, - "multi": [], - } - - -def test_v1_to_v2_list_to_item_empty(): - response = client.post("/v1-to-v2/list-to-item", json=[]) - assert response.status_code == 200 - assert response.json() == { - "new_title": "", - "new_size": 0, - "new_description": None, - "new_sub": {"new_sub_name": ""}, - "new_multi": [], - } - - -def test_v2_to_v1_list_to_item_empty(): - response = client.post("/v2-to-v1/list-to-item", json=[]) - assert response.status_code == 200 - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_v2_same_name_to_v1(): - response = client.post( - "/v2-to-v1/same-name", - json={ - "item1": { - "new_title": "Title1", - "new_size": 100, - "new_description": "Description1", - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [{"new_sub_name": "Multi1"}], - }, - "item2": { - "dup_title": "Title2", - "dup_size": 200, - "dup_description": "Description2", - "dup_sub": {"dup_sub_name": "Sub2"}, - "dup_multi": [ - {"dup_sub_name": "Multi2a"}, - {"dup_sub_name": "Multi2b"}, - ], - }, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "title": "Title1", - "size": 200, - "description": "Description1", - "sub": {"name": "Sub1"}, - "multi": [{"name": "Multi2a"}, {"name": "Multi2b"}], - } - - -def test_v2_items_in_list_to_v1_item_in_list(): - response = client.post( - "/v2-to-v1/list-of-items-to-list-of-items", - json={ - "data1": [{"name2": "Item1"}, {"name2": "Item2"}], - "data2": [{"dup_name2": "Item3"}, {"dup_name2": "Item4"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [ - {"name1": "Item1"}, - {"name1": "Item3"}, - ] - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/v1-to-v2/item": { - "post": { - "summary": "Handle V1 Item To V2", - "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item": { - "post": { - "summary": "Handle V2 Item To V1", - "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-to-list": { - "post": { - "summary": "Handle V1 Item To V2 List", - "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - }, - "type": "array", - "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-list": { - "post": { - "summary": "Handle V1 List To V2 List", - "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - }, - "type": "array", - "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-item": { - "post": { - "summary": "Handle V1 List To V2 Item", - "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-to-list": { - "post": { - "summary": "Handle V2 Item To V1 List", - "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-list": { - "post": { - "summary": "Handle V2 List To V1 List", - "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-item": { - "post": { - "summary": "Handle V2 List To V1 Item", - "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/same-name": { - "post": { - "summary": "Handle V2 Same Name To V1", - "operationId": "handle_v2_same_name_to_v1_v2_to_v1_same_name_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-of-items-to-list-of-items": { - "post": { - "summary": "Handle V2 Items In List To V1 Item In List", - "operationId": "handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList" - }, - "type": "array", - "title": "Response Handle V2 Items In List To V1 Item In List V2 To V1 List Of Items To List Of Items Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": { - "properties": { - "data1": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList" - }, - "type": "array", - "title": "Data1", - }, - "data2": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList" - }, - "type": "array", - "title": "Data2", - }, - }, - "type": "object", - "required": ["data1", "data2"], - "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", - }, - "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": { - "properties": { - "item1": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - "item2": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item" - }, - }, - "type": "object", - "required": ["item1", "item2"], - "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - {"type": "string"}, - {"type": "integer"}, - ] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": { - "type": "string", - "title": "Description", - }, - "sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": { - "properties": {"name1": {"type": "string", "title": "Name1"}}, - "type": "object", - "required": ["name1"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": { - "properties": { - "new_title": { - "type": "string", - "title": "New Title", - }, - "new_size": { - "type": "integer", - "title": "New Size", - }, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "new_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input": { - "properties": { - "new_title": { - "type": "string", - "title": "New Title", - }, - "new_size": { - "type": "integer", - "title": "New Size", - }, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "new_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": { - "properties": {"name2": {"type": "string", "title": "Name2"}}, - "type": "object", - "required": ["name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": { - "properties": { - "new_sub_name": { - "type": "string", - "title": "New Sub Name", - } - }, - "type": "object", - "required": ["new_sub_name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": { - "properties": { - "dup_title": { - "type": "string", - "title": "Dup Title", - }, - "dup_size": { - "type": "integer", - "title": "Dup Size", - }, - "dup_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Dup Description", - }, - "dup_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "dup_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "type": "array", - "title": "Dup Multi", - "default": [], - }, - }, - "type": "object", - "required": ["dup_title", "dup_size", "dup_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": { - "properties": { - "dup_name2": { - "type": "string", - "title": "Dup Name2", - } - }, - "type": "object", - "required": ["dup_name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": { - "properties": { - "dup_sub_name": { - "type": "string", - "title": "Dup Sub Name", - } - }, - "type": "object", - "required": ["dup_sub_name"], - "title": "SubItem", - }, - }, - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_noneable.py b/tests/test_pydantic_v1_v2_noneable.py deleted file mode 100644 index ba98b5653..000000000 --- a/tests/test_pydantic_v1_v2_noneable.py +++ /dev/null @@ -1,692 +0,0 @@ -import sys -import warnings -from typing import Any, Union - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot -from pydantic import BaseModel as NewBaseModel - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -class NewSubItem(NewBaseModel): - new_sub_name: str - - -class NewItem(NewBaseModel): - new_title: str - new_size: int - new_description: Union[str, None] = None - new_sub: NewSubItem - new_multi: list[NewSubItem] = [] - - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/v1-to-v2/") - def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]: - if data.size < 0: - return None - return NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) - - @app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None]) - def handle_v1_item_to_v2_filter(data: Item) -> Any: - if data.size < 0: - return None - result = { - "new_title": data.title, - "new_size": data.size, - "new_description": data.description, - "new_sub": { - "new_sub_name": data.sub.name, - "new_sub_secret": "sub_hidden", - }, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} - for s in data.multi - ], - "secret": "hidden_v1_to_v2", - } - return result - - @app.post("/v2-to-v1/item") - def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]: - if data.new_size < 0: - return None - return Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - - @app.post("/v2-to-v1/item-filter", response_model=Union[Item, None]) - def handle_v2_item_to_v1_filter(data: NewItem) -> Any: - if data.new_size < 0: - return None - result = { - "title": data.new_title, - "size": data.new_size, - "description": data.new_description, - "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} - for s in data.new_multi - ], - "secret": "hidden_v2_to_v1", - } - return result - - -client = TestClient(app) - - -def test_v1_to_v2_item_success(): - response = client.post( - "/v1-to-v2/", - json={ - "title": "Old Item", - "size": 100, - "description": "V1 description", - "sub": {"name": "V1 Sub"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Old Item", - "new_size": 100, - "new_description": "V1 description", - "new_sub": {"new_sub_name": "V1 Sub"}, - "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], - } - - -def test_v1_to_v2_item_returns_none(): - response = client.post( - "/v1-to-v2/", - json={"title": "Invalid Item", "size": -10, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v1_to_v2_item_minimal(): - response = client.post( - "/v1-to-v2/", json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Minimal", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "MinSub"}, - "new_multi": [], - } - - -def test_v1_to_v2_item_filter_success(): - response = client.post( - "/v1-to-v2/item-filter", - json={ - "title": "Filtered Item", - "size": 50, - "sub": {"name": "Sub"}, - "multi": [{"name": "Multi1"}], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result["new_title"] == "Filtered Item" - assert result["new_size"] == 50 - assert result["new_sub"]["new_sub_name"] == "Sub" - assert result["new_multi"][0]["new_sub_name"] == "Multi1" - # Verify secret fields are filtered out - assert "secret" not in result - assert "new_sub_secret" not in result["new_sub"] - assert "new_sub_secret" not in result["new_multi"][0] - - -def test_v1_to_v2_item_filter_returns_none(): - response = client.post( - "/v1-to-v2/item-filter", - json={"title": "Invalid", "size": -1, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v2_to_v1_item_success(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "New Item", - "new_size": 200, - "new_description": "V2 description", - "new_sub": {"new_sub_name": "V2 Sub"}, - "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "New Item", - "size": 200, - "description": "V2 description", - "sub": {"name": "V2 Sub"}, - "multi": [{"name": "N1"}, {"name": "N2"}], - } - - -def test_v2_to_v1_item_returns_none(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Invalid New", - "new_size": -5, - "new_sub": {"new_sub_name": "NewSub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v2_to_v1_item_minimal(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "MinimalNew", - "new_size": 75, - "new_sub": {"new_sub_name": "MinNewSub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "MinimalNew", - "size": 75, - "description": None, - "sub": {"name": "MinNewSub"}, - "multi": [], - } - - -def test_v2_to_v1_item_filter_success(): - response = client.post( - "/v2-to-v1/item-filter", - json={ - "new_title": "Filtered New", - "new_size": 75, - "new_sub": {"new_sub_name": "NewSub"}, - "new_multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result["title"] == "Filtered New" - assert result["size"] == 75 - assert result["sub"]["name"] == "NewSub" - # Verify secret fields are filtered out - assert "secret" not in result - assert "sub_secret" not in result["sub"] - - -def test_v2_to_v1_item_filter_returns_none(): - response = client.post( - "/v2-to-v1/item-filter", - json={ - "new_title": "Invalid Filtered", - "new_size": -100, - "new_sub": {"new_sub_name": "Sub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v1_to_v2_validation_error(): - response = client.post("/v1-to-v2/", json={"title": "Missing fields"}) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_v1_to_v2_nested_validation_error(): - response = client.post( - "/v1-to-v2/", - json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}}, - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 1 - assert error_detail[0]["loc"] == ["body", "sub", "name"] - - -def test_v1_to_v2_type_validation_error(): - response = client.post( - "/v1-to-v2/", - json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 1 - assert error_detail[0]["loc"] == ["body", "size"] - - -def test_v2_to_v1_validation_error(): - response = client.post("/v2-to-v1/item", json={"new_title": "Missing fields"}) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "new_size"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - { - "type": "missing", - "loc": ["body", "new_sub"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - ] - } - ) - - -def test_v2_to_v1_nested_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Bad sub", - "new_size": 200, - "new_sub": {"wrong_field": "value"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "Field required", - "input": {"wrong_field": "value"}, - } - ] - } - ) - - -def test_v2_to_v1_type_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Bad type", - "new_size": "not_a_number", - "new_sub": {"new_sub_name": "Sub"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "new_size"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "not_a_number", - } - ] - } - ) - - -def test_v1_to_v2_with_multi_items(): - response = client.post( - "/v1-to-v2/", - json={ - "title": "Complex Item", - "size": 300, - "description": "Item with multiple sub-items", - "sub": {"name": "Main Sub"}, - "multi": [{"name": "Sub1"}, {"name": "Sub2"}, {"name": "Sub3"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "new_title": "Complex Item", - "new_size": 300, - "new_description": "Item with multiple sub-items", - "new_sub": {"new_sub_name": "Main Sub"}, - "new_multi": [ - {"new_sub_name": "Sub1"}, - {"new_sub_name": "Sub2"}, - {"new_sub_name": "Sub3"}, - ], - } - ) - - -def test_v2_to_v1_with_multi_items(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Complex New Item", - "new_size": 400, - "new_description": "New item with multiple sub-items", - "new_sub": {"new_sub_name": "Main New Sub"}, - "new_multi": [{"new_sub_name": "NewSub1"}, {"new_sub_name": "NewSub2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "title": "Complex New Item", - "size": 400, - "description": "New item with multiple sub-items", - "sub": {"name": "Main New Sub"}, - "multi": [{"name": "NewSub1"}, {"name": "NewSub2"}], - } - ) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/v1-to-v2/": { - "post": { - "summary": "Handle V1 Item To V2", - "operationId": "handle_v1_item_to_v2_v1_to_v2__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/NewItem" - }, - {"type": "null"}, - ], - "title": "Response Handle V1 Item To V2 V1 To V2 Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-filter": { - "post": { - "summary": "Handle V1 Item To V2 Filter", - "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/NewItem" - }, - {"type": "null"}, - ], - "title": "Response Handle V1 Item To V2 Filter V1 To V2 Item Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item": { - "post": { - "summary": "Handle V2 Item To V1", - "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-filter": { - "post": { - "summary": "Handle V2 Item To V1 Filter", - "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "NewItem": { - "properties": { - "new_title": {"type": "string", "title": "New Title"}, - "new_size": {"type": "integer", "title": "New Size"}, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, - "new_multi": { - "items": {"$ref": "#/components/schemas/NewSubItem"}, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "NewItem", - }, - "NewSubItem": { - "properties": { - "new_sub_name": {"type": "string", "title": "New Sub Name"} - }, - "type": "object", - "required": ["new_sub_name"], - "title": "NewSubItem", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_read_with_orm_mode.py b/tests/test_read_with_orm_mode.py index a195634b8..cd7389252 100644 --- a/tests/test_read_with_orm_mode.py +++ b/tests/test_read_with_orm_mode.py @@ -1,12 +1,9 @@ -import warnings from typing import Any from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict -from .utils import needs_pydanticv1 - def test_read_with_orm_mode() -> None: class PersonBase(BaseModel): @@ -44,50 +41,3 @@ def test_read_with_orm_mode() -> None: assert data["name"] == person_data["name"] assert data["lastname"] == person_data["lastname"] assert data["full_name"] == person_data["name"] + " " + person_data["lastname"] - - -@needs_pydanticv1 -def test_read_with_orm_mode_pv1() -> None: - from pydantic import v1 - - class PersonBase(v1.BaseModel): - name: str - lastname: str - - class Person(PersonBase): - @property - def full_name(self) -> str: - return f"{self.name} {self.lastname}" - - class Config: - orm_mode = True - read_with_orm_mode = True - - class PersonCreate(PersonBase): - pass - - class PersonRead(PersonBase): - full_name: str - - class Config: - orm_mode = True - - app = FastAPI() - - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/people/", response_model=PersonRead) - def create_person(person: PersonCreate) -> Any: - db_person = Person.from_orm(person) - return db_person - - client = TestClient(app) - - person_data = {"name": "Dive", "lastname": "Wilson"} - response = client.post("/people/", json=person_data) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == person_data["name"] - assert data["lastname"] == person_data["lastname"] - assert data["full_name"] == person_data["name"] + " " + person_data["lastname"] diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 9e527d6a0..58fba89f1 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -1,4 +1,3 @@ -import warnings from typing import Union import pytest @@ -8,8 +7,6 @@ from fastapi.responses import JSONResponse, Response from fastapi.testclient import TestClient from pydantic import BaseModel -from tests.utils import needs_pydanticv1 - class BaseUser(BaseModel): name: str @@ -512,29 +509,6 @@ def test_invalid_response_model_field(): assert "parameter response_model=None" in e.value.args[0] -# TODO: remove when dropping Pydantic v1 support -@needs_pydanticv1 -def test_invalid_response_model_field_pv1(): - from fastapi._compat import v1 - - app = FastAPI() - - class Model(v1.BaseModel): - foo: str - - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - with pytest.raises(FastAPIError) as e: - - @app.get("/") - def read_root() -> Union[Response, Model, None]: - return Response(content="Foo") # pragma: no cover - - assert "valid Pydantic field type" in e.value.args[0] - assert "parameter response_model=None" in e.value.args[0] - - def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py deleted file mode 100644 index 62b67a98c..000000000 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py +++ /dev/null @@ -1,115 +0,0 @@ -import importlib - -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_pydanticv1 - - -@pytest.fixture( - name="client", - params=[ - pytest.param("tutorial007_pv1_py39"), - ], -) -def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module( - f"docs_src.path_operation_advanced_configuration.{request.param}" - ) - - client = TestClient(mod.app) - return client - - -@needs_pydanticv1 -def test_post(client: TestClient): - yaml_data = """ - name: Deadpoolio - tags: - - x-force - - x-men - - x-avengers - """ - response = client.post("/items/", content=yaml_data) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Deadpoolio", - "tags": ["x-force", "x-men", "x-avengers"], - } - - -@needs_pydanticv1 -def test_post_broken_yaml(client: TestClient): - yaml_data = """ - name: Deadpoolio - tags: - x - x-force - x - x-men - x - x-avengers - """ - response = client.post("/items/", content=yaml_data) - assert response.status_code == 422, response.text - assert response.json() == {"detail": "Invalid YAML"} - - -@needs_pydanticv1 -def test_post_invalid(client: TestClient): - yaml_data = """ - name: Deadpoolio - tags: - - x-force - - x-men - - x-avengers - - sneaky: object - """ - response = client.post("/items/", content=yaml_data) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"} - ] - } - - -@needs_pydanticv1 -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/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/x-yaml": { - "schema": { - "title": "Item", - "required": ["name", "tags"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - }, - }, - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, - } diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py b/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py deleted file mode 100644 index 4090eba01..000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys -from typing import Any - -import pytest - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -import importlib - -from ...utils import needs_py310 - - -@pytest.fixture( - name="mod", - params=[ - "tutorial001_an_py39", - pytest.param("tutorial001_an_py310", marks=needs_py310), - ], -) -def get_mod(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - return mod - - -def test_model(mod: Any): - item = mod.Item(name="Foo", size=3.4) - assert item.dict() == {"name": "Foo", "description": None, "size": 3.4} diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py deleted file mode 100644 index 9d1baf853..000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py +++ /dev/null @@ -1,143 +0,0 @@ -import sys -import warnings - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning -from inline_snapshot import snapshot - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -import importlib - -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture( - name="client", - params=[ - "tutorial002_an_py39", - pytest.param("tutorial002_an_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - - c = TestClient(mod.app) - return c - - -def test_call(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "size": 3.4}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "size": 3.4, - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py deleted file mode 100644 index 23b236888..000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py +++ /dev/null @@ -1,158 +0,0 @@ -import sys -import warnings - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning -from inline_snapshot import snapshot - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -import importlib - -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture( - name="client", - params=[ - "tutorial003_an_py39", - pytest.param("tutorial003_an_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - - c = TestClient(mod.app) - return c - - -def test_call(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "size": 3.4}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "size": 3.4, - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ItemV2" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "Item", - }, - "ItemV2": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "ItemV2", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py deleted file mode 100644 index 61c0f6357..000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py +++ /dev/null @@ -1,156 +0,0 @@ -import sys -import warnings - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning -from inline_snapshot import snapshot - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -import importlib - -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture( - name="client", - params=[ - pytest.param("tutorial004_an_py39"), - pytest.param("tutorial004_an_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - - c = TestClient(mod.app) - return c - - -def test_call(client: TestClient): - response = client.post("/items/", json={"item": {"name": "Foo", "size": 3.4}}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "size": 3.4, - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_item_items__post" - } - ], - "title": "Body", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "Body_create_item_items__post": { - "properties": { - "item": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - } - }, - "type": "object", - "required": ["item"], - "title": "Body_create_item_items__post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py deleted file mode 100644 index 50be45896..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py +++ /dev/null @@ -1,128 +0,0 @@ -import importlib -import warnings - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning -from fastapi.testclient import TestClient - -from ...utils import needs_pydanticv1 - - -@pytest.fixture( - name="client", - params=[ - "tutorial002_pv1_py39", - "tutorial002_pv1_an_py39", - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") - - client = TestClient(mod.app) - return client - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_extra_form(client: TestClient): - response = client.post( - "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} - ) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.extra", - "loc": ["body", "extra"], - "msg": "extra fields not permitted", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py deleted file mode 100644 index 83c717656..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py +++ /dev/null @@ -1,152 +0,0 @@ -import importlib -import warnings - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - -from ...utils import needs_py310, needs_pydanticv1 - - -@pytest.fixture( - name="client", - params=[ - pytest.param("tutorial001_pv1_py39"), - pytest.param("tutorial001_pv1_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") - - client = TestClient(mod.app) - return client - - -@needs_pydanticv1 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"type": "integer", "title": "Item Id"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Item", - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - } - } - }, - "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": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "price": {"type": "number", "title": "Price"}, - "tax": {"type": "number", "title": "Tax"}, - }, - "type": "object", - "required": ["name", "price"], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } - ], - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } - ) diff --git a/tests/test_tutorial/test_settings/test_app03.py b/tests/test_tutorial/test_settings/test_app03.py index 06e82398d..72de49796 100644 --- a/tests/test_tutorial/test_settings/test_app03.py +++ b/tests/test_tutorial/test_settings/test_app03.py @@ -5,8 +5,6 @@ import pytest from fastapi.testclient import TestClient from pytest import MonkeyPatch -from ...utils import needs_pydanticv1 - @pytest.fixture( name="mod_path", @@ -34,16 +32,6 @@ def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch): assert settings.items_per_user == 50 -@needs_pydanticv1 -def test_settings_pv1(mod_path: str, monkeypatch: MonkeyPatch): - monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") - config_mod = importlib.import_module(f"{mod_path}.config_pv1") - settings = config_mod.Settings() - assert settings.app_name == "Awesome API" - assert settings.admin_email == "admin@example.com" - assert settings.items_per_user == 50 - - def test_endpoint(main_mod: ModuleType, monkeypatch: MonkeyPatch): monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") client = TestClient(main_mod.app) diff --git a/tests/test_tutorial/test_settings/test_tutorial001.py b/tests/test_tutorial/test_settings/test_tutorial001.py index 6a0809698..f4576a0d2 100644 --- a/tests/test_tutorial/test_settings/test_tutorial001.py +++ b/tests/test_tutorial/test_settings/test_tutorial001.py @@ -4,16 +4,8 @@ import pytest from fastapi.testclient import TestClient from pytest import MonkeyPatch -from ...utils import needs_pydanticv1 - -@pytest.fixture( - name="app", - params=[ - pytest.param("tutorial001_py39"), - pytest.param("tutorial001_pv1_py39", marks=needs_pydanticv1), - ], -) +@pytest.fixture(name="app", params=[pytest.param("tutorial001_py39")]) def get_app(request: pytest.FixtureRequest, monkeypatch: MonkeyPatch): monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") mod = importlib.import_module(f"docs_src.settings.{request.param}") diff --git a/tests/utils.py b/tests/utils.py index b896d4527..efa0bfd52 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -10,8 +10,6 @@ needs_py_lt_314 = pytest.mark.skipif( sys.version_info >= (3, 14), reason="requires python3.13-" ) -needs_pydanticv1 = needs_py_lt_314 - def skip_module_if_py_gte_314(): """Skip entire module on Python 3.14+ at import time.""" From 4b2cfcfd34d0b2e158a05b377426e0c8376b94de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 12:55:22 +0000 Subject: [PATCH 021/243] =?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 10b262b48..21523b2e2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Breaking Changes + +* ➖ Drop support for `pydantic.v1`. PR [#14609](https://github.com/fastapi/fastapi/pull/14609) by [@tiangolo](https://github.com/tiangolo). + ### Internal * ✅ Run performance tests only on Pydantic v2. PR [#14608](https://github.com/fastapi/fastapi/pull/14608) by [@tiangolo](https://github.com/tiangolo). From 8322a4445a3b25acd9b26b61192571b2d92f9bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 16:19:50 +0100 Subject: [PATCH 022/243] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?8.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 21523b2e2..ef7894037 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.128.0 + ### Breaking Changes * ➖ Drop support for `pydantic.v1`. PR [#14609](https://github.com/fastapi/fastapi/pull/14609) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index dc447c8bf..6133787b0 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.127.1" +__version__ = "0.128.0" from starlette import status as status From 44c849c4fc3636f54eae74ab4e586c8ca9f3fb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 10:19:10 -0800 Subject: [PATCH 023/243] =?UTF-8?q?=F0=9F=94=A5=20Remove=20Pydantic=20v1?= =?UTF-8?q?=20=20specific=20test=20variants=20(#14611)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_additional_properties_bool.py | 21 +- ...onal_responses_custom_model_in_callback.py | 207 ++- tests/test_annotated.py | 48 +- tests/test_application.py | 25 +- tests/test_dependency_duplicates.py | 34 +- tests/test_dependency_overrides.py | 405 ++---- tests/test_extra_routes.py | 13 +- tests/test_filter_pydantic_sub_model_pv2.py | 45 +- tests/test_forms_single_model.py | 84 +- tests/test_infer_param_optionality.py | 57 +- tests/test_multi_body_errors.py | 185 +-- tests/test_multi_query_errors.py | 51 +- tests/test_openapi_examples.py | 114 +- .../test_openapi_query_parameter_extension.py | 21 +- tests/test_openapi_servers.py | 64 +- tests/test_params_repr.py | 43 +- tests/test_path.py | 1182 +++++------------ tests/test_query.py | 364 ++--- tests/test_regex_deprecated_body.py | 196 ++- tests/test_regex_deprecated_params.py | 61 +- .../test_body/test_list.py | 66 +- .../test_body/test_optional_list.py | 285 ++-- .../test_body/test_optional_str.py | 249 ++-- .../test_body/test_required_str.py | 101 +- .../test_cookie/test_optional_str.py | 59 +- .../test_cookie/test_required_str.py | 107 +- .../test_file/test_list.py | 99 +- .../test_file/test_optional.py | 89 +- .../test_file/test_optional_list.py | 103 +- .../test_file/test_required.py | 100 +- .../test_form/test_list.py | 66 +- .../test_form/test_optional_list.py | 153 +-- .../test_form/test_optional_str.py | 117 +- .../test_form/test_required_str.py | 101 +- .../test_header/test_list.py | 66 +- .../test_header/test_optional_list.py | 75 +- .../test_header/test_optional_str.py | 59 +- .../test_header/test_required_str.py | 101 +- .../test_query/test_list.py | 66 +- .../test_query/test_optional_list.py | 75 +- .../test_query/test_optional_str.py | 59 +- .../test_query/test_required_str.py | 107 +- tests/test_schema_extra_examples.py | 221 +-- tests/test_security_oauth2.py | 165 +-- tests/test_security_oauth2_optional.py | 165 +-- ...st_security_oauth2_optional_description.py | 165 +-- tests/test_sub_callbacks.py | 51 +- tests/test_tuples.py | 97 +- .../test_tutorial002.py | 15 +- .../test_tutorial004.py | 15 +- .../test_behind_a_proxy/test_tutorial003.py | 68 +- .../test_behind_a_proxy/test_tutorial004.py | 66 +- .../test_bigger_applications/test_main.py | 315 ++--- .../test_body/test_tutorial001.py | 355 ++--- .../test_body_fields/test_tutorial001.py | 75 +- .../test_tutorial001.py | 96 +- .../test_tutorial003.py | 177 +-- .../test_tutorial009.py | 34 +- .../test_tutorial001.py | 69 +- .../test_tutorial002.py | 52 +- .../test_cookie_params/test_tutorial001.py | 15 +- .../test_tutorial002.py | 45 +- .../test_dataclasses/test_tutorial001.py | 62 +- .../test_dataclasses/test_tutorial002.py | 108 +- .../test_dependencies/test_tutorial006.py | 51 +- .../test_dependencies/test_tutorial012.py | 101 +- .../test_extra_data_types/test_tutorial001.py | 217 ++- .../test_extra_models/test_tutorial003.py | 200 +-- .../test_handling_errors/test_tutorial005.py | 37 +- .../test_handling_errors/test_tutorial006.py | 34 +- .../test_tutorial001.py | 71 +- .../test_tutorial002.py | 79 +- .../test_tutorial003.py | 115 +- .../test_header_params/test_tutorial001.py | 15 +- .../test_header_params/test_tutorial002.py | 15 +- .../test_header_params/test_tutorial003.py | 25 +- .../test_tutorial001.py | 51 +- .../test_path_params/test_tutorial005.py | 58 +- .../test_tutorial001.py | 81 +- .../test_tutorial002.py | 103 +- .../test_query_params/test_tutorial005.py | 34 +- .../test_query_params/test_tutorial006.py | 82 +- .../test_tutorial010.py | 88 +- .../test_tutorial011.py | 25 +- .../test_request_files/test_tutorial001.py | 67 +- .../test_request_files/test_tutorial001_02.py | 268 ++-- .../test_request_files/test_tutorial002.py | 67 +- .../test_tutorial001.py | 167 +-- .../test_request_forms/test_tutorial001.py | 167 +-- .../test_tutorial001.py | 235 ++-- .../test_response_model/test_tutorial003.py | 212 ++- .../test_tutorial003_01.py | 218 ++- .../test_response_model/test_tutorial004.py | 178 ++- .../test_response_model/test_tutorial005.py | 230 ++-- .../test_response_model/test_tutorial006.py | 230 ++-- .../test_tutorial004.py | 85 +- .../test_tutorial005.py | 40 +- .../test_security/test_tutorial003.py | 59 +- .../test_security/test_tutorial005.py | 364 +++-- .../test_sql_databases/test_tutorial001.py | 36 +- .../test_sql_databases/test_tutorial002.py | 87 +- tests/test_union_body.py | 17 +- tests/test_union_body_discriminator.py | 21 +- tests/test_union_inherited_body.py | 29 +- 104 files changed, 4139 insertions(+), 8074 deletions(-) diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py index 3756b7d7c..063297a3f 100644 --- a/tests/test_additional_properties_bool.py +++ b/tests/test_additional_properties_bool.py @@ -1,6 +1,5 @@ from typing import Union -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict @@ -52,19 +51,13 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/Foo"}, - {"type": "null"}, - ], - "title": "Foo", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/Foo"} - ) + "schema": { + "anyOf": [ + {"$ref": "#/components/schemas/Foo"}, + {"type": "null"}, + ], + "title": "Foo", + } } } }, diff --git a/tests/test_additional_responses_custom_model_in_callback.py b/tests/test_additional_responses_custom_model_in_callback.py index 2ad575455..376d7714e 100644 --- a/tests/test_additional_responses_custom_model_in_callback.py +++ b/tests/test_additional_responses_custom_model_in_callback.py @@ -1,6 +1,6 @@ -from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, HttpUrl from starlette.responses import JSONResponse @@ -32,121 +32,114 @@ client = TestClient(app) 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": { - "/": { - "post": { - "summary": "Main Route", - "operationId": "main_route__post", - "parameters": [ - { - "required": True, - "schema": IsDict( - { - "title": "Callback Url", - "minLength": 1, - "type": "string", - "format": "uri", - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict( - { + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Main Route", + "operationId": "main_route__post", + "parameters": [ + { + "required": True, + "schema": { "title": "Callback Url", "maxLength": 2083, "minLength": 1, "type": "string", "format": "uri", - } - ), - "name": "callback_url", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + }, + "name": "callback_url", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "callbacks": { + "callback_route": { + "{$callback_url}/callback/": { + "get": { + "summary": "Callback Route", + "operationId": "callback_route__callback_url__callback__get", + "responses": { + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomModel" + } + } + }, + "description": "Bad Request", + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + }, } } - }, + } + }, + } + } + }, + "components": { + "schemas": { + "CustomModel": { + "title": "CustomModel", + "required": ["a"], + "type": "object", + "properties": {"a": {"title": "A", "type": "integer"}}, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } }, }, - "callbacks": { - "callback_route": { - "{$callback_url}/callback/": { - "get": { - "summary": "Callback Route", - "operationId": "callback_route__callback_url__callback__get", - "responses": { - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomModel" - } - } - }, - "description": "Bad Request", - }, - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - }, - } - } - } + "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"}, + }, }, } - } - }, - "components": { - "schemas": { - "CustomModel": { - "title": "CustomModel", - "required": ["a"], - "type": "object", - "properties": {"a": {"title": "A", "type": "integer"}}, - }, - "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_annotated.py b/tests/test_annotated.py index a9e7c78c9..39f6f83b2 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -1,7 +1,6 @@ from typing import Annotated import pytest -from dirty_equals import IsDict from fastapi import APIRouter, FastAPI, Query from fastapi.testclient import TestClient @@ -32,44 +31,23 @@ client = TestClient(app) foo_is_missing = { "detail": [ - IsDict( - { - "loc": ["query", "foo"], - "msg": "Field required", - "type": "missing", - "input": None, - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict( - { - "loc": ["query", "foo"], - "msg": "field required", - "type": "value_error.missing", - } - ) + { + "loc": ["query", "foo"], + "msg": "Field required", + "type": "missing", + "input": None, + } ] } foo_is_short = { "detail": [ - IsDict( - { - "ctx": {"min_length": 1}, - "loc": ["query", "foo"], - "msg": "String should have at least 1 character", - "type": "string_too_short", - "input": "", - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict( - { - "ctx": {"limit_value": 1}, - "loc": ["query", "foo"], - "msg": "ensure this value has at least 1 characters", - "type": "value_error.any_str.min_length", - } - ) + { + "ctx": {"min_length": 1}, + "loc": ["query", "foo"], + "msg": "String should have at least 1 character", + "type": "string_too_short", + "input": "", + } ] } diff --git a/tests/test_application.py b/tests/test_application.py index 8f1b0a18d..001586ff7 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,5 +1,4 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from .main import app @@ -274,14 +273,10 @@ def test_openapi_schema(): "name": "item_id", "in": "path", "required": True, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Item Id", - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict({"title": "Item Id", "type": "string"}), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Item Id", + }, } ], } @@ -984,14 +979,10 @@ def test_openapi_schema(): "name": "query", "in": "query", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Query", - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict({"title": "Query", "type": "integer"}), + "schema": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Query", + }, } ], } diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py index 7c6717e2a..a8658e03b 100644 --- a/tests/test_dependency_duplicates.py +++ b/tests/test_dependency_duplicates.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -46,29 +45,16 @@ async def no_duplicates_sub( def test_no_duplicates_invalid(): response = client.post("/no-duplicates", json={"item": {"data": "myitem"}}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item2"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item2"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "item2"], + "msg": "Field required", + "input": None, + } + ] + } def test_no_duplicates(): diff --git a/tests/test_dependency_overrides.py b/tests/test_dependency_overrides.py index 154937fa0..e25db624d 100644 --- a/tests/test_dependency_overrides.py +++ b/tests/test_dependency_overrides.py @@ -1,7 +1,6 @@ from typing import Optional import pytest -from dirty_equals import IsDict from fastapi import APIRouter, Depends, FastAPI from fastapi.testclient import TestClient @@ -54,29 +53,16 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend def test_main_depends(): response = client.get("/main-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "q"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } def test_main_depends_q_foo(): @@ -100,29 +86,16 @@ def test_main_depends_q_foo_skip_100_limit_200(): def test_decorator_depends(): response = client.get("/decorator-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "q"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } def test_decorator_depends_q_foo(): @@ -140,29 +113,16 @@ def test_decorator_depends_q_foo_skip_100_limit_200(): def test_router_depends(): response = client.get("/router-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "q"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } def test_router_depends_q_foo(): @@ -186,29 +146,16 @@ def test_router_depends_q_foo_skip_100_limit_200(): def test_router_decorator_depends(): response = client.get("/router-decorator-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "q"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } def test_router_decorator_depends_q_foo(): @@ -272,29 +219,17 @@ def test_override_with_sub_main_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/main-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -302,29 +237,17 @@ def test_override_with_sub__main_depends_q_foo(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/main-depends/?q=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -340,29 +263,17 @@ def test_override_with_sub_decorator_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/decorator-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -370,29 +281,17 @@ def test_override_with_sub_decorator_depends_q_foo(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/decorator-depends/?q=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -408,29 +307,17 @@ def test_override_with_sub_router_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/router-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -438,29 +325,17 @@ def test_override_with_sub_router_depends_q_foo(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/router-depends/?q=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -476,29 +351,17 @@ def test_override_with_sub_router_decorator_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/router-decorator-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -506,29 +369,17 @@ def test_override_with_sub_router_decorator_depends_q_foo(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/router-decorator-depends/?q=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index bd16fe925..45734ec28 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -1,6 +1,5 @@ from typing import Optional -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient @@ -328,14 +327,10 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "price": IsDict( - { - "title": "Price", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict({"title": "Price", "type": "number"}), + "price": { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py index d70f53043..fc5876410 100644 --- a/tests/test_filter_pydantic_sub_model_pv2.py +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -1,7 +1,7 @@ from typing import Optional import pytest -from dirty_equals import HasRepr, IsDict, IsOneOf +from dirty_equals import HasRepr from fastapi import Depends, FastAPI from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient @@ -63,23 +63,13 @@ def test_validator_is_cloned(client: TestClient): with pytest.raises(ResponseValidationError) as err: client.get("/model/modelX") assert err.value.errors() == [ - IsDict( - { - "type": "value_error", - "loc": ("response", "name"), - "msg": "Value error, name must end in A", - "input": "modelX", - "ctx": {"error": HasRepr("ValueError('name must end in A')")}, - } - ) - | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "loc": ("response", "name"), - "msg": "name must end in A", - "type": "value_error", - } - ) + { + "type": "value_error", + "loc": ("response", "name"), + "msg": "Value error, name must end in A", + "input": "modelX", + "ctx": {"error": HasRepr("ValueError('name must end in A')")}, + } ] @@ -145,23 +135,14 @@ def test_openapi_schema(client: TestClient): }, "ModelA": { "title": "ModelA", - "required": IsOneOf( - ["name", "description", "foo"], - # TODO remove when deprecating Pydantic v1 - ["name", "foo"], - ), + "required": ["name", "foo"], "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | - # TODO remove when deprecating Pydantic v1 - IsDict({"title": "Description", "type": "string"}), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "foo": {"$ref": "#/components/schemas/ModelB"}, "tags": { "additionalProperties": {"type": "string"}, diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py index c401cc937..7d03d2957 100644 --- a/tests/test_forms_single_model.py +++ b/tests/test_forms_single_model.py @@ -1,6 +1,5 @@ from typing import Annotated, Optional -from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -79,68 +78,37 @@ def test_invalid_data(): }, ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "age"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "seventy", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "age"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "age"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "seventy", + } + ] + } def test_no_data(): response = client.post("/form/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"tags": ["foo", "bar"], "with": "nothing"}, - }, - { - "type": "missing", - "loc": ["body", "lastname"], - "msg": "Field required", - "input": {"tags": ["foo", "bar"], "with": "nothing"}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "lastname"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"tags": ["foo", "bar"], "with": "nothing"}, + }, + { + "type": "missing", + "loc": ["body", "lastname"], + "msg": "Field required", + "input": {"tags": ["foo", "bar"], "with": "nothing"}, + }, + ] + } def test_extra_param_single(): diff --git a/tests/test_infer_param_optionality.py b/tests/test_infer_param_optionality.py index e3d57bb42..147018996 100644 --- a/tests/test_infer_param_optionality.py +++ b/tests/test_infer_param_optionality.py @@ -1,6 +1,5 @@ from typing import Optional -from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient @@ -163,16 +162,10 @@ def test_openapi_schema(): "required": False, "name": "user_id", "in": "query", - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, } ], "responses": { @@ -208,16 +201,10 @@ def test_openapi_schema(): "required": False, "name": "user_id", "in": "query", - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, }, ], "responses": { @@ -247,16 +234,10 @@ def test_openapi_schema(): "required": True, "name": "user_id", "in": "path", - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, } ], "responses": { @@ -292,16 +273,10 @@ def test_openapi_schema(): "required": True, "name": "user_id", "in": "path", - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, }, ], "responses": { diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 6ea405fe7..4418c77cb 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -1,8 +1,9 @@ from decimal import Decimal -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, condecimal app = FastAPI() @@ -24,109 +25,65 @@ client = TestClient(app) def test_put_correct_body(): response = client.post("/items/", json=[{"name": "Foo", "age": 5}]) assert response.status_code == 200, response.text - assert response.json() == { - "item": [ - { - "name": "Foo", - "age": IsOneOf( - 5, - # TODO: remove when deprecating Pydantic v1 - "5", - ), - } - ] - } + assert response.json() == snapshot( + { + "item": [ + { + "name": "Foo", + "age": "5", + } + ] + } + ) def test_jsonable_encoder_requiring_error(): response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}]) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["body", 0, "age"], - "msg": "Input should be greater than 0", - "input": -1.0, - "ctx": {"gt": 0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"limit_value": 0.0}, - "loc": ["body", 0, "age"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["body", 0, "age"], + "msg": "Input should be greater than 0", + "input": -1.0, + "ctx": {"gt": 0}, + } + ] + } def test_put_incorrect_body_multiple(): response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}]) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", 0, "name"], - "msg": "Field required", - "input": {"age": "five"}, - }, - { - "type": "decimal_parsing", - "loc": ["body", 0, "age"], - "msg": "Input should be a valid decimal", - "input": "five", - }, - { - "type": "missing", - "loc": ["body", 1, "name"], - "msg": "Field required", - "input": {"age": "six"}, - }, - { - "type": "decimal_parsing", - "loc": ["body", 1, "age"], - "msg": "Input should be a valid decimal", - "input": "six", - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", 0, "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 0, "age"], - "msg": "value is not a valid decimal", - "type": "type_error.decimal", - }, - { - "loc": ["body", 1, "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 1, "age"], - "msg": "value is not a valid decimal", - "type": "type_error.decimal", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", 0, "name"], + "msg": "Field required", + "input": {"age": "five"}, + }, + { + "type": "decimal_parsing", + "loc": ["body", 0, "age"], + "msg": "Input should be a valid decimal", + "input": "five", + }, + { + "type": "missing", + "loc": ["body", 1, "name"], + "msg": "Field required", + "input": {"age": "six"}, + }, + { + "type": "decimal_parsing", + "loc": ["body", 1, "age"], + "msg": "Input should be a valid decimal", + "input": "six", + }, + ] + } def test_openapi_schema(): @@ -179,31 +136,21 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [ - {"exclusiveMinimum": 0.0, "type": "number"}, - IsOneOf( - # pydantic < 2.12.0 - {"type": "string"}, - # pydantic >= 2.12.0 - { - "type": "string", - "pattern": r"^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$", - }, - ), - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Age", - "exclusiveMinimum": 0.0, - "type": "number", - } - ), + "age": { + "title": "Age", + "anyOf": [ + {"exclusiveMinimum": 0.0, "type": "number"}, + IsOneOf( + # pydantic < 2.12.0 + {"type": "string"}, + # pydantic >= 2.12.0 + { + "type": "string", + "pattern": r"^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$", + }, + ), + ], + }, }, }, "ValidationError": { diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index 7387a81dd..5df51ba18 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi import FastAPI, Query from fastapi.testclient import TestClient @@ -22,40 +21,22 @@ def test_multi_query(): def test_multi_query_incorrect(): response = client.get("/items/?q=five&q=six") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "q", 0], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "five", - }, - { - "type": "int_parsing", - "loc": ["query", "q", 1], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "six", - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q", 0], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "q", 1], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "q", 0], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "five", + }, + { + "type": "int_parsing", + "loc": ["query", "q", 1], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "six", + }, + ] + } def test_openapi_schema(): diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py index b3f83ae23..bd0d55452 100644 --- a/tests/test_openapi_examples.py +++ b/tests/test_openapi_examples.py @@ -1,6 +1,5 @@ from typing import Union -from dirty_equals import IsDict from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi.testclient import TestClient from pydantic import BaseModel @@ -155,26 +154,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - {"data": "Data in Body examples, example1"} - ], - } - ) - | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - {"data": "Data in Body examples, example1"} - ], - } - ), + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"} + ], + }, "examples": { "Example One": { "summary": "Example One Summary", @@ -265,27 +250,14 @@ def test_openapi_schema(): "name": "data", "in": "query", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "examples": [ - "json_schema_query1", - "json_schema_query2", - ], - "title": "Data", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "examples": [ - "json_schema_query1", - "json_schema_query2", - ], - "type": "string", - "title": "Data", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_query1", + "json_schema_query2", + ], + "title": "Data", + }, "examples": { "Query One": { "summary": "Query One Summary", @@ -323,27 +295,14 @@ def test_openapi_schema(): "name": "data", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "examples": [ - "json_schema_header1", - "json_schema_header2", - ], - "title": "Data", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "examples": [ - "json_schema_header1", - "json_schema_header2", - ], - "title": "Data", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_header1", + "json_schema_header2", + ], + "title": "Data", + }, "examples": { "Header One": { "summary": "Header One Summary", @@ -381,27 +340,14 @@ def test_openapi_schema(): "name": "data", "in": "cookie", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "examples": [ - "json_schema_cookie1", - "json_schema_cookie2", - ], - "title": "Data", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "examples": [ - "json_schema_cookie1", - "json_schema_cookie2", - ], - "title": "Data", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_cookie1", + "json_schema_cookie2", + ], + "title": "Data", + }, "examples": { "Cookie One": { "summary": "Cookie One Summary", diff --git a/tests/test_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py index dc7147c71..084cb695d 100644 --- a/tests/test_openapi_query_parameter_extension.py +++ b/tests/test_openapi_query_parameter_extension.py @@ -1,6 +1,5 @@ from typing import Optional -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient @@ -53,21 +52,11 @@ def test_openapi(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "default": 50, - "title": "Standard Query Param", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Standard Query Param", - "type": "integer", - "default": 50, - } - ), + "schema": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "default": 50, + "title": "Standard Query Param", + }, "name": "standard_query_param", "in": "query", }, diff --git a/tests/test_openapi_servers.py b/tests/test_openapi_servers.py index 8697c8438..33079e4b1 100644 --- a/tests/test_openapi_servers.py +++ b/tests/test_openapi_servers.py @@ -1,6 +1,6 @@ -from dirty_equals import IsOneOf from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI( servers=[ @@ -30,39 +30,31 @@ def test_app(): 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"}, - "servers": [ - {"url": "/", "description": "Default, relative server"}, - { - "url": IsOneOf( - "http://staging.localhost.tiangolo.com:8000/", - # TODO: remove when deprecating Pydantic v1 - "http://staging.localhost.tiangolo.com:8000", - ), - "description": "Staging but actually localhost still", - }, - { - "url": IsOneOf( - "https://prod.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://prod.example.com", - ) - }, - ], - "paths": { - "/foo": { - "get": { - "summary": "Foo", - "operationId": "foo_foo_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + {"url": "/", "description": "Default, relative server"}, + { + "url": "http://staging.localhost.tiangolo.com:8000", + "description": "Staging but actually localhost still", + }, + {"url": "https://prod.example.com"}, + ], + "paths": { + "/foo": { + "get": { + "summary": "Foo", + "operationId": "foo_foo_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py index 19c2e8d69..670e4f5dd 100644 --- a/tests/test_params_repr.py +++ b/tests/test_params_repr.py @@ -1,6 +1,5 @@ from typing import Any -from dirty_equals import IsOneOf from fastapi.params import Body, Cookie, Header, Param, Path, Query test_data: list[Any] = ["teststr", None, ..., 1, []] @@ -19,11 +18,7 @@ def test_param_repr_none(): def test_param_repr_ellipsis(): - assert repr(Param(...)) == IsOneOf( - "Param(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Param(Ellipsis)", - ) + assert repr(Param(...)) == "Param(PydanticUndefined)" def test_param_repr_number(): @@ -35,16 +30,8 @@ def test_param_repr_list(): def test_path_repr(): - assert repr(Path()) == IsOneOf( - "Path(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Path(Ellipsis)", - ) - assert repr(Path(...)) == IsOneOf( - "Path(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Path(Ellipsis)", - ) + assert repr(Path()) == "Path(PydanticUndefined)" + assert repr(Path(...)) == "Path(PydanticUndefined)" def test_query_repr_str(): @@ -56,11 +43,7 @@ def test_query_repr_none(): def test_query_repr_ellipsis(): - assert repr(Query(...)) == IsOneOf( - "Query(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Query(Ellipsis)", - ) + assert repr(Query(...)) == "Query(PydanticUndefined)" def test_query_repr_number(): @@ -80,11 +63,7 @@ def test_header_repr_none(): def test_header_repr_ellipsis(): - assert repr(Header(...)) == IsOneOf( - "Header(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Header(Ellipsis)", - ) + assert repr(Header(...)) == "Header(PydanticUndefined)" def test_header_repr_number(): @@ -104,11 +83,7 @@ def test_cookie_repr_none(): def test_cookie_repr_ellipsis(): - assert repr(Cookie(...)) == IsOneOf( - "Cookie(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Cookie(Ellipsis)", - ) + assert repr(Cookie(...)) == "Cookie(PydanticUndefined)" def test_cookie_repr_number(): @@ -128,11 +103,7 @@ def test_body_repr_none(): def test_body_repr_ellipsis(): - assert repr(Body(...)) == IsOneOf( - "Body(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Body(Ellipsis)", - ) + assert repr(Body(...)) == "Body(PydanticUndefined)" def test_body_repr_number(): diff --git a/tests/test_path.py b/tests/test_path.py index 09c1f13fb..47051b927 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from .main import app @@ -45,57 +44,31 @@ def test_path_str_True(): def test_path_int_foobar(): response = client.get("/path/int/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foobar", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foobar", + } + ] + } def test_path_int_True(): response = client.get("/path/int/True") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "True", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "True", + } + ] + } def test_path_int_42(): @@ -107,85 +80,46 @@ def test_path_int_42(): def test_path_int_42_5(): response = client.get("/path/int/42.5") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "42.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "42.5", + } + ] + } def test_path_float_foobar(): response = client.get("/path/float/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "foobar", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "float_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "foobar", + } + ] + } def test_path_float_True(): response = client.get("/path/float/True") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "True", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "float_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "True", + } + ] + } def test_path_float_42(): @@ -203,29 +137,16 @@ def test_path_float_42_5(): def test_path_bool_foobar(): response = client.get("/path/bool/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "bool_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid boolean, unable to interpret input", - "input": "foobar", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value could not be parsed to a boolean", - "type": "type_error.bool", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "foobar", + } + ] + } def test_path_bool_True(): @@ -237,57 +158,31 @@ def test_path_bool_True(): def test_path_bool_42(): response = client.get("/path/bool/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "bool_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid boolean, unable to interpret input", - "input": "42", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value could not be parsed to a boolean", - "type": "type_error.bool", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "42", + } + ] + } def test_path_bool_42_5(): response = client.get("/path/bool/42.5") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "bool_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid boolean, unable to interpret input", - "input": "42.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value could not be parsed to a boolean", - "type": "type_error.bool", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "42.5", + } + ] + } def test_path_bool_1(): @@ -335,31 +230,17 @@ def test_path_param_minlength_foo(): def test_path_param_minlength_fo(): response = client.get("/path/param-minlength/fo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_too_short", - "loc": ["path", "item_id"], - "msg": "String should have at least 3 characters", - "input": "fo", - "ctx": {"min_length": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value has at least 3 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["path", "item_id"], + "msg": "String should have at least 3 characters", + "input": "fo", + "ctx": {"min_length": 3}, + } + ] + } def test_path_param_maxlength_foo(): @@ -371,31 +252,17 @@ def test_path_param_maxlength_foo(): def test_path_param_maxlength_foobar(): response = client.get("/path/param-maxlength/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_too_long", - "loc": ["path", "item_id"], - "msg": "String should have at most 3 characters", - "input": "foobar", - "ctx": {"max_length": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value has at most 3 characters", - "type": "value_error.any_str.max_length", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_too_long", + "loc": ["path", "item_id"], + "msg": "String should have at most 3 characters", + "input": "foobar", + "ctx": {"max_length": 3}, + } + ] + } def test_path_param_min_maxlength_foo(): @@ -407,60 +274,33 @@ def test_path_param_min_maxlength_foo(): def test_path_param_min_maxlength_foobar(): response = client.get("/path/param-min_maxlength/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_too_long", - "loc": ["path", "item_id"], - "msg": "String should have at most 3 characters", - "input": "foobar", - "ctx": {"max_length": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value has at most 3 characters", - "type": "value_error.any_str.max_length", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_too_long", + "loc": ["path", "item_id"], + "msg": "String should have at most 3 characters", + "input": "foobar", + "ctx": {"max_length": 3}, + } + ] + } def test_path_param_min_maxlength_f(): response = client.get("/path/param-min_maxlength/f") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_too_short", - "loc": ["path", "item_id"], - "msg": "String should have at least 2 characters", - "input": "f", - "ctx": {"min_length": 2}, - } - ] - } - ) | IsDict( - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value has at least 2 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 2}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["path", "item_id"], + "msg": "String should have at least 2 characters", + "input": "f", + "ctx": {"min_length": 2}, + } + ] + } def test_path_param_gt_42(): @@ -472,31 +312,17 @@ def test_path_param_gt_42(): def test_path_param_gt_2(): response = client.get("/path/param-gt/2") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 3", - "input": "2", - "ctx": {"gt": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 3", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 3", + "input": "2", + "ctx": {"gt": 3.0}, + } + ] + } def test_path_param_gt0_0_05(): @@ -508,31 +334,17 @@ def test_path_param_gt0_0_05(): def test_path_param_gt0_0(): response = client.get("/path/param-gt0/0") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 0", - "input": "0", - "ctx": {"gt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 0", + "input": "0", + "ctx": {"gt": 0.0}, + } + ] + } def test_path_param_ge_42(): @@ -550,61 +362,33 @@ def test_path_param_ge_3(): def test_path_param_ge_2(): response = client.get("/path/param-ge/2") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be greater than or equal to 3", - "input": "2", - "ctx": {"ge": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than or equal to 3", - "type": "value_error.number.not_ge", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be greater than or equal to 3", + "input": "2", + "ctx": {"ge": 3.0}, + } + ] + } def test_path_param_lt_42(): response = client.get("/path/param-lt/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 3", - "input": "42", - "ctx": {"lt": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "42", + "ctx": {"lt": 3.0}, + } + ] + } def test_path_param_lt_2(): @@ -622,61 +406,33 @@ def test_path_param_lt0__1(): def test_path_param_lt0_0(): response = client.get("/path/param-lt0/0") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 0", - "input": "0", - "ctx": {"lt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 0", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 0}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 0", + "input": "0", + "ctx": {"lt": 0.0}, + } + ] + } def test_path_param_le_42(): response = client.get("/path/param-le/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be less than or equal to 3", - "input": "42", - "ctx": {"le": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "42", + "ctx": {"le": 3.0}, + } + ] + } def test_path_param_le_3(): @@ -700,61 +456,33 @@ def test_path_param_lt_gt_2(): def test_path_param_lt_gt_4(): response = client.get("/path/param-lt-gt/4") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 3", - "input": "4", - "ctx": {"lt": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "4", + "ctx": {"lt": 3.0}, + } + ] + } def test_path_param_lt_gt_0(): response = client.get("/path/param-lt-gt/0") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 1", - "input": "0", - "ctx": {"gt": 1.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 1", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 1}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 1", + "input": "0", + "ctx": {"gt": 1.0}, + } + ] + } def test_path_param_le_ge_2(): @@ -777,31 +505,17 @@ def test_path_param_le_ge_3(): def test_path_param_le_ge_4(): response = client.get("/path/param-le-ge/4") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be less than or equal to 3", - "input": "4", - "ctx": {"le": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "4", + "ctx": {"le": 3.0}, + } + ] + } def test_path_param_lt_int_2(): @@ -813,59 +527,32 @@ def test_path_param_lt_int_2(): def test_path_param_lt_int_42(): response = client.get("/path/param-lt-int/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 3", - "input": "42", - "ctx": {"lt": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "42", + "ctx": {"lt": 3}, + } + ] + } def test_path_param_lt_int_2_7(): response = client.get("/path/param-lt-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + } + ] + } def test_path_param_gt_int_42(): @@ -877,89 +564,48 @@ def test_path_param_gt_int_42(): def test_path_param_gt_int_2(): response = client.get("/path/param-gt-int/2") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 3", - "input": "2", - "ctx": {"gt": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 3", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 3", + "input": "2", + "ctx": {"gt": 3}, + } + ] + } def test_path_param_gt_int_2_7(): response = client.get("/path/param-gt-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + } + ] + } def test_path_param_le_int_42(): response = client.get("/path/param-le-int/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be less than or equal to 3", - "input": "42", - "ctx": {"le": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "42", + "ctx": {"le": 3}, + } + ] + } def test_path_param_le_int_3(): @@ -977,29 +623,16 @@ def test_path_param_le_int_2(): def test_path_param_le_int_2_7(): response = client.get("/path/param-le-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + } + ] + } def test_path_param_ge_int_42(): @@ -1017,59 +650,32 @@ def test_path_param_ge_int_3(): def test_path_param_ge_int_2(): response = client.get("/path/param-ge-int/2") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be greater than or equal to 3", - "input": "2", - "ctx": {"ge": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than or equal to 3", - "type": "value_error.number.not_ge", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be greater than or equal to 3", + "input": "2", + "ctx": {"ge": 3}, + } + ] + } def test_path_param_ge_int_2_7(): response = client.get("/path/param-ge-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + } + ] + } def test_path_param_lt_gt_int_2(): @@ -1081,89 +687,48 @@ def test_path_param_lt_gt_int_2(): def test_path_param_lt_gt_int_4(): response = client.get("/path/param-lt-gt-int/4") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 3", - "input": "4", - "ctx": {"lt": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "4", + "ctx": {"lt": 3}, + } + ] + } def test_path_param_lt_gt_int_0(): response = client.get("/path/param-lt-gt-int/0") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 1", - "input": "0", - "ctx": {"gt": 1}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 1", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 1}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 1", + "input": "0", + "ctx": {"gt": 1}, + } + ] + } def test_path_param_lt_gt_int_2_7(): response = client.get("/path/param-lt-gt-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + } + ] + } def test_path_param_le_ge_int_2(): @@ -1187,56 +752,29 @@ def test_path_param_le_ge_int_3(): def test_path_param_le_ge_int_4(): response = client.get("/path/param-le-ge-int/4") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be less than or equal to 3", - "input": "4", - "ctx": {"le": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "4", + "ctx": {"le": 3}, + } + ] + } def test_path_param_le_ge_int_2_7(): response = client.get("/path/param-le-ge-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + } + ] + } diff --git a/tests/test_query.py b/tests/test_query.py index 57f551d2a..c25960cac 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from .main import app @@ -9,29 +8,16 @@ client = TestClient(app) def test_query(): response = client.get("/query") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_query_baz(): @@ -43,29 +29,16 @@ def test_query_query_baz(): def test_query_not_declared_baz(): response = client.get("/query?not_declared=baz") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_optional(): @@ -89,29 +62,16 @@ def test_query_optional_not_declared_baz(): def test_query_int(): response = client.get("/query/int") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_int_query_42(): @@ -123,85 +83,46 @@ def test_query_int_query_42(): def test_query_int_query_42_5(): response = client.get("/query/int?query=42.5") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "42.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "42.5", + } + ] + } def test_query_int_query_baz(): response = client.get("/query/int?query=baz") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "baz", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "baz", + } + ] + } def test_query_int_not_declared_baz(): response = client.get("/query/int?not_declared=baz") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_int_optional(): @@ -219,29 +140,16 @@ def test_query_int_optional_query_50(): def test_query_int_optional_query_foo(): response = client.get("/query/int/optional?query=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_query_int_default(): @@ -259,29 +167,16 @@ def test_query_int_default_query_50(): def test_query_int_default_query_foo(): response = client.get("/query/int/default?query=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_query_param(): @@ -299,29 +194,16 @@ def test_query_param_query_50(): def test_query_param_required(): response = client.get("/query/param-required") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_param_required_query_50(): @@ -333,29 +215,16 @@ def test_query_param_required_query_50(): def test_query_param_required_int(): response = client.get("/query/param-required/int") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_param_required_int_query_50(): @@ -367,29 +236,16 @@ def test_query_param_required_int_query_50(): def test_query_param_required_int_query_foo(): response = client.get("/query/param-required/int?query=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_query_frozenset_query_1_query_1_query_2(): diff --git a/tests/test_regex_deprecated_body.py b/tests/test_regex_deprecated_body.py index 9d58c5dae..5b4daa450 100644 --- a/tests/test_regex_deprecated_body.py +++ b/tests/test_regex_deprecated_body.py @@ -1,10 +1,10 @@ from typing import Annotated import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient +from inline_snapshot import snapshot from .utils import needs_py310 @@ -47,31 +47,17 @@ def test_query_nonregexquery(): client = get_client() response = client.post("/items/", data={"q": "nonregexquery"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["body", "q"], - "msg": "String should match pattern '^fixedquery$'", - "input": "nonregexquery", - "ctx": {"pattern": "^fixedquery$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["body", "q"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + } + ] + } @needs_py310 @@ -79,104 +65,88 @@ def test_openapi_schema(): client = get_client() response = client.get("/openapi.json") assert response.status_code == 200, response.text - # insert_assert(response.json()) - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Read Items", - "operationId": "read_items_items__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_read_items_items__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Read Items", + "operationId": "read_items_items__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { "$ref": "#/components/schemas/Body_read_items_items__post" } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - }, + } } - } - }, - "components": { - "schemas": { - "Body_read_items_items__post": { - "properties": { - "q": IsDict( - { + }, + "components": { + "schemas": { + "Body_read_items_items__post": { + "properties": { + "q": { "anyOf": [ {"type": "string", "pattern": "^fixedquery$"}, {"type": "null"}, ], "title": "Q", } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"type": "string", "pattern": "^fixedquery$", "title": "Q"} - ) - }, - "type": "object", - "title": "Body_read_items_items__post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, + "type": "object", + "title": "Body_read_items_items__post", }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_regex_deprecated_params.py b/tests/test_regex_deprecated_params.py index 8973657a9..d6eaa45fb 100644 --- a/tests/test_regex_deprecated_params.py +++ b/tests/test_regex_deprecated_params.py @@ -1,7 +1,6 @@ from typing import Annotated import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Query from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient @@ -47,31 +46,17 @@ def test_query_params_str_validations_item_query_nonregexquery(): client = get_client() response = client.get("/items/", params={"q": "nonregexquery"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["query", "q"], - "msg": "String should match pattern '^fixedquery$'", - "input": "nonregexquery", - "ctx": {"pattern": "^fixedquery$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "q"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + } + ] + } @needs_py310 @@ -93,23 +78,13 @@ def test_openapi_schema(): "name": "q", "in": "query", "required": False, - "schema": IsDict( - { - "anyOf": [ - {"type": "string", "pattern": "^fixedquery$"}, - {"type": "null"}, - ], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "pattern": "^fixedquery$", - "title": "Q", - } - ), + "schema": { + "anyOf": [ + {"type": "string", "pattern": "^fixedquery$"}, + {"type": "null"}, + ], + "title": "Q", + }, } ], "responses": { diff --git a/tests/test_request_params/test_body/test_list.py b/tests/test_request_params/test_body/test_list.py index 0048da0f8..50847335c 100644 --- a/tests/test_request_params/test_body/test_list.py +++ b/tests/test_request_params/test_body/test_list.py @@ -148,29 +148,16 @@ def test_required_list_alias_missing(path: str, json: Union[dict, None]): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": IsOneOf(["body", "p_alias"], ["body"]), - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": IsOneOf(["body", "p_alias"], ["body"]), - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p_alias"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -181,29 +168,16 @@ def test_required_list_alias_by_name(path: str): client = TestClient(app) response = client.post(path, json={"p": ["hello", "world"]}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {"p": ["hello", "world"]}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_body/test_optional_list.py b/tests/test_request_params/test_body/test_optional_list.py index 960e8890f..ba8ba9092 100644 --- a/tests/test_request_params/test_body/test_optional_list.py +++ b/tests/test_request_params/test_body/test_optional_list.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -38,30 +37,19 @@ def test_optional_list_str_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p": {"items": {"type": "string"}, "type": "array", "title": "P"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_list_str_missing(): @@ -75,29 +63,16 @@ def test_model_optional_list_str_missing(): client = TestClient(app) response = client.post("/model-optional-list-str") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( @@ -153,34 +128,19 @@ def test_optional_list_str_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_alias": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_alias": { - "items": {"type": "string"}, - "type": "array", - "title": "P Alias", - }, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_list_alias_missing(): @@ -194,29 +154,16 @@ def test_model_optional_list_alias_missing(): client = TestClient(app) response = client.post("/model-optional-list-alias") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( @@ -289,34 +236,19 @@ def test_optional_list_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": { - "items": {"type": "string"}, - "type": "array", - "title": "P Val Alias", - }, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_list_validation_alias_missing(): @@ -330,29 +262,16 @@ def test_model_optional_list_validation_alias_missing(): client = TestClient(app) response = client.post("/model-optional-list-validation-alias") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( @@ -438,34 +357,19 @@ def test_optional_list_alias_and_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": { - "items": {"type": "string"}, - "type": "array", - "title": "P Val Alias", - }, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_list_alias_and_validation_alias_missing(): @@ -479,29 +383,16 @@ def test_model_optional_list_alias_and_validation_alias_missing(): client = TestClient(app) response = client.post("/model-optional-list-alias-and-validation-alias") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_body/test_optional_str.py b/tests/test_request_params/test_body/test_optional_str.py index 59732688a..b9c18034d 100644 --- a/tests/test_request_params/test_body/test_optional_str.py +++ b/tests/test_request_params/test_body/test_optional_str.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -36,27 +35,16 @@ def test_optional_str_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p": {"type": "string", "title": "P"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_str_missing(): @@ -70,29 +58,16 @@ def test_model_optional_str_missing(): client = TestClient(app) response = client.post("/model-optional-str") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( @@ -148,27 +123,16 @@ def test_optional_str_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_alias": {"type": "string", "title": "P Alias"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_alias_missing(): @@ -182,29 +146,16 @@ def test_model_optional_alias_missing(): client = TestClient(app) response = client.post("/model-optional-alias") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( @@ -274,27 +225,16 @@ def test_optional_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": {"type": "string", "title": "P Val Alias"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_validation_alias_missing(): @@ -308,29 +248,16 @@ def test_model_optional_validation_alias_missing(): client = TestClient(app) response = client.post("/model-optional-validation-alias") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( @@ -413,27 +340,16 @@ def test_optional_alias_and_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": {"type": "string", "title": "P Val Alias"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_alias_and_validation_alias_missing(): @@ -447,29 +363,16 @@ def test_model_optional_alias_and_validation_alias_missing(): client = TestClient(app) response = client.post("/model-optional-alias-and-validation-alias") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_body/test_required_str.py b/tests/test_request_params/test_body/test_required_str.py index 5571ba5d5..5b434fa1d 100644 --- a/tests/test_request_params/test_body/test_required_str.py +++ b/tests/test_request_params/test_body/test_required_str.py @@ -1,7 +1,7 @@ from typing import Annotated, Any, Union import pytest -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -55,29 +55,16 @@ def test_required_str_missing(path: str, json: Union[dict[str, Any], None]): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": IsOneOf(["body"], ["body", "p"]), - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": IsOneOf(["body"], ["body", "p"]), - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body"], ["body", "p"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -141,29 +128,16 @@ def test_required_alias_missing(path: str, json: Union[dict[str, Any], None]): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": IsOneOf(["body", "p_alias"], ["body"]), - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": IsOneOf(["body", "p_alias"], ["body"]), - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p_alias"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -174,29 +148,16 @@ def test_required_alias_by_name(path: str): client = TestClient(app) response = client.post(path, json={"p": "hello"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {"p": "hello"}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": IsOneOf(["body", "p_alias"], ["body"]), - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_cookie/test_optional_str.py b/tests/test_request_params/test_cookie/test_optional_str.py index b2f7f9cef..6f381c8b8 100644 --- a/tests/test_request_params/test_cookie/test_optional_str.py +++ b/tests/test_request_params/test_cookie/test_optional_str.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import Cookie, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -32,26 +31,15 @@ async def read_model_optional_str(p: Annotated[CookieModelOptionalStr, Cookie()] ) def test_optional_str_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P", - }, - "name": "p", - "in": "cookie", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"title": "P", "type": "string"}, - "name": "p", - "in": "cookie", - } - ) + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "cookie", + } ] @@ -104,26 +92,15 @@ async def read_model_optional_alias(p: Annotated[CookieModelOptionalAlias, Cooki ) def test_optional_str_alias_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Alias", - }, - "name": "p_alias", - "in": "cookie", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"title": "P Alias", "type": "string"}, - "name": "p_alias", - "in": "cookie", - } - ) + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "cookie", + } ] diff --git a/tests/test_request_params/test_cookie/test_required_str.py b/tests/test_request_params/test_cookie/test_required_str.py index 58bb7af5b..3e877b3e3 100644 --- a/tests/test_request_params/test_cookie/test_required_str.py +++ b/tests/test_request_params/test_cookie/test_required_str.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import Cookie, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -49,29 +49,16 @@ def test_required_str_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["cookie", "p"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["cookie", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -127,29 +114,16 @@ def test_required_alias_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["cookie", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["cookie", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -164,32 +138,19 @@ def test_required_alias_by_name(path: str): client.cookies.set("p", "hello") response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["cookie", "p_alias"], - "msg": "Field required", - "input": IsOneOf( - None, - {"p": "hello"}, - ), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["cookie", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + {"p": "hello"}, + ), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_file/test_list.py b/tests/test_request_params/test_file/test_list.py index 90f36e5f7..f09653255 100644 --- a/tests/test_request_params/test_file/test_list.py +++ b/tests/test_request_params/test_file/test_list.py @@ -75,29 +75,16 @@ def test_list_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -182,29 +169,16 @@ def test_list_alias_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -218,29 +192,16 @@ def test_list_alias_by_name(path: str): client = TestClient(app) response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_file/test_optional.py b/tests/test_request_params/test_file/test_optional.py index 4e9564873..45ef7bdec 100644 --- a/tests/test_request_params/test_file/test_optional.py +++ b/tests/test_request_params/test_file/test_optional.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, File, UploadFile from fastapi.testclient import TestClient @@ -36,21 +35,13 @@ def test_optional_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p": ( - IsDict( - { - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - "title": "P", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "P", "type": "string", "format": "binary"} - ) - ), + "p": { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P", + } }, "title": body_model_name, "type": "object", @@ -116,21 +107,13 @@ def test_optional_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_alias": ( - IsDict( - { - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - "title": "P Alias", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "P Alias", "type": "string", "format": "binary"} - ) - ), + "p_alias": { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P Alias", + } }, "title": body_model_name, "type": "object", @@ -215,21 +198,13 @@ def test_optional_validation_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_val_alias": ( - IsDict( - { - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - "title": "P Val Alias", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "P Val Alias", "type": "string", "format": "binary"} - ) - ), + "p_val_alias": { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P Val Alias", + } }, "title": body_model_name, "type": "object", @@ -319,21 +294,13 @@ def test_optional_alias_and_validation_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_val_alias": ( - IsDict( - { - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - "title": "P Val Alias", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "P Val Alias", "type": "string", "format": "binary"} - ) - ), + "p_val_alias": { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P Val Alias", + } }, "title": body_model_name, "type": "object", diff --git a/tests/test_request_params/test_file/test_optional_list.py b/tests/test_request_params/test_file/test_optional_list.py index e18f36e15..162fbe08a 100644 --- a/tests/test_request_params/test_file/test_optional_list.py +++ b/tests/test_request_params/test_file/test_optional_list.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, File, UploadFile from fastapi.testclient import TestClient @@ -38,28 +37,16 @@ def test_optional_list_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p": ( - IsDict( + "p": { + "anyOf": [ { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "P", "type": "array", "items": {"type": "string", "format": "binary"}, }, - ) - ), + {"type": "null"}, + ], + "title": "P", + } }, "title": body_model_name, "type": "object", @@ -125,28 +112,16 @@ def test_optional_list_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_alias": ( - IsDict( + "p_alias": { + "anyOf": [ { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P Alias", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "P Alias", "type": "array", "items": {"type": "string", "format": "binary"}, - } - ) - ), + }, + {"type": "null"}, + ], + "title": "P Alias", + } }, "title": body_model_name, "type": "object", @@ -228,28 +203,16 @@ def test_optional_validation_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_val_alias": ( - IsDict( + "p_val_alias": { + "anyOf": [ { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P Val Alias", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "P Val Alias", "type": "array", "items": {"type": "string", "format": "binary"}, - } - ) - ), + }, + {"type": "null"}, + ], + "title": "P Val Alias", + } }, "title": body_model_name, "type": "object", @@ -336,28 +299,16 @@ def test_optional_list_alias_and_validation_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_val_alias": ( - IsDict( + "p_val_alias": { + "anyOf": [ { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P Val Alias", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "P Val Alias", "type": "array", "items": {"type": "string", "format": "binary"}, - } - ) - ), + }, + {"type": "null"}, + ], + "title": "P Val Alias", + } }, "title": body_model_name, "type": "object", diff --git a/tests/test_request_params/test_file/test_required.py b/tests/test_request_params/test_file/test_required.py index 9783f4bce..a0f9d23a6 100644 --- a/tests/test_request_params/test_file/test_required.py +++ b/tests/test_request_params/test_file/test_required.py @@ -1,7 +1,6 @@ from typing import Annotated import pytest -from dirty_equals import IsDict from fastapi import FastAPI, File, UploadFile from fastapi.testclient import TestClient @@ -55,29 +54,16 @@ def test_required_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -142,29 +128,16 @@ def test_required_alias_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -178,29 +151,16 @@ def test_required_alias_by_name(path: str): client = TestClient(app) response = client.post(path, files=[("p", b"hello")]) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_form/test_list.py b/tests/test_request_params/test_form/test_list.py index 600dba4ae..cfc42f523 100644 --- a/tests/test_request_params/test_form/test_list.py +++ b/tests/test_request_params/test_form/test_list.py @@ -146,29 +146,16 @@ def test_required_list_alias_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -182,29 +169,16 @@ def test_required_list_alias_by_name(path: str): client = TestClient(app) response = client.post(path, data={"p": ["hello", "world"]}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {"p": ["hello", "world"]}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_form/test_optional_list.py b/tests/test_request_params/test_form/test_optional_list.py index 4552623f5..6d1957a18 100644 --- a/tests/test_request_params/test_form/test_optional_list.py +++ b/tests/test_request_params/test_form/test_optional_list.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -38,30 +37,19 @@ def test_optional_list_str_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p": {"items": {"type": "string"}, "type": "array", "title": "P"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( @@ -119,34 +107,19 @@ def test_optional_list_str_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_alias": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_alias": { - "items": {"type": "string"}, - "type": "array", - "title": "P Alias", - }, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( @@ -217,34 +190,19 @@ def test_optional_list_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": { - "items": {"type": "string"}, - "type": "array", - "title": "P Val Alias", - }, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( @@ -326,34 +284,19 @@ def test_optional_list_alias_and_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": { - "items": {"type": "string"}, - "type": "array", - "title": "P Val Alias", - }, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_form/test_optional_str.py b/tests/test_request_params/test_form/test_optional_str.py index 1b0829904..810e83caa 100644 --- a/tests/test_request_params/test_form/test_optional_str.py +++ b/tests/test_request_params/test_form/test_optional_str.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -36,27 +35,16 @@ def test_optional_str_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p": {"type": "string", "title": "P"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( @@ -112,27 +100,16 @@ def test_optional_str_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_alias": {"type": "string", "title": "P Alias"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( @@ -200,27 +177,16 @@ def test_optional_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": {"type": "string", "title": "P Val Alias"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( @@ -303,27 +269,16 @@ def test_optional_alias_and_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": {"type": "string", "title": "P Val Alias"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_form/test_required_str.py b/tests/test_request_params/test_form/test_required_str.py index 1d2431b33..7c9523b30 100644 --- a/tests/test_request_params/test_form/test_required_str.py +++ b/tests/test_request_params/test_form/test_required_str.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -54,29 +54,16 @@ def test_required_str_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -137,29 +124,16 @@ def test_required_alias_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -170,29 +144,16 @@ def test_required_alias_by_name(path: str): client = TestClient(app) response = client.post(path, data={"p": "hello"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {"p": "hello"}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_header/test_list.py b/tests/test_request_params/test_header/test_list.py index 2eba17559..65510094a 100644 --- a/tests/test_request_params/test_header/test_list.py +++ b/tests/test_request_params/test_header/test_list.py @@ -135,29 +135,16 @@ def test_required_list_alias_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "p_alias"], - "msg": "Field required", - "input": AnyThing, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": AnyThing, + } + ] + } @pytest.mark.parametrize( @@ -171,29 +158,16 @@ def test_required_list_alias_by_name(path: str): client = TestClient(app) response = client.get(path, headers=[("p", "hello"), ("p", "world")]) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_header/test_optional_list.py b/tests/test_request_params/test_header/test_optional_list.py index cd6167a18..5dd4ea9ad 100644 --- a/tests/test_request_params/test_header/test_optional_list.py +++ b/tests/test_request_params/test_header/test_optional_list.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Header from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -36,29 +35,18 @@ async def read_model_optional_list_str( ) def test_optional_list_str_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P", - }, - "name": "p", - "in": "header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"items": {"type": "string"}, "type": "array", "title": "P"}, - "name": "p", - "in": "header", - } - ) + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + "name": "p", + "in": "header", + } ] @@ -112,33 +100,18 @@ async def read_model_optional_list_alias( ) def test_optional_list_str_alias_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Alias", - }, - "name": "p_alias", - "in": "header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": { - "items": {"type": "string"}, - "type": "array", - "title": "P Alias", - }, - "name": "p_alias", - "in": "header", - } - ) + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + "name": "p_alias", + "in": "header", + } ] diff --git a/tests/test_request_params/test_header/test_optional_str.py b/tests/test_request_params/test_header/test_optional_str.py index d4f25cc1e..0bd0eddc1 100644 --- a/tests/test_request_params/test_header/test_optional_str.py +++ b/tests/test_request_params/test_header/test_optional_str.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Header from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -32,26 +31,15 @@ async def read_model_optional_str(p: Annotated[HeaderModelOptionalStr, Header()] ) def test_optional_str_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P", - }, - "name": "p", - "in": "header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"title": "P", "type": "string"}, - "name": "p", - "in": "header", - } - ) + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "header", + } ] @@ -103,26 +91,15 @@ async def read_model_optional_alias(p: Annotated[HeaderModelOptionalAlias, Heade ) def test_optional_str_alias_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Alias", - }, - "name": "p_alias", - "in": "header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"title": "P Alias", "type": "string"}, - "name": "p_alias", - "in": "header", - } - ) + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "header", + } ] diff --git a/tests/test_request_params/test_header/test_required_str.py b/tests/test_request_params/test_header/test_required_str.py index 85bb43d5a..20dd29657 100644 --- a/tests/test_request_params/test_header/test_required_str.py +++ b/tests/test_request_params/test_header/test_required_str.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict +from dirty_equals import AnyThing, IsOneOf, IsPartialDict from fastapi import FastAPI, Header from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -49,29 +49,16 @@ def test_required_str_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "p"], - "msg": "Field required", - "input": AnyThing, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p"], + "msg": "Field required", + "input": AnyThing, + } + ] + } @pytest.mark.parametrize( @@ -126,29 +113,16 @@ def test_required_alias_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "p_alias"], - "msg": "Field required", - "input": AnyThing, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": AnyThing, + } + ] + } @pytest.mark.parametrize( @@ -162,29 +136,16 @@ def test_required_alias_by_name(path: str): client = TestClient(app) response = client.get(path, headers={"p": "hello"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, IsPartialDict({"p": "hello"})), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": "hello"})), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_query/test_list.py b/tests/test_request_params/test_query/test_list.py index dc21a8500..ed2ea6c80 100644 --- a/tests/test_request_params/test_query/test_list.py +++ b/tests/test_request_params/test_query/test_list.py @@ -135,29 +135,16 @@ def test_required_list_alias_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -171,29 +158,16 @@ def test_required_list_alias_by_name(path: str): client = TestClient(app) response = client.get(f"{path}?p=hello&p=world") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {"p": ["hello", "world"]}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_query/test_optional_list.py b/tests/test_request_params/test_query/test_optional_list.py index 2a8f63a36..351e03a71 100644 --- a/tests/test_request_params/test_query/test_optional_list.py +++ b/tests/test_request_params/test_query/test_optional_list.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Query from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -36,29 +35,18 @@ async def read_model_optional_list_str( ) def test_optional_list_str_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P", - }, - "name": "p", - "in": "query", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"items": {"type": "string"}, "type": "array", "title": "P"}, - "name": "p", - "in": "query", - } - ) + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + "name": "p", + "in": "query", + } ] @@ -112,33 +100,18 @@ async def read_model_optional_list_alias( ) def test_optional_list_str_alias_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Alias", - }, - "name": "p_alias", - "in": "query", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": { - "items": {"type": "string"}, - "type": "array", - "title": "P Alias", - }, - "name": "p_alias", - "in": "query", - } - ) + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + "name": "p_alias", + "in": "query", + } ] diff --git a/tests/test_request_params/test_query/test_optional_str.py b/tests/test_request_params/test_query/test_optional_str.py index c6a70bc28..12e1b465a 100644 --- a/tests/test_request_params/test_query/test_optional_str.py +++ b/tests/test_request_params/test_query/test_optional_str.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Query from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -32,26 +31,15 @@ async def read_model_optional_str(p: Annotated[QueryModelOptionalStr, Query()]): ) def test_optional_str_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P", - }, - "name": "p", - "in": "query", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"title": "P", "type": "string"}, - "name": "p", - "in": "query", - } - ) + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "query", + } ] @@ -103,26 +91,15 @@ async def read_model_optional_alias(p: Annotated[QueryModelOptionalAlias, Query( ) def test_optional_str_alias_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Alias", - }, - "name": "p_alias", - "in": "query", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"title": "P Alias", "type": "string"}, - "name": "p_alias", - "in": "query", - } - ) + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "query", + } ] diff --git a/tests/test_request_params/test_query/test_required_str.py b/tests/test_request_params/test_query/test_required_str.py index 2ef1b0373..9e7b96145 100644 --- a/tests/test_request_params/test_query/test_required_str.py +++ b/tests/test_request_params/test_query/test_required_str.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import FastAPI, Query from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -49,29 +49,16 @@ def test_required_str_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "p"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -126,29 +113,16 @@ def test_required_alias_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -162,32 +136,19 @@ def test_required_alias_by_name(path: str): client = TestClient(app) response = client.get(f"{path}?p=hello") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "p_alias"], - "msg": "Field required", - "input": IsOneOf( - None, - {"p": "hello"}, - ), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + {"p": "hello"}, + ), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index 8f5195ba1..ac8999c90 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -1,7 +1,6 @@ from typing import Union import pytest -from dirty_equals import IsDict from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient @@ -336,28 +335,13 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - {"data": "Data in Body examples, example1"}, - {"data": "Data in Body examples, example2"}, - ], - } - ) - | IsDict( - # TODO: remove this when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - {"data": "Data in Body examples, example1"}, - {"data": "Data in Body examples, example2"}, - ], - } - ) + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + } } }, "required": True, @@ -387,28 +371,13 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - {"data": "examples example_examples 1"}, - {"data": "examples example_examples 2"}, - ], - } - ) - | IsDict( - # TODO: remove this when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - {"data": "examples example_examples 1"}, - {"data": "examples example_examples 2"}, - ], - }, - ), + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], + }, "example": {"data": "Overridden example"}, } }, @@ -539,16 +508,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - {"title": "Data", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + }, "example": "query1", "name": "data", "in": "query", @@ -579,21 +542,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["query1", "query2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "type": "string", - "title": "Data", - "examples": ["query1", "query2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + }, "name": "data", "in": "query", } @@ -623,21 +576,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["query1", "query2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "type": "string", - "title": "Data", - "examples": ["query1", "query2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + }, "example": "query_overridden", "name": "data", "in": "query", @@ -668,16 +611,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - {"title": "Data", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + }, "example": "header1", "name": "data", "in": "header", @@ -708,21 +645,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["header1", "header2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "type": "string", - "title": "Data", - "examples": ["header1", "header2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + }, "name": "data", "in": "header", } @@ -752,21 +679,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["header1", "header2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "title": "Data", - "type": "string", - "examples": ["header1", "header2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + }, "example": "header_overridden", "name": "data", "in": "header", @@ -797,16 +714,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - {"title": "Data", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + }, "example": "cookie1", "name": "data", "in": "cookie", @@ -837,21 +748,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["cookie1", "cookie2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "title": "Data", - "type": "string", - "examples": ["cookie1", "cookie2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + }, "name": "data", "in": "cookie", } @@ -881,21 +782,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["cookie1", "cookie2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "title": "Data", - "type": "string", - "examples": ["cookie1", "cookie2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + }, "example": "cookie_overridden", "name": "data", "in": "cookie", diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 804e4152d..7ad936995 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -1,5 +1,4 @@ import pytest -from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient @@ -64,79 +63,43 @@ def test_security_oauth2_password_bearer_no_header(): def test_strict_login_no_data(): response = client.post("/login") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_strict_login_no_grant_type(): response = client.post("/login", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -153,31 +116,17 @@ def test_strict_login_incorrect_grant_type(grant_type: str): data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["body", "grant_type"], - "msg": "String should match pattern '^password$'", - "input": grant_type, - "ctx": {"pattern": "^password$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "^password$"', - "type": "value_error.str.regex", - "ctx": {"pattern": "^password$"}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, + } + ] + } def test_strict_login_correct_grant_type(): @@ -264,26 +213,14 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index 046ac5763..57c16058a 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -1,7 +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 from fastapi.testclient import TestClient @@ -67,79 +66,43 @@ def test_security_oauth2_password_bearer_no_header(): def test_strict_login_no_data(): response = client.post("/login") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_strict_login_no_grant_type(): response = client.post("/login", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -156,31 +119,17 @@ def test_strict_login_incorrect_grant_type(grant_type: str): data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["body", "grant_type"], - "msg": "String should match pattern '^password$'", - "input": grant_type, - "ctx": {"pattern": "^password$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "^password$"', - "type": "value_error.str.regex", - "ctx": {"pattern": "^password$"}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, + } + ] + } def test_strict_login_correct_data(): @@ -267,26 +216,14 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index 629cddca2..60c6c242e 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -1,7 +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 from fastapi.testclient import TestClient @@ -68,79 +67,43 @@ def test_security_oauth2_password_bearer_no_header(): def test_strict_login_None(): response = client.post("/login", data=None) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_strict_login_no_grant_type(): response = client.post("/login", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -157,31 +120,17 @@ def test_strict_login_incorrect_grant_type(grant_type: str): data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["body", "grant_type"], - "msg": "String should match pattern '^password$'", - "input": grant_type, - "ctx": {"pattern": "^password$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "^password$"', - "type": "value_error.str.regex", - "ctx": {"pattern": "^password$"}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, + } + ] + } def test_strict_login_correct_correct_grant_type(): @@ -268,26 +217,14 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index ed7f4efe8..cc7e5f5c6 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -1,6 +1,5 @@ from typing import Optional -from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, HttpUrl @@ -99,30 +98,18 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "title": "Callback Url", - "anyOf": [ - { - "type": "string", - "format": "uri", - "minLength": 1, - "maxLength": 2083, - }, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - } - ), + "schema": { + "title": "Callback Url", + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + }, "name": "callback_url", "in": "query", } @@ -262,16 +249,10 @@ def test_openapi_schema(): "type": "object", "properties": { "id": {"title": "Id", "type": "string"}, - "title": IsDict( - { - "title": "Title", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Title", "type": "string"} - ), + "title": { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "customer": {"title": "Customer", "type": "string"}, "total": {"title": "Total", "type": "number"}, }, diff --git a/tests/test_tuples.py b/tests/test_tuples.py index fbc69a614..d3c89045b 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel @@ -125,31 +124,16 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "title": "Square", - "maxItems": 2, - "minItems": 2, - "type": "array", - "prefixItems": [ - {"$ref": "#/components/schemas/Coordinate"}, - {"$ref": "#/components/schemas/Coordinate"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Square", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [ - {"$ref": "#/components/schemas/Coordinate"}, - {"$ref": "#/components/schemas/Coordinate"}, - ], - } - ) + "schema": { + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], + } } }, "required": True, @@ -212,28 +196,16 @@ def test_openapi_schema(): "required": ["values"], "type": "object", "properties": { - "values": IsDict( - { - "title": "Values", - "maxItems": 2, - "minItems": 2, - "type": "array", - "prefixItems": [ - {"type": "integer"}, - {"type": "integer"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Values", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "integer"}, {"type": "integer"}], - } - ) + "values": { + "title": "Values", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "integer"}, + {"type": "integer"}, + ], + } }, }, "Coordinate": { @@ -264,26 +236,15 @@ def test_openapi_schema(): "items": { "title": "Items", "type": "array", - "items": IsDict( - { - "maxItems": 2, - "minItems": 2, - "type": "array", - "prefixItems": [ - {"type": "string"}, - {"type": "string"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "string"}, {"type": "string"}], - } - ), + "items": { + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "string"}, + {"type": "string"}, + ], + }, } }, }, diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index bbcad8f29..820860595 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -3,7 +3,6 @@ import os import shutil import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from tests.utils import needs_py310 @@ -80,16 +79,10 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "boolean"}, {"type": "null"}], - "title": "Img", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Img", "type": "boolean"} - ), + "schema": { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + }, "name": "img", "in": "query", }, diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index cbd4fff7d..c6abf5e46 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -3,7 +3,6 @@ import os import shutil import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from tests.utils import needs_py310 @@ -83,16 +82,10 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "boolean"}, {"type": "null"}], - "title": "Img", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Img", "type": "boolean"} - ), + "schema": { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + }, "name": "img", "in": "query", }, diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py index 2d1d1b03c..a164bb80b 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py @@ -1,5 +1,5 @@ -from dirty_equals import IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.behind_a_proxy.tutorial003_py39 import app @@ -15,40 +15,34 @@ def test_main(): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "servers": [ - {"url": "/api/v1"}, - { - "url": IsOneOf( - "https://stag.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://stag.example.com", - ), - "description": "Staging environment", - }, - { - "url": IsOneOf( - "https://prod.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://prod.example.com", - ), - "description": "Production environment", - }, - ], - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + {"url": "/api/v1"}, + { + "url": "https://stag.example.com", + "description": "Staging environment", + }, + { + "url": "https://prod.example.com", + "description": "Production environment", + }, + ], + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py index e8a03e811..01bba9fed 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py @@ -1,5 +1,5 @@ -from dirty_equals import IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.behind_a_proxy.tutorial004_py39 import app @@ -15,39 +15,33 @@ def test_main(): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "servers": [ - { - "url": IsOneOf( - "https://stag.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://stag.example.com", - ), - "description": "Staging environment", - }, - { - "url": IsOneOf( - "https://prod.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://prod.example.com", - ), - "description": "Production environment", - }, - ], - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + { + "url": "https://stag.example.com", + "description": "Staging environment", + }, + { + "url": "https://prod.example.com", + "description": "Production environment", + }, + ], + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index 7493a9e66..f5e243b95 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -28,29 +27,16 @@ def test_users_token_jessica(client: TestClient): def test_users_with_no_token(client: TestClient): response = client.get("/users") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_users_foo_token_jessica(client: TestClient): @@ -62,29 +48,16 @@ def test_users_foo_token_jessica(client: TestClient): def test_users_foo_with_no_token(client: TestClient): response = client.get("/users/foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_users_me_token_jessica(client: TestClient): @@ -96,29 +69,16 @@ def test_users_me_token_jessica(client: TestClient): def test_users_me_with_no_token(client: TestClient): response = client.get("/users/me") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_users_token_monica_with_no_jessica(client: TestClient): @@ -141,29 +101,16 @@ def test_items_token_jessica(client: TestClient): def test_items_with_no_token_jessica(client: TestClient): response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_items_plumbus_token_jessica(client: TestClient): @@ -187,29 +134,16 @@ def test_items_plumbus_with_no_token(client: TestClient): "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} ) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_items_with_invalid_token(client: TestClient): @@ -227,57 +161,31 @@ def test_items_bar_with_invalid_token(client: TestClient): def test_items_with_missing_x_token_header(client: TestClient): response = client.get("/items?token=jessica") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + } + ] + } def test_items_plumbus_with_missing_x_token_header(client: TestClient): response = client.get("/items/plumbus?token=jessica") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + } + ] + } def test_root_token_jessica(client: TestClient): @@ -289,68 +197,37 @@ def test_root_token_jessica(client: TestClient): def test_root_with_no_token(client: TestClient): response = client.get("/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_put_no_header(client: TestClient): response = client.put("/items/foo") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + }, + ] + } def test_put_invalid_header(client: TestClient): diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index 6aa9f2593..5a7cae160 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -2,7 +2,6 @@ import importlib from unittest.mock import patch import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -74,124 +73,67 @@ def test_post_with_str_float_description_tax(client: TestClient): def test_post_with_only_name(client: TestClient): response = client.post("/items/", json={"name": "Foo"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "price"], - "msg": "Field required", - "input": {"name": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {"name": "Foo"}, + } + ] + } def test_post_with_only_name_price(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": "twenty"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["body", "price"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "twenty", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "twenty", + } + ] + } def test_post_with_no_data(client: TestClient): response = client.post("/items/", json={}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "name"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "price"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + }, + ] + } def test_post_with_none(client: TestClient): response = client.post("/items/", json=None) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_broken_body(client: TestClient): @@ -201,67 +143,32 @@ def test_post_broken_body(client: TestClient): content="{some broken json}", ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "json_invalid", - "loc": ["body", 1], - "msg": "JSON decode error", - "input": {}, - "ctx": { - "error": "Expecting property name enclosed in double quotes" - }, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", 1], - "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", - "type": "value_error.jsondecode", - "ctx": { - "msg": "Expecting property name enclosed in double quotes", - "doc": "{some broken json}", - "pos": 1, - "lineno": 1, - "colno": 2, - }, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "json_invalid", + "loc": ["body", 1], + "msg": "JSON decode error", + "input": {}, + "ctx": {"error": "Expecting property name enclosed in double quotes"}, + } + ] + } def test_post_form_for_json(client: TestClient): response = client.post("/items/", data={"name": "Foo", "price": 50.5}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": "name=Foo&price=50.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": "name=Foo&price=50.5", + } + ] + } def test_explicit_content_type(client: TestClient): @@ -302,84 +209,46 @@ def test_wrong_headers(client: TestClient): "/items/", content=data, headers={"Content-Type": "text/plain"} ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": '{"name": "Foo", "price": 50.5}', - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + } + ] + } response = client.post( "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": '{"name": "Foo", "price": 50.5}', - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + } + ] + } + response = client.post( "/items/", content=data, headers={"Content-Type": "application/not-really-json"} ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": '{"name": "Foo", "price": 50.5}', - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + } + ] + } def test_other_exceptions(client: TestClient): @@ -435,26 +304,14 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001.py b/tests/test_tutorial/test_body_fields/test_tutorial001.py index d54ec7191..0ecadbb66 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -59,31 +58,17 @@ def test_items_6(client: TestClient): def test_invalid_price(client: TestClient): response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["body", "item", "price"], - "msg": "Input should be greater than 0", - "input": -3.0, - "ctx": {"gt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + } + ] + } def test_openapi_schema(client: TestClient): @@ -142,39 +127,23 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "The description of the item", - "anyOf": [ - {"maxLength": 300, "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - } - ), + "description": { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + }, "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index 2035cf944..63c9c16d6 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -53,29 +52,16 @@ def test_post_no_body(client: TestClient): def test_post_id_foo(client: TestClient): response = client.put("/items/foo", json=None) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_openapi_schema(client: TestClient): @@ -119,16 +105,10 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, "name": "q", "in": "query", }, @@ -136,19 +116,13 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/Item"}, - {"type": "null"}, - ], - "title": "Item", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/Item"} - ) + "schema": { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } } } }, @@ -163,27 +137,15 @@ def test_openapi_schema(client: TestClient): "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"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "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"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { 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 d3e6401af..76b7ff709 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -49,101 +48,55 @@ def test_post_body_valid(client: TestClient): 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", - }, - ] - } - ) + assert response.json() == { + "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, + }, + ] + } 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", - }, - ] - } - ) + assert response.json() == { + "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, + }, + ] + } def test_openapi_schema(client: TestClient): @@ -202,27 +155,15 @@ def test_openapi_schema(client: TestClient): "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"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "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"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "User": { @@ -231,16 +172,10 @@ def test_openapi_schema(client: TestClient): "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"} - ), + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py index db9f04546..f2e56d40f 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -29,29 +28,16 @@ def test_post_invalid_body(client: TestClient): data = {"foo": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "foo", "[key]"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "__key__"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "foo", "[key]"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_openapi_schema(client: TestClient): diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py index 265dee944..ac8e7bdae 100644 --- a/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -54,30 +53,16 @@ def test_cookie_param_model_invalid(client: TestClient): response = client.get("/items/") assert response.status_code == 422 assert response.json() == snapshot( - IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["cookie", "session_id"], - "msg": "Field required", - "input": {}, - } - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "type": "value_error.missing", - "loc": ["cookie", "session_id"], - "msg": "field required", - } - ] - } - ) + { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "session_id"], + "msg": "Field required", + "input": {}, + } + ] + } ) @@ -115,37 +100,19 @@ def test_openapi_schema(client: TestClient): "name": "fatebook_tracker", "in": "cookie", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Fatebook Tracker", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Fatebook Tracker", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Fatebook Tracker", + }, }, { "name": "googall_tracker", "in": "cookie", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Googall Tracker", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Googall Tracker", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Googall Tracker", + }, }, ], "responses": { diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py index 0fbf141e0..d7c3d15f1 100644 --- a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -72,30 +71,16 @@ def test_cookie_param_model_extra(client: TestClient): response = c.get("/items/") assert response.status_code == 422 assert response.json() == snapshot( - IsDict( - { - "detail": [ - { - "type": "extra_forbidden", - "loc": ["cookie", "extra"], - "msg": "Extra inputs are not permitted", - "input": "track-me-here-too", - } - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "type": "value_error.extra", - "loc": ["cookie", "extra"], - "msg": "extra fields not permitted", - } - ] - } - ) + { + "detail": [ + { + "type": "extra_forbidden", + "loc": ["cookie", "extra"], + "msg": "Extra inputs are not permitted", + "input": "track-me-here-too", + } + ] + } ) @@ -134,19 +119,10 @@ def test_openapi_schema(client: TestClient): "name": "googall_tracker", "in": "cookie", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Googall Tracker", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Googall Tracker", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Googall Tracker", + }, }, ], "responses": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index a65249d65..9b47cbc67 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -2,7 +2,6 @@ import importlib from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -75,16 +74,10 @@ def test_openapi_schema(mod: ModuleType): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Ads Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Ads Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + }, "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py index 643011637..a9c7ae638 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py @@ -1,7 +1,7 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from tests.utils import needs_py310 @@ -30,34 +30,17 @@ def test_endpoint_works(client: TestClient): def test_exception_handler_body_access(client: TestClient): response = client.post("/", json={"numbers": [1, 2, 3]}) - assert response.json() == IsDict( - { - "detail": { - "errors": [ - { - "type": "list_type", - "loc": ["body"], - "msg": "Input should be a valid list", - "input": {"numbers": [1, 2, 3]}, - } - ], - # httpx 0.28.0 switches to compact JSON https://github.com/encode/httpx/issues/3363 - "body": IsOneOf('{"numbers": [1, 2, 3]}', '{"numbers":[1,2,3]}'), - } + assert response.json() == { + "detail": { + "errors": [ + { + "type": "list_type", + "loc": ["body"], + "msg": "Input should be a valid list", + "input": {"numbers": [1, 2, 3]}, + } + ], + # httpx 0.28.0 switches to compact JSON https://github.com/encode/httpx/issues/3363 + "body": IsOneOf('{"numbers": [1, 2, 3]}', '{"numbers":[1,2,3]}'), } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": { - # httpx 0.28.0 switches to compact JSON https://github.com/encode/httpx/issues/3363 - "body": IsOneOf('{"numbers": [1, 2, 3]}', '{"numbers":[1,2,3]}'), - "errors": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ], - } - } - ) + } diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index bc407234a..4683062f5 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from tests.utils import needs_py310 @@ -36,29 +35,16 @@ def test_post_item(client: TestClient): def test_post_invalid_item(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": "invalid price"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["body", "price"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "invalid price", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "invalid price", + } + ] + } def test_openapi_schema(client: TestClient): @@ -119,26 +105,14 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 995d92675..210d743bb 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from tests.utils import needs_py310 @@ -37,77 +37,53 @@ def test_get_item(client: TestClient): def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/next": { - "get": { - "summary": "Read Next Item", - "operationId": "read_next_item_items_next_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/next": { + "get": { + "summary": "Read Next Item", + "operationId": "read_next_item_items_next_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + } + }, + } } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "price", "tags", "description", "tax"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tags": IsDict( - { + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tags": { "title": "Tags", "type": "array", "items": {"type": "string"}, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - } - ), - "description": IsDict( - { + }, + "description": { "title": "Description", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": IsDict( - { + }, + "tax": { "title": "Tax", "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, + }, + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py index 46f0066f9..59202df3b 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -22,40 +21,22 @@ def get_client(request: pytest.FixtureRequest): def test_get_no_headers(client: TestClient): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + }, + ] + } def test_get_invalid_one_header(client: TestClient): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012.py b/tests/test_tutorial/test_dependencies/test_tutorial012.py index b791ee0aa..d5599ac73 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -22,79 +21,43 @@ def get_client(request: pytest.FixtureRequest): def test_get_no_headers_items(client: TestClient): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + }, + ] + } def test_get_no_headers_users(client: TestClient): response = client.get("/users/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + }, + ] + } def test_get_invalid_one_header_items(client: TestClient): diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py index e11f73fe3..5479e2925 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -47,146 +47,117 @@ def test_extra_types(client: TestClient): 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": {}}}, + assert response.json() == snapshot( + { + "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" + } + } + }, + }, }, - "422": { - "description": "Validation Error", + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "required": True, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" } } }, }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ) - } - }, - }, + } } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": IsDict( - { + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { "title": "Repeat At", "anyOf": [ {"type": "string", "format": "time"}, {"type": "null"}, ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Repeat At", - "type": "string", - "format": "time", - } - ), - "process_after": IsDict( - { + }, + "process_after": { "title": "Process After", "type": "string", "format": "duration", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Process After", - "type": "number", - "format": "time-delta", - } - ), - }, - "required": ["start_datetime", "end_datetime", "process_after"], - }, - "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"}, + "required": ["start_datetime", "end_datetime", "process_after"], }, - }, - "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"}, + }, }, - }, - } - }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py index 3aa83c0c4..872af5383 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -43,107 +43,115 @@ def test_get_plane(client: TestClient): 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}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Item Items Item Id Get", - "anyOf": [ - {"$ref": "#/components/schemas/PlaneItem"}, - {"$ref": "#/components/schemas/CarItem"}, - ], + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Item Items Item Id Get", + "anyOf": [ + { + "$ref": "#/components/schemas/PlaneItem" + }, + { + "$ref": "#/components/schemas/CarItem" + }, + ], + } } - } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } } - } - }, - "components": { - "schemas": { - "PlaneItem": { - "title": "PlaneItem", - "required": IsOneOf( - ["description", "type", "size"], - # TODO: remove when deprecating Pydantic v1 - ["description", "size"], - ), - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "plane"}, - "size": {"title": "Size", "type": "integer"}, + }, + "components": { + "schemas": { + "PlaneItem": { + "title": "PlaneItem", + "required": ["description", "size"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": { + "title": "Type", + "type": "string", + "default": "plane", + }, + "size": {"title": "Size", "type": "integer"}, + }, }, - }, - "CarItem": { - "title": "CarItem", - "required": IsOneOf( - ["description", "type"], - # TODO: remove when deprecating Pydantic v1 - ["description"], - ), - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "car"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "CarItem": { + "title": "CarItem", + "required": ["description"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": { + "title": "Type", + "type": "string", + "default": "car", }, }, - "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"}, - } + "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_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py index d713c5d87..7bd947f19 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.handling_errors.tutorial005_py39 import app @@ -9,31 +8,17 @@ client = TestClient(app) def test_post_validation_error(): response = client.post("/items/", json={"title": "towel", "size": "XL"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "size"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "XL", - } - ], - "body": {"title": "towel", "size": "XL"}, - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ], - "body": {"title": "towel", "size": "XL"}, - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "size"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "XL", + } + ], + "body": {"title": "towel", "size": "XL"}, + } def test_post(): diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py index 491e461b3..e95e53d5e 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial006.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial006.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.handling_errors.tutorial006_py39 import app @@ -9,29 +8,16 @@ client = TestClient(app) def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_get_http_error(): diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial001.py b/tests/test_tutorial/test_header_param_models/test_tutorial001.py index f59d50762..1fa8aee46 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -63,29 +62,19 @@ def test_header_param_model_invalid(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "missing", - "loc": ["header", "save_data"], - "msg": "Field required", - "input": { - "x_tag": [], - "host": "testserver", - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "user-agent": "testclient", - }, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.missing", - "loc": ["header", "save_data"], - "msg": "field required", - } - ) + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": { + "x_tag": [], + "host": "testserver", + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "connection": "keep-alive", + "user-agent": "testclient", + }, + } ] } ) @@ -136,37 +125,19 @@ def test_openapi_schema(client: TestClient): "name": "if-modified-since", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "If Modified Since", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "If Modified Since", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + }, }, { "name": "traceparent", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Traceparent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Traceparent", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + }, }, { "name": "x-tag", diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial002.py b/tests/test_tutorial/test_header_param_models/test_tutorial002.py index ed4743ebf..079a8f540 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial002.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -64,22 +63,12 @@ def test_header_param_model_invalid(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "missing", - "loc": ["header", "save_data"], - "msg": "Field required", - "input": {"x_tag": [], "host": "testserver"}, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.missing", - "loc": ["header", "save_data"], - "msg": "field required", - } - ) + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": {"x_tag": [], "host": "testserver"}, + } ] } ) @@ -93,22 +82,12 @@ def test_header_param_model_extra(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "extra_forbidden", - "loc": ["header", "tool"], - "msg": "Extra inputs are not permitted", - "input": "plumbus", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.extra", - "loc": ["header", "tool"], - "msg": "extra fields not permitted", - } - ) + { + "type": "extra_forbidden", + "loc": ["header", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } ] } ) @@ -143,37 +122,19 @@ def test_openapi_schema(client: TestClient): "name": "if-modified-since", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "If Modified Since", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "If Modified Since", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + }, }, { "name": "traceparent", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Traceparent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Traceparent", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + }, }, { "name": "x-tag", diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial003.py b/tests/test_tutorial/test_header_param_models/test_tutorial003.py index 947587504..4c89d80ee 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial003.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial003.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -60,33 +59,23 @@ def test_header_param_model_no_underscore(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "missing", - "loc": ["header", "save_data"], - "msg": "Field required", - "input": { - "host": "testserver", - "traceparent": "123", - "x_tag": [], - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "user-agent": "testclient", - "save-data": "true", - "if-modified-since": "yesterday", - "x-tag": ["one", "two"], - }, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.missing", - "loc": ["header", "save_data"], - "msg": "field required", - } - ) + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": { + "host": "testserver", + "traceparent": "123", + "x_tag": [], + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "connection": "keep-alive", + "user-agent": "testclient", + "save-data": "true", + "if-modified-since": "yesterday", + "x-tag": ["one", "two"], + }, + } ] } ) @@ -110,29 +99,19 @@ def test_header_param_model_invalid(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "missing", - "loc": ["header", "save_data"], - "msg": "Field required", - "input": { - "x_tag": [], - "host": "testserver", - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "user-agent": "testclient", - }, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.missing", - "loc": ["header", "save_data"], - "msg": "field required", - } - ) + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": { + "x_tag": [], + "host": "testserver", + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "connection": "keep-alive", + "user-agent": "testclient", + }, + } ] } ) @@ -183,37 +162,19 @@ def test_openapi_schema(client: TestClient): "name": "if_modified_since", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "If Modified Since", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "If Modified Since", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + }, }, { "name": "traceparent", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Traceparent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Traceparent", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + }, }, { "name": "x_tag", diff --git a/tests/test_tutorial/test_header_params/test_tutorial001.py b/tests/test_tutorial/test_header_params/test_tutorial001.py index beaf917f9..88591b822 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -67,16 +66,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User-Agent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User-Agent", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + }, "name": "user-agent", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py index b892ff905..229f96c1f 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -78,16 +77,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Strange Header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Strange Header", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + }, "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py index ef7624415..cf067ccf9 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -56,23 +55,13 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "title": "X-Token", - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - } - ), + "schema": { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + }, "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index 975e07cbd..6fde96cb5 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -2,7 +2,6 @@ import importlib from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from tests.utils import needs_py310 @@ -55,30 +54,18 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [ - { - "type": "string", - "format": "uri", - "minLength": 1, - "maxLength": 2083, - }, - {"type": "null"}, - ], - "title": "Callback Url", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - } - ), + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + "title": "Callback Url", + }, "name": "callback_url", "in": "query", } @@ -171,16 +158,10 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "id": {"title": "Id", "type": "string"}, - "title": IsDict( - { - "title": "Title", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Title", "type": "string"} - ), + "title": { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "customer": {"title": "Customer", "type": "string"}, "total": {"title": "Total", "type": "number"}, }, diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index b3be70471..86ccce7b6 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_params.tutorial005_py39 import app @@ -27,31 +26,17 @@ def test_get_enums_resnet(): def test_get_enums_invalid(): response = client.get("/models/foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "enum", - "loc": ["path", "model_name"], - "msg": "Input should be 'alexnet', 'resnet' or 'lenet'", - "input": "foo", - "ctx": {"expected": "'alexnet', 'resnet' or 'lenet'"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]}, - "loc": ["path", "model_name"], - "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'", - "type": "type_error.enum", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "enum", + "loc": ["path", "model_name"], + "msg": "Input should be 'alexnet', 'resnet' or 'lenet'", + "input": "foo", + "ctx": {"expected": "'alexnet', 'resnet' or 'lenet'"}, + } + ] + } def test_openapi_schema(): @@ -106,22 +91,11 @@ def test_openapi_schema(): } }, }, - "ModelName": IsDict( - { - "title": "ModelName", - "enum": ["alexnet", "resnet", "lenet"], - "type": "string", - } - ) - | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "title": "ModelName", - "enum": ["alexnet", "resnet", "lenet"], - "type": "string", - "description": "An enumeration.", - } - ), + "ModelName": { + "title": "ModelName", + "enum": ["alexnet", "resnet", "lenet"], + "type": "string", + }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial001.py b/tests/test_tutorial/test_query_param_models/test_tutorial001.py index 86830b934..d3ce57121 100644 --- a/tests/test_tutorial/test_query_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_query_param_models/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -65,61 +64,31 @@ def test_query_param_model_invalid(client: TestClient): ) assert response.status_code == 422 assert response.json() == snapshot( - IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["query", "limit"], - "msg": "Input should be less than or equal to 100", - "input": "150", - "ctx": {"le": 100}, - }, - { - "type": "greater_than_equal", - "loc": ["query", "offset"], - "msg": "Input should be greater than or equal to 0", - "input": "-1", - "ctx": {"ge": 0}, - }, - { - "type": "literal_error", - "loc": ["query", "order_by"], - "msg": "Input should be 'created_at' or 'updated_at'", - "input": "invalid", - "ctx": {"expected": "'created_at' or 'updated_at'"}, - }, - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "type": "value_error.number.not_le", - "loc": ["query", "limit"], - "msg": "ensure this value is less than or equal to 100", - "ctx": {"limit_value": 100}, - }, - { - "type": "value_error.number.not_ge", - "loc": ["query", "offset"], - "msg": "ensure this value is greater than or equal to 0", - "ctx": {"limit_value": 0}, - }, - { - "type": "value_error.const", - "loc": ["query", "order_by"], - "msg": "unexpected value; permitted: 'created_at', 'updated_at'", - "ctx": { - "given": "invalid", - "permitted": ["created_at", "updated_at"], - }, - }, - ] - } - ) + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["query", "limit"], + "msg": "Input should be less than or equal to 100", + "input": "150", + "ctx": {"le": 100}, + }, + { + "type": "greater_than_equal", + "loc": ["query", "offset"], + "msg": "Input should be greater than or equal to 0", + "input": "-1", + "ctx": {"ge": 0}, + }, + { + "type": "literal_error", + "loc": ["query", "order_by"], + "msg": "Input should be 'created_at' or 'updated_at'", + "input": "invalid", + "ctx": {"expected": "'created_at' or 'updated_at'"}, + }, + ] + } ) diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial002.py b/tests/test_tutorial/test_query_param_models/test_tutorial002.py index 0e9c3351a..96abce6ab 100644 --- a/tests/test_tutorial/test_query_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_query_param_models/test_tutorial002.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -65,61 +64,31 @@ def test_query_param_model_invalid(client: TestClient): ) assert response.status_code == 422 assert response.json() == snapshot( - IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["query", "limit"], - "msg": "Input should be less than or equal to 100", - "input": "150", - "ctx": {"le": 100}, - }, - { - "type": "greater_than_equal", - "loc": ["query", "offset"], - "msg": "Input should be greater than or equal to 0", - "input": "-1", - "ctx": {"ge": 0}, - }, - { - "type": "literal_error", - "loc": ["query", "order_by"], - "msg": "Input should be 'created_at' or 'updated_at'", - "input": "invalid", - "ctx": {"expected": "'created_at' or 'updated_at'"}, - }, - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "type": "value_error.number.not_le", - "loc": ["query", "limit"], - "msg": "ensure this value is less than or equal to 100", - "ctx": {"limit_value": 100}, - }, - { - "type": "value_error.number.not_ge", - "loc": ["query", "offset"], - "msg": "ensure this value is greater than or equal to 0", - "ctx": {"limit_value": 0}, - }, - { - "type": "value_error.const", - "loc": ["query", "order_by"], - "msg": "unexpected value; permitted: 'created_at', 'updated_at'", - "ctx": { - "given": "invalid", - "permitted": ["created_at", "updated_at"], - }, - }, - ] - } - ) + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["query", "limit"], + "msg": "Input should be less than or equal to 100", + "input": "150", + "ctx": {"le": 100}, + }, + { + "type": "greater_than_equal", + "loc": ["query", "offset"], + "msg": "Input should be greater than or equal to 0", + "input": "-1", + "ctx": {"ge": 0}, + }, + { + "type": "literal_error", + "loc": ["query", "order_by"], + "msg": "Input should be 'created_at' or 'updated_at'", + "input": "invalid", + "ctx": {"expected": "'created_at' or 'updated_at'"}, + }, + ] + } ) @@ -138,22 +107,12 @@ def test_query_param_model_extra(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "extra_forbidden", - "loc": ["query", "tool"], - "msg": "Extra inputs are not permitted", - "input": "plumbus", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.extra", - "loc": ["query", "tool"], - "msg": "extra fields not permitted", - } - ) + { + "type": "extra_forbidden", + "loc": ["query", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } ] } ) diff --git a/tests/test_tutorial/test_query_params/test_tutorial005.py b/tests/test_tutorial/test_query_params/test_tutorial005.py index ad4e4efa6..103078147 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params/test_tutorial005.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.query_params.tutorial005_py39 import app @@ -15,29 +14,16 @@ def test_foo_needy_very(): def test_foo_no_needy(): response = client.get("/items/foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "needy"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + } + ] + } def test_openapi_schema(): diff --git a/tests/test_tutorial/test_query_params/test_tutorial006.py b/tests/test_tutorial/test_query_params/test_tutorial006.py index 349f8dd22..157322c7e 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -35,51 +34,28 @@ def test_foo_needy_very(client: TestClient): def test_foo_no_needy(client: TestClient): response = client.get("/items/foo?skip=a&limit=b") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "needy"], - "msg": "Field required", - "input": None, - }, - { - "type": "int_parsing", - "loc": ["query", "skip"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "a", - }, - { - "type": "int_parsing", - "loc": ["query", "limit"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "b", - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["query", "skip"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "limit"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + }, + { + "type": "int_parsing", + "loc": ["query", "skip"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "a", + }, + { + "type": "int_parsing", + "loc": ["query", "limit"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "b", + }, + ] + } def test_openapi_schema(client: TestClient): @@ -134,16 +110,10 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Limit", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Limit", "type": "integer"} - ), + "schema": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Limit", + }, "name": "limit", "in": "query", }, 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 de5dbbb2e..00889c5bf 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,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE from fastapi.testclient import TestClient @@ -50,31 +49,17 @@ def test_query_params_str_validations_q_fixedquery(client: TestClient): 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", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + } + ] + } def test_openapi_schema(client: TestClient): @@ -109,38 +94,25 @@ def test_openapi_schema(client: TestClient): "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", - } - ), + "schema": { + "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 {} + ), + }, "name": "item-query", "in": "query", } 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 50b3c5683..11de33ae1 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,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -69,23 +68,13 @@ def test_openapi_schema(client: TestClient): "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"}, - } - ), + "schema": { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + }, "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index a16d951dc..e0e1bbe63 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -22,57 +21,31 @@ def get_client(request: pytest.FixtureRequest): 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", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + } + ] + } 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", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_file(tmp_path, client: TestClient): 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 caea0d2e8..18948c544 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -2,8 +2,8 @@ import importlib from pathlib import Path import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -59,166 +59,132 @@ def test_post_upload_file(tmp_path: Path, client: TestClient): 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 - { + assert response.json() == snapshot( + { + "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" } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } } + } + }, + "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 - { + } + }, + "/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" } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } } + } + }, + "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": { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + }, + }, + "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"}, }, }, } }, - }, - "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_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index 34dbbb985..03772419a 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient @@ -28,57 +27,31 @@ def get_client(app: FastAPI): 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", "files"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + } + ] + } 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", "files"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_files(tmp_path, app: FastAPI): diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001.py b/tests/test_tutorial/test_request_form_models/test_tutorial001.py index a54df8536..0c43dd7b2 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -28,135 +27,73 @@ def test_post_body_form(client: TestClient): def test_post_body_form_no_password(client: TestClient): response = client.post("/login/", data={"username": "Foo"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } def test_post_body_form_no_username(client: TestClient): response = client.post("/login/", data={"password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } def test_post_body_form_no_data(client: TestClient): response = client.post("/login/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } def test_openapi_schema(client: TestClient): diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001.py b/tests/test_tutorial/test_request_forms/test_tutorial001.py index da20535cf..4276414fc 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -28,135 +27,73 @@ def test_post_body_form(client: TestClient): def test_post_body_form_no_password(client: TestClient): response = client.post("/login/", data={"username": "Foo"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_body_form_no_username(client: TestClient): response = client.post("/login/", data={"password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_body_form_no_data(client: TestClient): response = client.post("/login/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_openapi_schema(client: TestClient): diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index f37ffad44..7fa4c3de5 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient @@ -28,140 +27,76 @@ def get_client(app: FastAPI): 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, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_form_no_file(client: TestClient): response = client.post("/files/", data={"token": "foo"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_file_no_token(tmp_path, app: FastAPI): @@ -172,40 +107,22 @@ def test_post_file_no_token(tmp_path, app: FastAPI): with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_files_and_token(tmp_path, app: FastAPI): diff --git a/tests/test_tutorial/test_response_model/test_tutorial003.py b/tests/test_tutorial/test_response_model/test_tutorial003.py index 0f9eac890..35ed5572d 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -42,125 +42,115 @@ def test_post_user(client: TestClient): 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": { - "/user/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserOut"} - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/user/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserOut" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "422": { - "description": "Validation Error", + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/UserIn"} } }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "UserOut": { + "title": "UserOut", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, - "summary": "Create User", - "operationId": "create_user_user__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserIn"} + "UserIn": { + "title": "UserIn", + "required": ["username", "password", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "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" + }, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "UserOut": { - "title": "UserOut", - "required": IsOneOf( - ["username", "email", "full_name"], - # TODO: remove when deprecating Pydantic v1 - ["username", "email"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "UserIn": { - "title": "UserIn", - "required": ["username", "password", "email"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py index 1a7ce4c7a..fa1eb6277 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -42,125 +42,115 @@ def test_post_user(client: TestClient): 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": { - "/user/": { - "post": { - "summary": "Create User", - "operationId": "create_user_user__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserIn"} + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/user/": { + "post": { + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseUser" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "BaseUser": { + "title": "BaseUser", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/BaseUser"} - } + "UserIn": { + "title": "UserIn", + "required": ["username", "email", "password"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "password": {"title": "Password", "type": "string"}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + "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"}, }, }, } - } - }, - "components": { - "schemas": { - "BaseUser": { - "title": "BaseUser", - "required": IsOneOf( - ["username", "email", "full_name"], - # TODO: remove when deprecating Pydantic v1 - ["username", "email"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "UserIn": { - "title": "UserIn", - "required": ["username", "email", "password"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py index 19f6998f7..9c0d95ebd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -50,104 +50,98 @@ def test_get(url, data, client: TestClient): 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}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} } - } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax", "tags"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { "title": "Description", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], }, }, - "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"}, - } + "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_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index 47d77dc49..63e8535db 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -40,132 +40,126 @@ def test_read_item_public_data(client: TestClient): 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}/name": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}/name": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} } - } + }, }, - }, - }, - "summary": "Read Item Name", - "operationId": "read_item_name_items__item_id__name_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/items/{item_id}/public": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read Item Public Data", - "operationId": "read_item_public_data_items__item_id__public_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( + "summary": "Read Item Name", + "operationId": "read_item_name_items__item_id__name_get", + "parameters": [ { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/items/{item_id}/public": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Public Data", + "operationId": "read_item_public_data_items__item_id__public_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { "title": "Description", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, - "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"}, - } + "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_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index a03aa41e8..08ab65952 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -40,132 +40,126 @@ def test_read_item_public_data(client: TestClient): 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}/name": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}/name": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} } - } + }, }, - }, - }, - "summary": "Read Item Name", - "operationId": "read_item_name_items__item_id__name_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/items/{item_id}/public": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read Item Public Data", - "operationId": "read_item_public_data_items__item_id__public_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( + "summary": "Read Item Name", + "operationId": "read_item_name_items__item_id__name_get", + "parameters": [ { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/items/{item_id}/public": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Public Data", + "operationId": "read_item_public_data_items__item_id__public_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { "title": "Description", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, - "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"}, - } + "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_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index 47ecb9ba7..9326e0629 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -58,46 +57,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } } }, "required": True, @@ -140,27 +115,15 @@ def test_openapi_schema(client: TestClient): "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"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "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"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py index 1c964f3d1..2d0dee48c 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -58,16 +57,7 @@ def test_openapi_schema(client: TestClient) -> None: "requestBody": { "content": { "application/json": { - "schema": IsDict({"$ref": "#/components/schemas/Item"}) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), + "schema": {"$ref": "#/components/schemas/Item"}, "examples": { "normal": { "summary": "A normal example", @@ -134,27 +124,15 @@ def test_openapi_schema(client: TestClient) -> None: "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"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "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"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index 000c8b2ac..6a786348c 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -144,23 +143,13 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "^password$", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "^password$", - "type": "string", - } - ), + "grant_type": { + "title": "Grant Type", + "anyOf": [ + {"pattern": "^password$", "type": "string"}, + {"type": "null"}, + ], + }, "username": {"title": "Username", "type": "string"}, "password": { "title": "Password", @@ -168,31 +157,15 @@ def test_openapi_schema(client: TestClient): "format": "password", }, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - "format": "password", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Client Secret", - "type": "string", - "format": "password", - } - ), + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + "format": "password", + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index 7953e8e3f..25b47f0ad 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -2,8 +2,8 @@ import importlib from types import ModuleType import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -215,240 +215,200 @@ def test_openapi_schema(mod: ModuleType): client = TestClient(mod.app) 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": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "422": { - "description": "Validation Error", + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { "content": { - "application/json": { + "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" } } }, + "required": True, }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, } }, - "required": True, - }, - } + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { "title": "Email", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { + }, + "full_name": { "title": "Full Name", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { + }, + "disabled": { "title": "Disabled", "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), + }, + }, }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { "title": "Grant Type", "anyOf": [ {"pattern": "^password$", "type": "string"}, {"type": "null"}, ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "^password$", + }, + "username": {"title": "Username", "type": "string"}, + "password": { + "title": "Password", "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": { - "title": "Password", - "type": "string", - "format": "password", - }, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { + "format": "password", + }, + "scope": { + "title": "Scope", + "type": "string", + "default": "", + }, + "client_id": { "title": "Client Id", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { + }, + "client_secret": { "title": "Client Secret", "anyOf": [{"type": "string"}, {"type": "null"}], "format": "password", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Client Secret", - "type": "string", - "format": "password", - } - ), - }, - }, - "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"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, - "tokenUrl": "token", - } + "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" + }, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, }, - }, - } + } + ) diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_sql_databases/test_tutorial001.py index e3e6bac12..2c628f525 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial001.py @@ -2,7 +2,7 @@ import importlib import warnings import pytest -from dirty_equals import IsDict, IsInt +from dirty_equals import IsInt from fastapi.testclient import TestClient from inline_snapshot import snapshot from sqlalchemy import StaticPool @@ -318,33 +318,15 @@ def test_openapi_schema(client: TestClient): }, "Hero": { "properties": { - "id": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Id", - } - ), + "id": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Id", + }, "name": {"type": "string", "title": "Name"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Age", - } - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "secret_name": {"type": "string", "title": "Secret Name"}, }, "type": "object", diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial002.py b/tests/test_tutorial/test_sql_databases/test_tutorial002.py index e3b8c7f9e..c72c16e9a 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial002.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial002.py @@ -2,7 +2,7 @@ import importlib import warnings import pytest -from dirty_equals import IsDict, IsInt +from dirty_equals import IsInt from fastapi.testclient import TestClient from inline_snapshot import Is, snapshot from sqlalchemy import StaticPool @@ -373,19 +373,10 @@ def test_openapi_schema(client: TestClient): "HeroCreate": { "properties": { "name": {"type": "string", "title": "Name"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Age", - } - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "secret_name": {"type": "string", "title": "Secret Name"}, }, "type": "object", @@ -395,19 +386,10 @@ def test_openapi_schema(client: TestClient): "HeroPublic": { "properties": { "name": {"type": "string", "title": "Name"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Age", - } - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "id": {"type": "integer", "title": "Id"}, }, "type": "object", @@ -416,45 +398,18 @@ def test_openapi_schema(client: TestClient): }, "HeroUpdate": { "properties": { - "name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Name", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Name", - } - ), - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Age", - } - ), - "secret_name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Secret Name", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Secret Name", - } - ), + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, + "secret_name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Secret Name", + }, }, "type": "object", "title": "HeroUpdate", diff --git a/tests/test_union_body.py b/tests/test_union_body.py index c15acacd1..ee7fcc423 100644 --- a/tests/test_union_body.py +++ b/tests/test_union_body.py @@ -1,6 +1,5 @@ from typing import Optional, Union -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -91,18 +90,12 @@ def test_openapi_schema(): "Item": { "title": "Item", "type": "object", - "properties": IsDict( - { - "name": { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } + "properties": { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"name": {"title": "Name", "type": "string"}} - ), + }, }, "ValidationError": { "title": "ValidationError", diff --git a/tests/test_union_body_discriminator.py b/tests/test_union_body_discriminator.py index 40fd0065a..6c31649bc 100644 --- a/tests/test_union_body_discriminator.py +++ b/tests/test_union_body_discriminator.py @@ -1,6 +1,5 @@ from typing import Annotated, Any, Union -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -90,21 +89,11 @@ def test_discriminator_pydantic_v2() -> None: "description": "Successful Response", "content": { "application/json": { - "schema": IsDict( - { - # Pydantic 2.10, in Python 3.8 - # TODO: remove when dropping support for Python 3.8 - "type": "object", - "title": "Response Save Union Body Discriminator Items Post", - } - ) - | IsDict( - { - "type": "object", - "additionalProperties": True, - "title": "Response Save Union Body Discriminator Items Post", - } - ) + "schema": { + "type": "object", + "additionalProperties": True, + "title": "Response Save Union Body Discriminator Items Post", + } } }, }, diff --git a/tests/test_union_inherited_body.py b/tests/test_union_inherited_body.py index ef75d459e..3c062e7f5 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -1,6 +1,5 @@ from typing import Optional, Union -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -86,16 +85,10 @@ def test_openapi_schema(): "title": "Item", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ) + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } }, }, "ExtendedItem": { @@ -103,16 +96,10 @@ def test_openapi_schema(): "required": ["age"], "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "age": {"title": "Age", "type": "integer"}, }, }, From ded035a421a9eb11d23b51016b626bbe2433493f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 18:19:33 +0000 Subject: [PATCH 024/243] =?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 ef7894037..8d080d6c5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 🔥 Remove Pydantic v1 specific test variants. PR [#14611](https://github.com/fastapi/fastapi/pull/14611) by [@tiangolo](https://github.com/tiangolo). + ## 0.128.0 ### Breaking Changes From 1b42639296b47f1344e24c1789f649caabbee67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 10:31:34 -0800 Subject: [PATCH 025/243] =?UTF-8?q?=F0=9F=94=A5=20Remove=20test=20variants?= =?UTF-8?q?=20for=20Pydantic=20v1=20in=20test=5Frequest=5Fparams=20(#14612?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_body/test_list.py | 34 ++---- .../test_file/test_list.py | 105 ++++-------------- .../test_form/test_list.py | 34 ++---- .../test_header/test_list.py | 34 ++---- .../test_query/test_list.py | 34 ++---- 5 files changed, 64 insertions(+), 177 deletions(-) diff --git a/tests/test_request_params/test_body/test_list.py b/tests/test_request_params/test_body/test_list.py index 50847335c..970e6a660 100644 --- a/tests/test_request_params/test_body/test_list.py +++ b/tests/test_request_params/test_body/test_list.py @@ -1,7 +1,7 @@ from typing import Annotated, Union import pytest -from dirty_equals import IsDict, IsOneOf, IsPartialDict +from dirty_equals import IsOneOf, IsPartialDict from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -59,28 +59,16 @@ def test_required_list_str_missing(path: str, json: Union[dict, None]): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": IsOneOf(["body", "p"], ["body"]), - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - { - "detail": [ - { - "loc": IsOneOf(["body", "p"], ["body"]), - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_file/test_list.py b/tests/test_request_params/test_file/test_list.py index f09653255..68280fcf3 100644 --- a/tests/test_request_params/test_file/test_list.py +++ b/tests/test_request_params/test_file/test_list.py @@ -1,7 +1,6 @@ from typing import Annotated import pytest -from dirty_equals import IsDict from fastapi import FastAPI, File, UploadFile from fastapi.testclient import TestClient @@ -36,27 +35,11 @@ def test_list_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p": ( - IsDict( - { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P", - }, - ) - | IsDict( - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - "title": "P", - }, - ) - ) + "p": { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P", + }, }, "required": ["p"], "title": body_model_name, @@ -130,27 +113,11 @@ def test_list_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_alias": ( - IsDict( - { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P Alias", - }, - ) - | IsDict( - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - "title": "P Alias", - }, - ) - ) + "p_alias": { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P Alias", + }, }, "required": ["p_alias"], "title": body_model_name, @@ -252,27 +219,11 @@ def test_list_validation_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_val_alias": ( - IsDict( - { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P Val Alias", - }, - ) - | IsDict( - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - "title": "P Val Alias", - }, - ) - ) + "p_val_alias": { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P Val Alias", + }, }, "required": ["p_val_alias"], "title": body_model_name, @@ -385,27 +336,11 @@ def test_list_alias_and_validation_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_val_alias": ( - IsDict( - { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P Val Alias", - }, - ) - | IsDict( - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - "title": "P Val Alias", - }, - ) - ) + "p_val_alias": { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P Val Alias", + }, }, "required": ["p_val_alias"], "title": body_model_name, diff --git a/tests/test_request_params/test_form/test_list.py b/tests/test_request_params/test_form/test_list.py index cfc42f523..abe781c94 100644 --- a/tests/test_request_params/test_form/test_list.py +++ b/tests/test_request_params/test_form/test_list.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import IsDict, IsOneOf, IsPartialDict +from dirty_equals import IsOneOf, IsPartialDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -58,28 +58,16 @@ def test_required_list_str_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - { - "detail": [ - { - "loc": ["body", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_header/test_list.py b/tests/test_request_params/test_header/test_list.py index 65510094a..489a6b3e7 100644 --- a/tests/test_request_params/test_header/test_list.py +++ b/tests/test_request_params/test_header/test_list.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict +from dirty_equals import AnyThing, IsOneOf, IsPartialDict from fastapi import FastAPI, Header from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -53,28 +53,16 @@ def test_required_list_str_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "p"], - "msg": "Field required", - "input": AnyThing, - } - ] - } - ) | IsDict( - { - "detail": [ - { - "loc": ["header", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p"], + "msg": "Field required", + "input": AnyThing, + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_query/test_list.py b/tests/test_request_params/test_query/test_list.py index ed2ea6c80..e933da214 100644 --- a/tests/test_request_params/test_query/test_list.py +++ b/tests/test_request_params/test_query/test_list.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import FastAPI, Query from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -53,28 +53,16 @@ def test_required_list_str_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "p"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - { - "detail": [ - { - "loc": ["query", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( From a1735d6d119a7f8a33dd0f20d0cf0bb1b1f631c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 18:31:59 +0000 Subject: [PATCH 026/243] =?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 8d080d6c5..45a6d31d6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 🔥 Remove test variants for Pydantic v1 in test_request_params. PR [#14612](https://github.com/fastapi/fastapi/pull/14612) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove Pydantic v1 specific test variants. PR [#14611](https://github.com/fastapi/fastapi/pull/14611) by [@tiangolo](https://github.com/tiangolo). ## 0.128.0 From 4d4fb28f9f7331d758a0da908804348b5a0ba719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 10:48:45 -0800 Subject: [PATCH 027/243] =?UTF-8?q?=F0=9F=91=B7=20Do=20not=20run=20transla?= =?UTF-8?q?tions=20on=20cron=20while=20finishing=20updating=20existing=20l?= =?UTF-8?q?anguages=20(#14613)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/translate.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index e681762ca..f1267d21f 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -1,8 +1,8 @@ name: Translate on: - schedule: - - cron: "0 5 15 * *" # Run at 05:00 on the 15 of every month + # schedule: + # - cron: "0 5 15 * *" # Run at 05:00 on the 15 of every month workflow_dispatch: inputs: From 52842fb8d371ad1517dbac48950d55e9cfa5f64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 10:49:08 -0800 Subject: [PATCH 028/243] =?UTF-8?q?=F0=9F=94=A7=20Add=20LLM=20prompt=20fil?= =?UTF-8?q?e=20for=20Simplified=20Chinese,=20generated=20from=20the=20exis?= =?UTF-8?q?ting=20translations=20(#14549)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 史雲昔 (Vincy SHI) --- docs/zh/llm-prompt.md | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/zh/llm-prompt.md diff --git a/docs/zh/llm-prompt.md b/docs/zh/llm-prompt.md new file mode 100644 index 000000000..1dfbe5916 --- /dev/null +++ b/docs/zh/llm-prompt.md @@ -0,0 +1,46 @@ +### Target language + +Translate to Simplified Chinese (简体中文). + +Language code: zh. + +### Grammar and tone + +1) Use clear, concise technical Chinese consistent with existing docs. +2) Address the reader naturally (commonly using “你/你的”). + +### Headings + +1) Follow existing Simplified Chinese heading style (short and descriptive). +2) Do not add trailing punctuation to headings. +3) If a heading contains only the name of a FastAPI feature, do not translate it. + +### Quotes and punctuation + +1) Keep punctuation style consistent with existing Simplified Chinese docs (they often mix English terms like “FastAPI” with Chinese text). +2) Never change punctuation inside inline code, code blocks, URLs, or file paths. + +### Ellipsis + +1) Keep ellipsis style consistent within each document, prefer `...` over `……`. +2) Never change ellipsis in code, URLs, or CLI examples. + +### Preferred translations / glossary + +Use the following preferred translations when they apply in documentation prose: + +- request (HTTP): 请求 +- response (HTTP): 响应 +- path operation: 路径操作 +- path operation function: 路径操作函数 + +### `///` admonitions + +1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +2) If a title is present, prefer these canonical titles: + +- `/// tip | 提示` +- `/// note | 注意` +- `/// warning | 警告` +- `/// info | 信息` +- `/// danger | 危险` From 13743e115a2a842e0c0076041f750660305c6b77 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 18:49:09 +0000 Subject: [PATCH 029/243] =?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 45a6d31d6..50a83da2e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👷 Do not run translations on cron while finishing updating existing languages. PR [#14613](https://github.com/fastapi/fastapi/pull/14613) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove test variants for Pydantic v1 in test_request_params. PR [#14612](https://github.com/fastapi/fastapi/pull/14612) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove Pydantic v1 specific test variants. PR [#14611](https://github.com/fastapi/fastapi/pull/14611) by [@tiangolo](https://github.com/tiangolo). From dbe83f3919c6de3438b572b8395e8bed9f0060d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 10:49:46 -0800 Subject: [PATCH 030/243] =?UTF-8?q?=F0=9F=94=A7=20Add=20LLM=20prompt=20fil?= =?UTF-8?q?e=20for=20Traditional=20Chinese,=20generated=20from=20the=20exi?= =?UTF-8?q?sting=20translations=20(#14550)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: W. H. Wang --- docs/zh-hant/llm-prompt.md | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/zh-hant/llm-prompt.md diff --git a/docs/zh-hant/llm-prompt.md b/docs/zh-hant/llm-prompt.md new file mode 100644 index 000000000..043501162 --- /dev/null +++ b/docs/zh-hant/llm-prompt.md @@ -0,0 +1,54 @@ +### Target language + +Translate to Traditional Chinese (繁體中文). + +Language code: zh-hant. + +### Grammar and tone + +1) Use clear, concise technical Traditional Chinese consistent with existing docs. +2) Address the reader naturally (commonly using “你/你的”). + +### Headings + +1) Follow existing Traditional Chinese heading style (short and descriptive). +2) Do not add trailing punctuation to headings. + +### Quotes and punctuation + +1) Keep punctuation style consistent with existing Traditional Chinese docs (they often mix English terms like “FastAPI” with Chinese text). +2) Never change punctuation inside inline code, code blocks, URLs, or file paths. +3) For more details, please follow the [Chinese Copywriting Guidelines](https://github.com/sparanoid/chinese-copywriting-guidelines). + +### Ellipsis + +1) Keep ellipsis style consistent within each document, prefer `...` over `……`. +2) Never change ellipsis in code, URLs, or CLI examples. + +### Preferred translations / glossary + +1. Should avoid using simplified Chinese characters and terms. Always examine if the translation can be easily comprehended by the Traditional Chinese readers. +2. For some Python-specific terms like "pickle", "list", "dict" etc, we don't have to translate them. +3. Use the following preferred translations when they apply in documentation prose: + +- request (HTTP): 請求 +- response (HTTP): 回應 +- path operation: 路徑操作 +- path operation function: 路徑操作函式 + +The translation can optionally include the original English text only in the first occurrence of each page (e.g. "路徑操作 (path operation)") if the translation is hard to be comprehended by most of the Chinese readers. + +### `///` admonitions + +1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +2) Many Traditional Chinese docs currently omit titles in `///` blocks; that is OK. +3) If a generic title is present, prefer these canonical titles: + +- `/// note | 注意` + +Notes: + +- `details` blocks exist; keep `/// details` as-is and translate only the title after `|`. +- Example canonical titles used in existing docs: + - `/// details | 上述指令的含義` + - `/// details | 關於 `requirements.txt`` From 4ce34686d98eae741f858f93f239c96baba83e01 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 18:51:18 +0000 Subject: [PATCH 031/243] =?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 50a83da2e..c4b88a473 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🔧 Add LLM prompt file for Simplified Chinese, generated from the existing translations. PR [#14549](https://github.com/fastapi/fastapi/pull/14549) by [@tiangolo](https://github.com/tiangolo). + ### Internal * 👷 Do not run translations on cron while finishing updating existing languages. PR [#14613](https://github.com/fastapi/fastapi/pull/14613) by [@tiangolo](https://github.com/tiangolo). From f362fdc2346d4c373c714263b9467a5edc1c420f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 18:51:39 +0000 Subject: [PATCH 032/243] =?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 c4b88a473..584137f5b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🔧 Add LLM prompt file for Traditional Chinese, generated from the existing translations. PR [#14550](https://github.com/fastapi/fastapi/pull/14550) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add LLM prompt file for Simplified Chinese, generated from the existing translations. PR [#14549](https://github.com/fastapi/fastapi/pull/14549) by [@tiangolo](https://github.com/tiangolo). ### Internal From 3b1b4f034bebddb873d4bd8c1bb5f52aaae17240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 11:05:53 -0800 Subject: [PATCH 033/243] =?UTF-8?q?=F0=9F=94=A8=20Update=20LLM=20translati?= =?UTF-8?q?on=20script=20to=20guide=20reviewers=20to=20change=20the=20prom?= =?UTF-8?q?pt=20(#14614)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/translate.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/translate.py b/scripts/translate.py index 6ebd24f54..66da46a08 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -1036,9 +1036,13 @@ def make_pr( print("Creating PR") g = Github(github_token) gh_repo = g.get_repo(github_repository) - pr = gh_repo.create_pull( - title=message, body=message, base="master", head=branch_name + body = ( + message + + "\n\nThis PR was created automatically using LLMs." + + f"\n\nIt uses the prompt file https://github.com/fastapi/fastapi/blob/master/docs/{language}/llm-prompt.md." + + "\n\nIn most cases, it's better to make PRs updating that file so that the LLM can do a better job generating the translations than suggesting changes in this PR." ) + pr = gh_repo.create_pull(title=message, body=body, base="master", head=branch_name) print(f"Created PR: {pr.number}") print("Finished") From 47391ea8fbdfcedf6ff3f4a56666ccf928ca10c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 19:06:15 +0000 Subject: [PATCH 034/243] =?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 584137f5b..1380d7ff6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Internal +* 🔨 Update LLM translation script to guide reviewers to change the prompt. PR [#14614](https://github.com/fastapi/fastapi/pull/14614) by [@tiangolo](https://github.com/tiangolo). * 👷 Do not run translations on cron while finishing updating existing languages. PR [#14613](https://github.com/fastapi/fastapi/pull/14613) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove test variants for Pydantic v1 in test_request_params. PR [#14612](https://github.com/fastapi/fastapi/pull/14612) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove Pydantic v1 specific test variants. PR [#14611](https://github.com/fastapi/fastapi/pull/14611) by [@tiangolo](https://github.com/tiangolo). From edf799577539de24b5ff52381dcc4830803fffbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 29 Dec 2025 04:38:11 -0800 Subject: [PATCH 035/243] =?UTF-8?q?=F0=9F=94=A7=20Add=20LLM=20prompt=20fil?= =?UTF-8?q?e=20for=20Turkish,=20generated=20from=20the=20existing=20transl?= =?UTF-8?q?ations=20(#14547)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/llm-prompt.md | 52 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/tr/llm-prompt.md diff --git a/docs/tr/llm-prompt.md b/docs/tr/llm-prompt.md new file mode 100644 index 000000000..38377f4f7 --- /dev/null +++ b/docs/tr/llm-prompt.md @@ -0,0 +1,52 @@ +### Target language + +Translate to Turkish (Türkçe). + +Language code: tr. + +### Grammar and tone + +1) Use instructional Turkish, consistent with existing Turkish docs. +2) Use imperative/guide language when appropriate (e.g. “açalım”, “gidin”, “kopyalayalım”). + +### Headings + +1) Follow existing Turkish heading style (Title Case where used; no trailing period). + +### Quotes + +1) Keep quote style consistent with the existing Turkish docs (they frequently use ASCII quotes in prose). +2) Never change quotes inside inline code, code blocks, URLs, or file paths. + +### Ellipsis + +1) Keep ellipsis style consistent with existing Turkish docs. +2) Never change `...` in code, URLs, or CLI examples. + +### Preferred translations / glossary + +Do not translate technical terms like path, route, request, response, query, body, cookie, and header, keep them as is. + +- Suffixing is very important, when adding Turkish suffixes to the English words, do that based on the pronunciation of the word and with an apostrophe. + +- Suffixes also changes based on what word comes next in Turkish too, here is an example: + +"Server'a gelen request'leri intercept... " or this could have been "request'e", "request'i" etc. + +- Some words are tricky like "path'e" can't be used like "path'a" but it could have been "path'i" "path'leri" etc. + +- You can use a more instructional style, that is consistent with the document, you can add the Turkish version of the term in parenthesis if it is not something very obvious, or an advanced concept, but do not over do it, do it only the first time it is mentioned, but keep the English term as the primary word. + +### `///` admonitions + +1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +2) If a title is present, prefer these canonical titles: + +- `/// note | Not` +- `/// note | Teknik Detaylar` +- `/// tip | İpucu` +- `/// warning | Uyarı` +- `/// info | Bilgi` +- `/// check | Ek bilgi` + +Prefer `İpucu` over `Ipucu`. From 9ed5f246ed97a5806f2ea2dd531f1c18b0bd17db Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 29 Dec 2025 12:38:35 +0000 Subject: [PATCH 036/243] =?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 1380d7ff6..9c4fe05ea 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🔧 Add LLM prompt file for Turkish, generated from the existing translations. PR [#14547](https://github.com/fastapi/fastapi/pull/14547) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add LLM prompt file for Traditional Chinese, generated from the existing translations. PR [#14550](https://github.com/fastapi/fastapi/pull/14550) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add LLM prompt file for Simplified Chinese, generated from the existing translations. PR [#14549](https://github.com/fastapi/fastapi/pull/14549) by [@tiangolo](https://github.com/tiangolo). From d9b7b65b8139eca2a3a2b61c9a024ffd970d5c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 29 Dec 2025 10:54:20 -0800 Subject: [PATCH 037/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translation=20p?= =?UTF-8?q?rompts=20(#14619)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/llm-prompt.md | 477 ++++++++++++------------- docs/en/docs/_llm-test.md | 2 +- docs/ja/llm-prompt.md | 16 +- docs/ko/llm-prompt.md | 16 +- docs/missing-translation.md | 2 +- docs/pt/llm-prompt.md | 18 +- docs/tr/llm-prompt.md | 18 +- docs/uk/llm-prompt.md | 20 +- docs/zh-hant/llm-prompt.md | 34 +- docs/zh/llm-prompt.md | 22 +- scripts/general-llm-prompt.md | 528 +++++++++++++++++++++++++++ scripts/translate.py | 646 +--------------------------------- 12 files changed, 837 insertions(+), 962 deletions(-) create mode 100644 scripts/general-llm-prompt.md diff --git a/docs/de/llm-prompt.md b/docs/de/llm-prompt.md index 35ca9f069..2d345bf6d 100644 --- a/docs/de/llm-prompt.md +++ b/docs/de/llm-prompt.md @@ -4,213 +4,197 @@ Translate to German (Deutsch). Language code: de. - -### Definitions - -"hyphen" - The character «-» - Unicode U+002D (HYPHEN-MINUS) - Alternative names: hyphen, dash, minus sign - -"dash" - The character «–» - Unicode U+2013 (EN DASH) - German name: Halbgeviertstrich - - ### Grammar to use when talking to the reader -Use the formal grammar (use «Sie» instead of «Du»). - +Use the formal grammar (use `Sie` instead of `Du`). ### Quotes -1) Convert neutral double quotes («"») and English double typographic quotes («“» and «”») to German double typographic quotes («„» and «“»). Convert neutral single quotes («'») and English single typographic quotes («‘» and «’») to German single typographic quotes («‚» and «‘»). Do NOT convert «`"» to «„», do NOT convert «"`» to «“». +1) Convert neutral double quotes (`"`) to German double typographic quotes (`„` and `“`). Convert neutral single quotes (`'`) to German single typographic quotes (`‚` and `‘`). + +Do NOT convert quotes in code snippets and code blocks to their German typographic equivalents. Examples: - Source (English): +Source (English): - ««« - "Hello world" - “Hello Universe” - "He said: 'Hello'" - “my name is ‘Nils’” - `"__main__"` - `"items"` - »»» +``` +"Hello world" +“Hello Universe” +"He said: 'Hello'" +“my name is ‘Nils’” +`"__main__"` +`"items"` +``` - Result (German): - - ««« - „Hallo Welt“ - „Hallo Universum“ - „Er sagte: ‚Hallo‘“ - „Mein Name ist ‚Nils‘“ - `"__main__"` - `"items"` - »»» +Result (German): +``` +„Hallo Welt“ +„Hallo Universum“ +„Er sagte: ‚Hallo‘“ +„Mein Name ist ‚Nils‘“ +`"__main__"` +`"items"` +``` ### Ellipsis -1) Make sure there is a space between an ellipsis and a word following or preceding the ellipsis. +- Make sure there is a space between an ellipsis and a word following or preceding the ellipsis. Examples: - Source (English): +Source (English): - ««« - ...as we intended. - ...this would work: - ...etc. - others... - More to come... - »»» +``` +...as we intended. +...this would work: +...etc. +others... +More to come... +``` - Result (German): +Result (German): - ««« - ... wie wir es beabsichtigt hatten. - ... das würde funktionieren: - ... usw. - Andere ... - Später mehr ... - »»» - -2) This does not apply in URLs, code blocks, and code snippets. Do not remove or add spaces there. +``` +... wie wir es beabsichtigt hatten. +... das würde funktionieren: +... usw. +Andere ... +Später mehr ... +``` +- This does not apply in URLs, code blocks, and code snippets. Do not remove or add spaces there. ### Headings -1) Translate headings using the infinite form. +- Translate headings using the infinite form. Examples: - Source (English): +Source (English): - ««« - ## Create a Project { #create-a-project } - »»» +``` +## Create a Project { #create-a-project } +``` - Translate with (German): +Result (German): - ««« - ## Ein Projekt erstellen { #create-a-project } - »»» +``` +## Ein Projekt erstellen { #create-a-project } +``` - Do NOT translate with (German): +Do NOT translate with (German): - ««« - ## Erstellen Sie ein Projekt { #create-a-project } - »»» +``` +## Erstellen Sie ein Projekt { #create-a-project } +``` - Source (English): +Source (English): - ««« - # Install Packages { #install-packages } - »»» +``` +# Install Packages { #install-packages } +``` - Translate with (German): +Translate with (German): - ««« - # Pakete installieren { #install-packages } - »»» +``` +# Pakete installieren { #install-packages } +``` - Do NOT translate with (German): +Do NOT translate with (German): - ««« - # Installieren Sie Pakete { #install-packages } - »»» +``` +# Installieren Sie Pakete { #install-packages } +``` - Source (English): +Source (English): - ««« - ### Run Your Program { #run-your-program } - »»» +``` +### Run Your Program { #run-your-program } +``` - Translate with (German): +Translate with (German): - ««« - ### Ihr Programm ausführen { #run-your-program } - »»» +``` +### Ihr Programm ausführen { #run-your-program } +``` - Do NOT translate with (German): +Do NOT translate with (German): - ««« - ### Führen Sie Ihr Programm aus { #run-your-program } - »»» +``` +### Führen Sie Ihr Programm aus { #run-your-program } +``` -2) Make sure that the translated part of the heading does not end with a period. +- Make sure that the translated part of the heading does not end with a period. Example: - Source (English): +Source (English): - ««« - ## Another module with `APIRouter` { #another-module-with-apirouter } - »»» +``` +## Another module with `APIRouter` { #another-module-with-apirouter } +``` - Translate with (German): +Translate with (German): - ««« - ## Ein weiteres Modul mit `APIRouter` { #another-module-with-apirouter } - »»» +``` +## Ein weiteres Modul mit `APIRouter` { #another-module-with-apirouter } +``` - Do NOT translate with (German) – notice the added period: +Do NOT translate with (German) – notice the added period: - ««« - ## Ein weiteres Modul mit `APIRouter`. { #another-module-with-apirouter } - »»» +``` +## Ein weiteres Modul mit `APIRouter`. { #another-module-with-apirouter } +``` -3) Replace occurrences of literal « - » (a space followed by a hyphen followed by a space) with « – » (a space followed by a dash followed by a space) in the translated part of the heading. +- Replace occurrences of literal ` - ` (a space followed by a hyphen followed by a space) with ` – ` (a space followed by a dash followed by a space) in the translated part of the heading. Example: - Source (English): +Source (English): - ««« - # FastAPI in Containers - Docker { #fastapi-in-containers-docker } - »»» +``` +# FastAPI in Containers - Docker { #fastapi-in-containers-docker } +``` - Translate with (German) – notice the dash: +Translate with (German) – notice the dash: - ««« - # FastAPI in Containern – Docker { #fastapi-in-containers-docker } - »»» +``` +# FastAPI in Containern – Docker { #fastapi-in-containers-docker } +``` - Do NOT translate with (German) – notice the hyphen: +Do NOT translate with (German) – notice the hyphen: - ««« - # FastAPI in Containern - Docker { #fastapi-in-containers-docker } - »»» +``` +# FastAPI in Containern - Docker { #fastapi-in-containers-docker } +``` -3.1) Do not apply rule 3 when there is no space before or no space after the hyphen. +- Do not apply rule 3 when there is no space before or no space after the hyphen. Example: - Source (English): +Source (English): - ««« - ## Type hints and annotations { #type-hints-and-annotations } - »»» +``` +## Type hints and annotations { #type-hints-and-annotations } +``` - Translate with (German) – notice the hyphen: +Translate with (German) - notice the hyphen: - ««« - ## Typhinweise und -annotationen { #type-hints-and-annotations } - »»» +``` +## Typhinweise und -annotationen { #type-hints-and-annotations } +``` - Do NOT translate with (German) – notice the dash: +Do NOT translate with (German) - notice the dash: - ««« - ## Typhinweise und –annotationen { #type-hints-and-annotations } - »»» +``` +## Typhinweise und –annotationen { #type-hints-and-annotations } +``` -3.2) Do not apply rule 3 to the untranslated part of the heading inside curly brackets, which you shall not translate. +- Do not modify the hyphens in the content in headers inside of curly braces, which you shall not translate. - -### German instructions, when to use and when not to use hyphens in words (written in first person, which is you) +### German instructions, when to use and when not to use hyphens in words (written in first person, which is you). In der Regel versuche ich so weit wie möglich Worte zusammenzuschreiben, also ohne Bindestrich, es sei denn, es ist Konkretesding-Klassevondingen, etwa «Pydantic-Modell» (aber: «Datenbankmodell»), «Python-Modul» (aber: «Standardmodul»). Ich setze auch einen Bindestrich, wenn er die gleichen Buchstaben verbindet, etwa «Enum-Member», «Cloud-Dienst», «Template-Engine». Oder wenn das Wort sonst einfach zu lang wird, etwa, «Performance-Optimierung». Oder um etwas visuell besser zu dokumentieren, etwa «Pfadoperation-Dekorator», «Pfadoperation-Funktion». @@ -219,123 +203,122 @@ In der Regel versuche ich so weit wie möglich Worte zusammenzuschreiben, also o Ich versuche nicht, alles einzudeutschen. Das bezieht sich besonders auf Begriffe aus dem Bereich der Programmierung. Ich wandele zwar korrekt in Großschreibung um und setze Bindestriche, wo notwendig, aber ansonsten lasse ich solch ein Wort unverändert. Beispielsweise wird aus dem englischen Wort «string» in der deutschen Übersetzung «String», aber nicht «Zeichenkette». Oder aus dem englischen Wort «request body» wird in der deutschen Übersetzung «Requestbody», aber nicht «Anfragekörper». Oder aus dem englischen «response» wird im Deutschen «Response», aber nicht «Antwort». - ### List of English terms and their preferred German translations -Below is a list of English terms and their preferred German translations, separated by a colon («:»). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them. In the below list, a term or a translation may be followed by an explanation in brackets, which explains when to translate the term this way. If a translation is preceded by «NOT», then that means: do NOT use this translation for this term. English nouns, starting with the word «the», have the German genus – «der», «die», «das» – prepended to their German translation, to help you to grammatically decline them in the translation. They are given in singular case, unless they have «(plural)» attached, which means they are given in plural case. Verbs are given in the full infinitive – starting with the word «to». +Below is a list of English terms and their preferred German translations, separated by a colon (:). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them. In the below list, a term or a translation may be followed by an explanation in brackets, which explains when to translate the term this way. If a translation is preceded by `NOT`, then that means: do NOT use this translation for this term. English nouns, starting with the word `the`, have the German genus – `der`, `die`, `das` – prepended to their German translation, to help you to grammatically decline them in the translation. They are given in singular case, unless they have `(plural)` attached, which means they are given in plural case. Verbs are given in the full infinitive – starting with the word `to`. -* «/// check»: «/// check | Testen» -* «/// danger»: «/// danger | Gefahr» -* «/// info»: «/// info | Info» -* «/// note | Technical Details»: «/// note | Technische Details» -* «/// note»: «/// note | Hinweis» -* «/// tip»: «/// tip | Tipp» -* «/// warning»: «/// warning | Achtung» -* «you»: «Sie» -* «your»: «Ihr» -* «e.g»: «z. B.» -* «etc.»: «usw.» -* «ref»: «Ref.» -* «the Tutorial - User guide»: «das Tutorial – Benutzerhandbuch» -* «the Advanced User Guide»: «das Handbuch für fortgeschrittene Benutzer» -* «the SQLModel docs»: «die SQLModel-Dokumentation» -* «the docs»: «die Dokumentation» (use singular case) -* «the env var»: «die Umgebungsvariable» -* «the `PATH` environment variable»: «die `PATH`-Umgebungsvariable» -* «the `PATH`»: «der `PATH`» -* «the `requirements.txt`»: «die `requirements.txt`» -* «the API Router»: «der API-Router» -* «the Authorization-Header»: «der Autorisierungsheader» -* «the `Authorization`-Header»: «der `Authorization`-Header» -* «the background task»: «der Hintergrundtask» -* «the button»: «der Button» -* «the cloud provider»: «der Cloudanbieter» -* «the CLI»: «Das CLI» -* «the coverage»: «Die Testabdeckung» -* «the command line interface»: «Das Kommandozeileninterface» -* «the default value»: «der Defaultwert» -* «the default value»: NOT «der Standardwert» -* «the default declaration»: «die Default-Deklaration» -* «the deployment»: «das Deployment» -* «the dict»: «das Dict» -* «the dictionary»: «das Dictionary» -* «the enumeration»: «die Enumeration» -* «the enum»: «das Enum» -* «the engine»: «die Engine» -* «the error response»: «die Error-Response» -* «the event»: «das Event» -* «the exception»: «die Exception» -* «the exception handler»: «der Exceptionhandler» -* «the form model»: «das Formularmodell» -* «the form body»: «der Formularbody» -* «the header»: «der Header» -* «the headers» (plural): «die Header» -* «in headers» (plural): «in Headern» -* «the forwarded header»: «der Forwarded-Header» -* «the lifespan event»: «das Lifespan-Event» -* «the lock»: «der Lock» -* «the locking»: «das Locking» -* «the mobile application»: «die Mobile-Anwendung» -* «the model object»: «das Modellobjekt» -* «the mounting»: «das Mounten» -* «mounted»: «gemountet» -* «the origin»: «das Origin» -* «the override»: «Die Überschreibung» -* «the parameter»: «der Parameter» -* «the parameters» (plural): «die Parameter» -* «the function parameter»: «der Funktionsparameter» -* «the default parameter»: «der Defaultparameter» -* «the body parameter»: «der Body-Parameter» -* «the request body parameter»: «der Requestbody-Parameter» -* «the path parameter»: «der Pfad-Parameter» -* «the query parameter»: «der Query-Parameter» -* «the cookie parameter»: «der Cookie-Parameter» -* «the header parameter»: «der Header-Parameter» -* «the form parameter»: «der Formular-Parameter» -* «the payload»: «die Payload» -* «the performance»: NOT «die Performance» -* «the query»: «die Query» -* «the recap»: «die Zusammenfassung» -* «the request» (what the client sends to the server): «der Request» -* «the request body»: «der Requestbody» -* «the request bodies» (plural): «die Requestbodys» -* «the response» (what the server sends back to the client): «die Response» -* «the return type»: «der Rückgabetyp» -* «the return value»: «der Rückgabewert» -* «the startup» (the event of the app): «der Startup» -* «the shutdown» (the event of the app): «der Shutdown» -* «the startup event»: «das Startup-Event» -* «the shutdown event»: «das Shutdown-Event» -* «the startup» (of the server): «das Hochfahren» -* «the startup» (the company): «das Startup» -* «the SDK»: «das SDK» -* «the tag»: «der Tag» -* «the type annotation»: «die Typannotation» -* «the type hint»: «der Typhinweis» -* «the wildcard»: «die Wildcard» -* «the worker class»: «die Workerklasse» -* «the worker class»: NOT «die Arbeiterklasse» -* «the worker process»: «der Workerprozess» -* «the worker process»: NOT «der Arbeiterprozess» -* «to commit»: «committen» -* «to deploy» (in the cloud): «deployen» -* «to modify»: «ändern» -* «to serve» (an application): «bereitstellen» -* «to serve» (a response): «ausliefern» -* «to serve»: NOT «bedienen» -* «to upgrade»: «aktualisieren» -* «to wrap»: «wrappen» -* «to wrap»: NOT «hüllen» -* «`foo` as a `type`»: «`foo` vom Typ `type`» -* «`foo` as a `type`»: «`foo`, ein `type`» -* «FastAPI's X»: «FastAPIs X» -* «Starlette's Y»: «Starlettes Y» -* «X is case-sensitive»: «Groß-/Klein­schrei­bung ist relevant in X» -* «X is case-insensitive»: «Groß-/Klein­schrei­bung ist nicht relevant in X» -* «standard Python»: «Standard-Python» -* «deprecated»: «deprecatet» +* /// check: /// check | Testen +* /// danger: /// danger | Gefahr +* /// info: /// info | Info +* /// note | Technical Details: /// note | Technische Details +* /// note: /// note | Hinweis +* /// tip: /// tip | Tipp +* /// warning: /// warning | Achtung +* you: Sie +* your: Ihr +* e.g: z. B. +* etc.: usw. +* ref: Ref. +* the Tutorial - User guide: das Tutorial – Benutzerhandbuch +* the Advanced User Guide: das Handbuch für fortgeschrittene Benutzer +* the SQLModel docs: die SQLModel-Dokumentation +* the docs: die Dokumentation (use singular case) +* the env var: die Umgebungsvariable +* the `PATH` environment variable: die `PATH`-Umgebungsvariable +* the `PATH`: der `PATH` +* the `requirements.txt`: die `requirements.txt` +* the API Router: der API-Router +* the Authorization-Header: der Autorisierungsheader +* the `Authorization`-Header: der `Authorization`-Header +* the background task: der Hintergrundtask +* the button: der Button +* the cloud provider: der Cloudanbieter +* the CLI: Das CLI +* the coverage: Die Testabdeckung +* the command line interface: Das Kommandozeileninterface +* the default value: der Defaultwert +* the default value: NOT der Standardwert +* the default declaration: die Default-Deklaration +* the deployment: das Deployment +* the dict: das Dict +* the dictionary: das Dictionary +* the enumeration: die Enumeration +* the enum: das Enum +* the engine: die Engine +* the error response: die Error-Response +* the event: das Event +* the exception: die Exception +* the exception handler: der Exceptionhandler +* the form model: das Formularmodell +* the form body: der Formularbody +* the header: der Header +* the headers (plural): die Header +* in headers (plural): in Headern +* the forwarded header: der Forwarded-Header +* the lifespan event: das Lifespan-Event +* the lock: der Lock +* the locking: das Locking +* the mobile application: die Mobile-Anwendung +* the model object: das Modellobjekt +* the mounting: das Mounten +* mounted: gemountet +* the origin: das Origin +* the override: Die Überschreibung +* the parameter: der Parameter +* the parameters (plural): die Parameter +* the function parameter: der Funktionsparameter +* the default parameter: der Defaultparameter +* the body parameter: der Body-Parameter +* the request body parameter: der Requestbody-Parameter +* the path parameter: der Pfad-Parameter +* the query parameter: der Query-Parameter +* the cookie parameter: der Cookie-Parameter +* the header parameter: der Header-Parameter +* the form parameter: der Formular-Parameter +* the payload: die Payload +* the performance: NOT die Performance +* the query: die Query +* the recap: die Zusammenfassung +* the request (what the client sends to the server): der Request +* the request body: der Requestbody +* the request bodies (plural): die Requestbodys +* the response (what the server sends back to the client): die Response +* the return type: der Rückgabetyp +* the return value: der Rückgabewert +* the startup (the event of the app): der Startup +* the shutdown (the event of the app): der Shutdown +* the startup event: das Startup-Event +* the shutdown event: das Shutdown-Event +* the startup (of the server): das Hochfahren +* the startup (the company): das Startup +* the SDK: das SDK +* the tag: der Tag +* the type annotation: die Typannotation +* the type hint: der Typhinweis +* the wildcard: die Wildcard +* the worker class: die Workerklasse +* the worker class: NOT die Arbeiterklasse +* the worker process: der Workerprozess +* the worker process: NOT der Arbeiterprozess +* to commit: committen +* to deploy (in the cloud): deployen +* to modify: ändern +* to serve (an application): bereitstellen +* to serve (a response): ausliefern +* to serve: NOT bedienen +* to upgrade: aktualisieren +* to wrap: wrappen +* to wrap: NOT hüllen +* `foo` as a `type`: `foo` vom Typ `type` +* `foo` as a `type`: `foo`, ein `type` +* FastAPI's X: FastAPIs X +* Starlette's Y: Starlettes Y +* X is case-sensitive: Groß-/Klein­schrei­bung ist relevant in X +* X is case-insensitive: Groß-/Klein­schrei­bung ist nicht relevant in X +* standard Python: Standard-Python +* deprecated: deprecatet ### Other rules -Preserve indentation. Keep emoticons. Encode in utf-8. Use Linux line breaks (LF). +Preserve indentation. Keep emojis. Encode in utf-8. Use Linux line breaks (LF). diff --git a/docs/en/docs/_llm-test.md b/docs/en/docs/_llm-test.md index 9f216f9d7..d218f7c76 100644 --- a/docs/en/docs/_llm-test.md +++ b/docs/en/docs/_llm-test.md @@ -6,7 +6,7 @@ Tests added here will be seen by all designers of language specific prompts. Use as follows: -* Have a language specific prompt – `docs/{language code}/llm-prompt.md`. +* Have a language specific prompt - `docs/{language code}/llm-prompt.md`. * Do a fresh translation of this document into your desired target language (see e.g. the `translate-page` command of the `translate.py`). This will create the translation under `docs/{language code}/docs/_llm-test.md`. * Check if things are okay in the translation. * If necessary, improve your language specific prompt, the general prompt, or the English document. diff --git a/docs/ja/llm-prompt.md b/docs/ja/llm-prompt.md index c47cc36df..18909cd59 100644 --- a/docs/ja/llm-prompt.md +++ b/docs/ja/llm-prompt.md @@ -6,23 +6,23 @@ Language code: ja. ### Grammar and tone -1) Use polite, instructional Japanese (です/ます調). -2) Keep the tone concise and technical (match existing Japanese FastAPI docs). +- Use polite, instructional Japanese (です/ます調). +- Keep the tone concise and technical (match existing Japanese FastAPI docs). ### Headings -1) Follow the existing Japanese style: short, descriptive headings (often noun phrases), e.g. 「チェック」. -2) Do not add a trailing period at the end of headings. +- Follow the existing Japanese style: short, descriptive headings (often noun phrases), e.g. 「チェック」. +- Do not add a trailing period at the end of headings. ### Quotes -1) Prefer Japanese corner brackets 「」 in normal prose when quoting a term. -2) Do not change quotes inside inline code, code blocks, URLs, or file paths. +- Prefer Japanese corner brackets 「」 in normal prose when quoting a term. +- Do not change quotes inside inline code, code blocks, URLs, or file paths. ### Ellipsis -1) Keep ellipsis style consistent with existing Japanese docs (commonly `...`). -2) Never change `...` in code, URLs, or CLI examples. +- Keep ellipsis style consistent with existing Japanese docs (commonly `...`). +- Never change `...` in code, URLs, or CLI examples. ### Preferred translations / glossary diff --git a/docs/ko/llm-prompt.md b/docs/ko/llm-prompt.md index 008511a5b..df807c949 100644 --- a/docs/ko/llm-prompt.md +++ b/docs/ko/llm-prompt.md @@ -6,23 +6,23 @@ Language code: ko. ### Grammar and tone -1) Use polite, instructional Korean (e.g. 합니다/하세요 style). -2) Keep the tone consistent with the existing Korean FastAPI docs. +- Use polite, instructional Korean (e.g. 합니다/하세요 style). +- Keep the tone consistent with the existing Korean FastAPI docs. ### Headings -1) Follow existing Korean heading style (short, action-oriented headings like “확인하기”). -2) Do not add trailing punctuation to headings. +- Follow existing Korean heading style (short, action-oriented headings like “확인하기”). +- Do not add trailing punctuation to headings. ### Quotes -1) Keep quote style consistent with the existing Korean docs. -2) Never change quotes inside inline code, code blocks, URLs, or file paths. +- Keep quote style consistent with the existing Korean docs. +- Never change quotes inside inline code, code blocks, URLs, or file paths. ### Ellipsis -1) Keep ellipsis style consistent with existing Korean docs (often `...`). -2) Never change `...` in code, URLs, or CLI examples. +- Keep ellipsis style consistent with existing Korean docs (often `...`). +- Never change `...` in code, URLs, or CLI examples. ### Preferred translations / glossary diff --git a/docs/missing-translation.md b/docs/missing-translation.md index bfff84766..71c0925c5 100644 --- a/docs/missing-translation.md +++ b/docs/missing-translation.md @@ -4,6 +4,6 @@ This page hasn’t been translated into your language yet. 🌍 We’re currently switching to an automated translation system 🤖, which will help keep all translations complete and up to date. -Learn more: [Contributing – Translations](https://fastapi.tiangolo.com/contributing/#translations){.internal-link target=_blank} +Learn more: [Contributing - Translations](https://fastapi.tiangolo.com/contributing/#translations){.internal-link target=_blank} /// diff --git a/docs/pt/llm-prompt.md b/docs/pt/llm-prompt.md index 2374070ce..3f5208e91 100644 --- a/docs/pt/llm-prompt.md +++ b/docs/pt/llm-prompt.md @@ -14,15 +14,15 @@ When translating documentation into Portuguese, use neutral and widely understan For the next terms, use the following translations: -* «/// check»: «/// check | Verifique» -* «/// danger»: «/// danger | Cuidado» -* «/// info»: «/// info | Informação» -* «/// note | Technical Details»: «/// note | Detalhes Técnicos» -* «/// info | Very Technical Details»: «/// note | Detalhes Técnicos Avançados» -* «/// note»: «/// note | Nota» -* «/// tip»: «/// tip | Dica» -* «/// warning»: «/// warning | Atenção» -* «(you should)»: «(você deveria)» +* /// check: /// check | Verifique +* /// danger: /// danger | Cuidado +* /// info: /// info | Informação +* /// note | Technical Details: /// note | Detalhes Técnicos +* /// info | Very Technical Details: /// note | Detalhes Técnicos Avançados +* /// note: /// note | Nota +* /// tip: /// tip | Dica +* /// warning: /// warning | Atenção +* (you should): (você deveria) * async context manager: gerenciador de contexto assíncrono * autocomplete: autocompletar * autocompletion: preenchimento automático diff --git a/docs/tr/llm-prompt.md b/docs/tr/llm-prompt.md index 38377f4f7..297b0a0e6 100644 --- a/docs/tr/llm-prompt.md +++ b/docs/tr/llm-prompt.md @@ -6,22 +6,22 @@ Language code: tr. ### Grammar and tone -1) Use instructional Turkish, consistent with existing Turkish docs. -2) Use imperative/guide language when appropriate (e.g. “açalım”, “gidin”, “kopyalayalım”). +- Use instructional Turkish, consistent with existing Turkish docs. +- Use imperative/guide language when appropriate (e.g. “açalım”, “gidin”, “kopyalayalım”). ### Headings -1) Follow existing Turkish heading style (Title Case where used; no trailing period). +- Follow existing Turkish heading style (Title Case where used; no trailing period). ### Quotes -1) Keep quote style consistent with the existing Turkish docs (they frequently use ASCII quotes in prose). -2) Never change quotes inside inline code, code blocks, URLs, or file paths. +- Alıntı stili mevcut Türkçe dokümanlarla tutarlı tutun (genellikle metin içinde ASCII tırnak işaretleri kullanılır). +- Satır içi kod, kod blokları, URL'ler veya dosya yolları içindeki tırnak işaretlerini asla değiştirmeyin. ### Ellipsis -1) Keep ellipsis style consistent with existing Turkish docs. -2) Never change `...` in code, URLs, or CLI examples. +- Üç nokta (...) stili mevcut Türkçe dokümanlarla tutarlı tutun. +- Kod, URL veya CLI örneklerindeki `...` ifadesini asla değiştirmeyin. ### Preferred translations / glossary @@ -39,8 +39,8 @@ Do not translate technical terms like path, route, request, response, query, bod ### `///` admonitions -1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). -2) If a title is present, prefer these canonical titles: +- Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +- If a title is present, prefer these canonical titles: - `/// note | Not` - `/// note | Teknik Detaylar` diff --git a/docs/uk/llm-prompt.md b/docs/uk/llm-prompt.md index d55d36ab5..f1c5377a4 100644 --- a/docs/uk/llm-prompt.md +++ b/docs/uk/llm-prompt.md @@ -6,23 +6,23 @@ Language code: uk. ### Grammar and tone -1) Use polite/formal address consistent with existing Ukrainian docs (use “ви/ваш”). -2) Keep the tone concise and technical. +- Use polite/formal address consistent with existing Ukrainian docs (use “ви/ваш”). +- Keep the tone concise and technical. ### Headings -1) Follow existing Ukrainian heading style; keep headings short and instructional. -2) Do not add trailing punctuation to headings. +- Follow existing Ukrainian heading style; keep headings short and instructional. +- Do not add trailing punctuation to headings. ### Quotes -1) Prefer Ukrainian guillemets «…» for quoted terms in prose, matching existing Ukrainian docs. -2) Never change quotes inside inline code, code blocks, URLs, or file paths. +- Prefer Ukrainian guillemets «…» for quoted terms in prose, matching existing Ukrainian docs. +- Never change quotes inside inline code, code blocks, URLs, or file paths. ### Ellipsis -1) Keep ellipsis style consistent with existing Ukrainian docs. -2) Never change `...` in code, URLs, or CLI examples. +- Keep ellipsis style consistent with existing Ukrainian docs. +- Never change `...` in code, URLs, or CLI examples. ### Preferred translations / glossary @@ -35,8 +35,8 @@ Use the following preferred translations when they apply in documentation prose: ### `///` admonitions -1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). -2) If a title is present, prefer these canonical titles (choose one canonical form where variants exist): +- Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +- If a title is present, prefer these canonical titles (choose one canonical form where variants exist): - `/// note | Примітка` - `/// note | Технічні деталі` diff --git a/docs/zh-hant/llm-prompt.md b/docs/zh-hant/llm-prompt.md index 043501162..d44709015 100644 --- a/docs/zh-hant/llm-prompt.md +++ b/docs/zh-hant/llm-prompt.md @@ -6,30 +6,30 @@ Language code: zh-hant. ### Grammar and tone -1) Use clear, concise technical Traditional Chinese consistent with existing docs. -2) Address the reader naturally (commonly using “你/你的”). +- Use clear, concise technical Traditional Chinese consistent with existing docs. +- Address the reader naturally (commonly using “你/你的”). ### Headings -1) Follow existing Traditional Chinese heading style (short and descriptive). -2) Do not add trailing punctuation to headings. +- Follow existing Traditional Chinese heading style (short and descriptive). +- Do not add trailing punctuation to headings. ### Quotes and punctuation -1) Keep punctuation style consistent with existing Traditional Chinese docs (they often mix English terms like “FastAPI” with Chinese text). -2) Never change punctuation inside inline code, code blocks, URLs, or file paths. -3) For more details, please follow the [Chinese Copywriting Guidelines](https://github.com/sparanoid/chinese-copywriting-guidelines). +- Keep punctuation style consistent with existing Traditional Chinese docs (they often mix English terms like “FastAPI” with Chinese text). +- Never change punctuation inside inline code, code blocks, URLs, or file paths. +- For more details, please follow the [Chinese Copywriting Guidelines](https://github.com/sparanoid/chinese-copywriting-guidelines). ### Ellipsis -1) Keep ellipsis style consistent within each document, prefer `...` over `……`. -2) Never change ellipsis in code, URLs, or CLI examples. +- Keep ellipsis style consistent within each document, prefer `...` over `……`. +- Never change ellipsis in code, URLs, or CLI examples. ### Preferred translations / glossary -1. Should avoid using simplified Chinese characters and terms. Always examine if the translation can be easily comprehended by the Traditional Chinese readers. -2. For some Python-specific terms like "pickle", "list", "dict" etc, we don't have to translate them. -3. Use the following preferred translations when they apply in documentation prose: +- Should avoid using simplified Chinese characters and terms. Always examine if the translation can be easily comprehended by the Traditional Chinese readers. +- For some Python-specific terms like "pickle", "list", "dict" etc, we don't have to translate them. +- Use the following preferred translations when they apply in documentation prose: - request (HTTP): 請求 - response (HTTP): 回應 @@ -50,5 +50,11 @@ Notes: - `details` blocks exist; keep `/// details` as-is and translate only the title after `|`. - Example canonical titles used in existing docs: - - `/// details | 上述指令的含義` - - `/// details | 關於 `requirements.txt`` + +``` +/// details | 上述指令的含義 +``` + +``` +/// details | 關於 `requirements.txt` +``` diff --git a/docs/zh/llm-prompt.md b/docs/zh/llm-prompt.md index 1dfbe5916..7ce6f96a4 100644 --- a/docs/zh/llm-prompt.md +++ b/docs/zh/llm-prompt.md @@ -6,24 +6,24 @@ Language code: zh. ### Grammar and tone -1) Use clear, concise technical Chinese consistent with existing docs. -2) Address the reader naturally (commonly using “你/你的”). +- Use clear, concise technical Chinese consistent with existing docs. +- Address the reader naturally (commonly using “你/你的”). ### Headings -1) Follow existing Simplified Chinese heading style (short and descriptive). -2) Do not add trailing punctuation to headings. -3) If a heading contains only the name of a FastAPI feature, do not translate it. +- Follow existing Simplified Chinese heading style (short and descriptive). +- Do not add trailing punctuation to headings. +- If a heading contains only the name of a FastAPI feature, do not translate it. ### Quotes and punctuation -1) Keep punctuation style consistent with existing Simplified Chinese docs (they often mix English terms like “FastAPI” with Chinese text). -2) Never change punctuation inside inline code, code blocks, URLs, or file paths. +- Keep punctuation style consistent with existing Simplified Chinese docs (they often mix English terms like “FastAPI” with Chinese text). +- Never change punctuation inside inline code, code blocks, URLs, or file paths. ### Ellipsis -1) Keep ellipsis style consistent within each document, prefer `...` over `……`. -2) Never change ellipsis in code, URLs, or CLI examples. +- Keep ellipsis style consistent within each document, prefer `...` over `……`. +- Never change ellipsis in code, URLs, or CLI examples. ### Preferred translations / glossary @@ -36,8 +36,8 @@ Use the following preferred translations when they apply in documentation prose: ### `///` admonitions -1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). -2) If a title is present, prefer these canonical titles: +- Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +- If a title is present, prefer these canonical titles: - `/// tip | 提示` - `/// note | 注意` diff --git a/scripts/general-llm-prompt.md b/scripts/general-llm-prompt.md new file mode 100644 index 000000000..d45ab8eb0 --- /dev/null +++ b/scripts/general-llm-prompt.md @@ -0,0 +1,528 @@ +### Your task + +Translate an English original content to a target language. + +The original content is written in Markdown, write the translation in Markdown as well. + +The original content will be surrounded by triple percentage signs (%%%). Do not include the triple percentage signs in the translation. + +### Technical terms in English + +For technical terms in English that don't have a common translation term, use the original term in English. + +### Content of code snippets + +Do not translate the content of code snippets, keep the original in English. For example, `list`, `dict`, keep them as is. + +### Content of code blocks + +Do not translate the content of code blocks, except for comments in the language which the code block uses. + +Examples: + +Source (English) - The code block is a bash code example with one comment: + +```bash +# Print greeting +echo "Hello, World!" +``` + +Result (German): + +```bash +# Gruß ausgeben +echo "Hello, World!" +``` + +Source (English) - The code block is a console example containing HTML tags. No comments, so nothing to change here: + +```console +$ fastapi run main.py + FastAPI Starting server + Searching for package file structure +``` + +Result (German): + +```console +$ fastapi run main.py + FastAPI Starting server + Searching for package file structure +``` + +Source (English) - The code block is a console example containing 5 comments: + + +```console +// Go to the home directory +$ cd +// Create a directory for all your code projects +$ mkdir code +// Enter into that code directory +$ cd code +// Create a directory for this project +$ mkdir awesome-project +// Enter into that project directory +$ cd awesome-project +``` + +Result (German): + +```console +// Gehe zum Home-Verzeichnis +$ cd +// Erstelle ein Verzeichnis für alle Ihre Code-Projekte +$ mkdir code +// Gehe in dieses Code-Verzeichnis +$ cd code +// Erstelle ein Verzeichnis für dieses Projekt +$ mkdir awesome-project +// Gehe in dieses Projektverzeichnis +$ cd awesome-project +``` + +If there is an existing translation and its Mermaid diagram is in sync with the Mermaid diagram in the English source, except a few translated words, then use the Mermaid diagram of the existing translation. The human editor of the translation translated these words in the Mermaid diagram. Keep these translations, do not revert them back to the English source. + +Example: + +Source (English): + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -->|requires| harry-1 + end +``` + +Existing translation (German) - has three translations: + +```mermaid +flowchart LR + subgraph global[globale Umgebung] + harry-1[harry v1] + end + subgraph stone-project[philosophers-stone-Projekt] + stone(philosophers-stone) -->|benötigt| harry-1 + end +``` + +Result (German) - you change nothing: + +```mermaid +flowchart LR + subgraph global[globale Umgebung] + harry-1[harry v1] + end + subgraph stone-project[philosophers-stone-Projekt] + stone(philosophers-stone) -->|benötigt| harry-1 + end +``` + +### Special blocks + +There are special blocks of notes, tips and others that look like: + +/// note +Here goes a note +/// + +To translate it, keep the same line and add the translation after a vertical bar. + +For example, if you were translating to Spanish, you would write: + +/// note | Nota + +Some examples in Spanish: + +Source (English): + +/// tip + +Result (Spanish): + +/// tip | Consejo + +Source (English): + +/// details | Preview + +Result (Spanish): + +/// details | Vista previa + +### Tab blocks + +There are special blocks surrounded by four slashes (////). They mark text, which will be rendered as part of a tab in the final document. The scheme is: + +//// tab | {tab title} +{tab content, may span many lines} +//// + +Keep everything before the vertical bar (|) as is, including the vertical bar. Translate the tab title. Translate the tab content, applying the rules you know. Keep the four block closing slashes as is. + +Examples: + +Source (English): + +//// tab | Python 3.8+ non-Annotated +Hello +//// + +Result (German): + +//// tab | Python 3.8+ nicht annotiert +Hallo +//// + +Source (English) - Here there is nothing to translate in the tab title: + +//// tab | Linux, macOS, Windows Bash +Hello again +//// + +Result (German): + +//// tab | Linux, macOS, Windows Bash +Hallo wieder +//// + +### Headings + +Every Markdown heading in the English text (all levels) ends with a part inside curly brackets. This part denotes the hash of this heading, which is used in links to this heading. In translations, translate the heading, but do not translate this hash part, so that links do not break. + +Examples of how to translate a heading: + +Source (English): + +``` +## Alternative API docs { #alternative-api-docs } +``` + +Result (Spanish): + +``` +## Documentación de la API alternativa { #alternative-api-docs } +``` + +Source (English): + +``` +### Example { #example } +``` + +Result (German): + +``` +### Beispiel { #example } +``` + +### Links + +Use the following rules for links (apply both to Markdown-style links ([text](url)) and to HTML-style text tags): + +- For relative URLs, only translate the link text. Do not translate the URL or its parts. + +Example: + +Source (English): + +``` +[One of the fastest Python frameworks available](#performance) +``` + +Result (German): + +``` +[Eines der schnellsten verfügbaren Python-Frameworks](#performance) +``` + +- For absolute URLs which DO NOT start EXACTLY with https://fastapi.tiangolo.com, only translate the link text and leave the URL unchanged. + +Example: + +Source (English): + +``` +SQLModel docs +``` + +Result (German): + +``` +SQLModel-Dokumentation +``` + +- For absolute URLs which DO start EXACTLY with https://fastapi.tiangolo.com, only translate the link text and change the URL by adding the language code (https://fastapi.tiangolo.com/{language_code}[rest part of the url]). + +Example: + +Source (English): + +``` +Documentation +``` + +Result (Spanish): + +``` +Documentación +``` + +- Do not add language codes for URLs that point to static assets (e.g., images, CSS, JavaScript). + +Example: + +Source (English): + +``` +Something +``` + +Result (Spanish): + +``` +Algo +``` + +- For internal links, only translate link text. + +Example: + +Source (English): + +``` +[Create Pull Requests](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} +``` + +Result (German): + +``` +[Pull Requests erzeugen](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} +``` + +- Do not translate anchor fragments in links (the part after `#`), as they must remain the same to work correctly. + +- If an existing translation has a link with an anchor fragment different to the anchor fragment in the English source, then this is an error. Fix this by using the anchor fragment of the English source. + +Example: + +Source (English): + +``` +[Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank} +``` + +Existing wrong translation (German) - notice the wrongly translated anchor fragment: + +``` +[Body - Mehrere Parameter: Einfache Werte im Body](body-multiple-params.md#einzelne-werte-im-body){.internal-link target=_blank}. +``` + +Result (German) - you fix the anchor fragment: + +``` +[Body - Mehrere Parameter: Einfache Werte im Body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. +``` + +- Do not add anchor fragments at will, even if this makes sense. If the English source has no anchor, don't add one. + +Example: + +Source (English): + +``` +Create a [virtual environment](../virtual-environments.md){.internal-link target=_blank} +``` + +Wrong translation in German - Anchor added to the URL. + +``` +Erstelle eine [virtuelle Umgebung](../virtual-environments.md#create-a-virtual-environment){.internal-link target=_blank} +``` + +Good translation (German) - URL stays like in the English source. + +``` +Erstelle eine [Virtuelle Umgebung](../virtual-environments.md){.internal-link target=_blank} +``` + +### HTML abbr elements + +Translate HTML abbr elements (`text`) as follows: + +- If the text surrounded by the abbr element is an abbreviation (the text may be surrounded by further HTML or Markdown markup or quotes, for example text or `text` or "text", ignore that further markup when deciding if the text is an abbreviation), and if the description (the text inside the title attribute) contains the full phrase for this abbreviation, then append a dash (-) to the full phrase, followed by the translation of the full phrase. + +Conversion scheme: + +Source (English): + +``` +{abbreviation} +``` + +Result: + +``` +{abbreviation} +``` + +Examples: + +Source (English): + +``` +IoT +CPU +TL;DR: +``` + +Result (German): + +``` +IoT +CPU +TL;DR: +``` + +- If the language to which you translate mostly uses the letters of the ASCII char set (for example Spanish, French, German, but not Russian, Chinese) and if the translation of the full phrase is identical to, or starts with the same letters as the original full phrase, then only give the translation of the full phrase. + +Conversion scheme: + +Source (English): + +``` +{abbreviation} +``` + +Result: + +``` +{abbreviation} +``` + +Examples: + +Source (English): + +``` +JWT +Enum +ASGI +``` + +Result (German): + +``` +JWT +Enum +ASGI +``` + +- If the description is not a full phrase for an abbreviation which the abbr element surrounds, but some other information, then just translate the description. + +Conversion scheme: + +Source (English): + +``` +{text} +``` + +Result: + +``` +{translation of text} +``` + +Examples: + + Source (English): + +``` +path +linter +parsing +0.95.0 +at the time of writing this +``` + +Result (German): + +``` +Pfad +Linter +Parsen +0.95.0 +zum Zeitpunkt als das hier geschrieben wurde +``` + +- If the text surrounded by the abbr element is an abbreviation and the description contains both the full phrase for that abbreviation, and other information, separated by a colon (`:`), then append a dash (`-`) and the translation of the full phrase to the original full phrase and translate the other information. + +Conversion scheme: + +Source (English): + +``` +{abbreviation} +``` + +Result: + +``` +{abbreviation} +``` + +Examples: + +Source (English): + +``` +I/O +CDN +IDE +``` + +Result (German): + +``` +I/O +CDN +IDE +``` + +- You can leave the original full phrase away, if the translated full phrase is identical or starts with the same letters as the original full phrase. + +Conversion scheme: + +Source (English): + +``` +{abbreviation} +``` + +Result: + +``` +{abbreviation} +``` + +Example: + +Source (English): + +``` +ORM +``` + +Result (German): + +``` +ORM +``` + +- If there is an existing translation, and it has ADDITIONAL abbr elements in a sentence, and these additional abbr elements do not exist in the related sentence in the English text, then KEEP those additional abbr elements in the translation. Do not remove them. Except when you remove the whole sentence from the translation, because the whole sentence was removed from the English text, then also remove the abbr element. The reasoning for this rule is, that such additional abbr elements are manually added by the human editor of the translation, in order to translate or explain an English word to the human readers of the translation. These additional abbr elements would not make sense in the English text, but they do make sense in the translation. So keep them in the translation, even though they are not part of the English text. This rule only applies to abbr elements. + +- Apply above rules also when there is an existing translation! Make sure that all title attributes in abbr elements get properly translated or updated, using the schemes given above. However, leave the ADDITIONAL abbr's described above alone. Do not change their formatting or content. diff --git a/scripts/translate.py b/scripts/translate.py index 66da46a08..ffecfde07 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -25,650 +25,8 @@ non_translated_sections = ( "contributing.md", ) - -general_prompt = """ -### About literal text in this prompt - -1) In the following instructions (after I say: `The above rules are in effect now`) the two characters `«` and `»` will be used to surround LITERAL TEXT, which is text or characters you shall interpret literally. The `«` and the `»` are not part of the literal text, they are the meta characters denoting it. - -2) Furthermore, text surrounded by `«««` and `»»»` is a BLOCK OF LITERAL TEXT which spans multiple lines. To get its content, dedent all lines of the block until the `«««` and `»»»` are at column zero, then remove the newline (`\n`) after the `«««` and the newline before the `»»»`. The `«««` and the `»»»` are not part of the literal text block, they are the meta characters denoting it. - -3) If you see backticks or any other quotes inside literal text – inside `«` and `»` – or inside blocks of literal text – inside `«««` and `»»»` – then interpret them as literal characters, do NOT interpret them as meta characters. - -The above rules are in effect now. - - -### Definitions of terms used in this prompt - -"backtick" - - The character «`» - Unicode U+0060 (GRAVE ACCENT) - -"single backtick" - - A single backtick – «`» - -"triple backticks" - - Three backticks in a row – «```» - -"neutral double quote" - - The character «"» - Unicode U+0022 (QUOTATION MARK) - -"neutral single quote" - - The character «'» - Unicode U+0027 (APOSTROPHE) - -"English double typographic quotes" - - The characters «“» and «”» - Unicode U+201C (LEFT DOUBLE QUOTATION MARK) and Unicode U+201D (RIGHT DOUBLE QUOTATION MARK) - -"English single typographic quotes" - - The characters «‘» and «’» - Unicode U+2018 (LEFT SINGLE QUOTATION MARK) and Unicode U+2019 (RIGHT SINGLE QUOTATION MARK) - -"code snippet" - - Also called "inline code". Text in a Markdown document which is surrounded by single backticks. A paragraph in a Markdown document can have a more than one code snippet. - - Example: - - ««« - `i am a code snippet` - »»» - - Example: - - ««« - `first code snippet` `second code snippet` `third code snippet` - »»» - -"code block" - - Text in a Markdown document which is surrounded by triple backticks. Spreads multiple lines. - - Example: - - ««« - ``` - Hello - World - ``` - »»» - - Example: - - ««« - ```python - print("hello World") - ``` - »»» - -"HTML element" - - a HTML opening tag – e.g. «
» – and a HTML closing tag – e.g. «
» – surrounding text or other HTML elements. - - -### Your task - -Translate an English text – the original content – to a target language. - -The original content is written in Markdown, write the translation in Markdown as well. - -The original content will be surrounded by triple percentage signs («%%%»). Do not include the triple percentage signs in the translation. - - -### Technical terms in English - -For technical terms in English that don't have a common translation term, use the original term in English. - - -### Content of code snippets - -Do not translate the content of code snippets, keep the original in English. For example, «`list`», «`dict`», keep them as is. - - -### Content of code blocks - -Do not translate the content of code blocks, except for comments in the language which the code block uses. - -Examples: - - Source (English) – The code block is a bash code example with one comment: - - ««« - ```bash - # Print greeting - echo "Hello, World!" - ``` - »»» - - Result (German): - - ««« - ```bash - # Gruß ausgeben - echo "Hello, World!" - ``` - »»» - - Source (English) – The code block is a console example containing HTML tags. No comments, so nothing to change here: - - ««« - ```console - $ fastapi run main.py - FastAPI Starting server - Searching for package file structure - ``` - »»» - - Result (German): - - ««« - ```console - $ fastapi run main.py - FastAPI Starting server - Searching for package file structure - ``` - »»» - - Source (English) – The code block is a console example containing 5 comments: - - ««« - ```console - // Go to the home directory - $ cd - // Create a directory for all your code projects - $ mkdir code - // Enter into that code directory - $ cd code - // Create a directory for this project - $ mkdir awesome-project - // Enter into that project directory - $ cd awesome-project - ``` - »»» - - Result (German): - - ««« - ```console - // Gehe zum Home-Verzeichnis - $ cd - // Erstelle ein Verzeichnis für alle Ihre Code-Projekte - $ mkdir code - // Gehe in dieses Code-Verzeichnis - $ cd code - // Erstelle ein Verzeichnis für dieses Projekt - $ mkdir awesome-project - // Gehe in dieses Projektverzeichnis - $ cd awesome-project - ``` - »»» - -If there is an existing translation and its Mermaid diagram is in sync with the Mermaid diagram in the English source, except a few translated words, then use the Mermaid diagram of the existing translation. The human editor of the translation translated these words in the Mermaid diagram. Keep these translations, do not revert them back to the English source. - -Example: - - Source (English): - - ««« - ```mermaid - flowchart LR - subgraph global[global env] - harry-1[harry v1] - end - subgraph stone-project[philosophers-stone project] - stone(philosophers-stone) -->|requires| harry-1 - end - ``` - »»» - - Existing translation (German) – has three translations: - - ««« - ```mermaid - flowchart LR - subgraph global[globale Umgebung] - harry-1[harry v1] - end - subgraph stone-project[philosophers-stone-Projekt] - stone(philosophers-stone) -->|benötigt| harry-1 - end - ``` - »»» - - Result (German) – you change nothing: - - ««« - ```mermaid - flowchart LR - subgraph global[globale Umgebung] - harry-1[harry v1] - end - subgraph stone-project[philosophers-stone-Projekt] - stone(philosophers-stone) -->|benötigt| harry-1 - end - ``` - »»» - - -### Special blocks - -There are special blocks of notes, tips and others that look like: - - ««« - /// note - »»» - -To translate it, keep the same line and add the translation after a vertical bar. - -For example, if you were translating to Spanish, you would write: - - ««« - /// note | Nota - »»» - -Some examples in Spanish: - - Source: - - ««« - /// tip - »»» - - Result: - - ««« - /// tip | Consejo - »»» - - Source: - - ««« - /// details | Preview - »»» - - Result: - - ««« - /// details | Vista previa - »»» - - -### Tab blocks - -There are special blocks surrounded by four slashes («////»). They mark text, which will be rendered as part of a tab in the final document. The scheme is: - - //// tab | {tab title} - {tab content, may span many lines} - //// - -Keep everything before the vertical bar («|») as is, including the vertical bar. Translate the tab title. Translate the tab content, applying the rules you know. Keep the four block closing slashes as is. - -Examples: - - Source (English): - - ««« - //// tab | Python 3.8+ non-Annotated - Hello - //// - »»» - - Result (German): - - ««« - //// tab | Python 3.8+ nicht annotiert - Hallo - //// - »»» - - Source (English) – Here there is nothing to translate in the tab title: - - ««« - //// tab | Linux, macOS, Windows Bash - Hello again - //// - »»» - - Result (German): - - ««« - //// tab | Linux, macOS, Windows Bash - Hallo wieder - //// - »»» - - -### Headings - -Every Markdown heading in the English text (all levels) ends with a part inside curly brackets. This part denotes the hash of this heading, which is used in links to this heading. In translations, translate the heading, but do not translate this hash part, so that links do not break. - -Examples of how to translate a heading: - - Source (English): - - ««« - ## Alternative API docs { #alternative-api-docs } - »»» - - Result (Spanish): - - ««« - ## Documentación de la API alternativa { #alternative-api-docs } - »»» - - Source (English): - - ««« - ### Example { #example } - »»» - - Result (German): - - ««« - ### Beispiel { #example } - »»» - - -### Links - -Use the following rules for links (apply both to Markdown-style links ([text](url)) and to HTML-style tags): - -1) For relative URLs, only translate link text. Do not translate the URL or its parts - -Example: - - Source (English): - - ««« - [One of the fastest Python frameworks available](#performance) - »»» - - Result (German): - - ««« - [Eines der schnellsten verfügbaren Python-Frameworks](#performance) - »»» - -2) For absolute URLs which DO NOT start EXACTLY with «https://fastapi.tiangolo.com», only translate link text and leave the URL unchanged. - -Example: - - Source (English): - - ««« - SQLModel docs - »»» - - Result (German): - - ««« - SQLModel-Dokumentation - »»» - -3) For absolute URLs which DO start EXACTLY with «https://fastapi.tiangolo.com», only translate link text and change the URL by adding language code («https://fastapi.tiangolo.com/{language_code}[rest part of the url]»). - -Example: - - Source (English): - - ««« - Documentation - »»» - - Result (Spanish): - - ««« - Documentación - »»» - -3.1) Do not add language codes for URLs that point to static assets (e.g., images, CSS, JavaScript). - -Example: - - Source (English): - - ««« - Something - »»» - - Result (Spanish): - - ««« - Algo - »»» - -4) For internal links, only translate link text. - -Example: - - Source (English): - - ««« - [Create Pull Requests](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} - »»» - - Result (German): - - ««« - [Pull Requests erzeugen](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} - »»» - -5) Do not translate anchor fragments in links (the part after «#»), as they must remain the same to work correctly. - -5.1) If an existing translation has a link with an anchor fragment different to the anchor fragment in the English source, then this is an error. Fix this by using the anchor fragment of the English source. - -Example: - - Source (English): - - ««« - [Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank} - »»» - - Existing wrong translation (German) – notice the wrongly translated anchor fragment: - - ««« - [Body – Mehrere Parameter: Einfache Werte im Body](body-multiple-params.md#einzelne-werte-im-body){.internal-link target=_blank}. - »»» - - Result (German) – you fix the anchor fragment: - - ««« - [Body – Mehrere Parameter: Einfache Werte im Body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. - »»» - -5.2) Do not add anchor fragments at will, even if this makes sense. If the English source has no anchor, don't add one. - -Example: - - Source (English): - - ««« - Create a [virtual environment](../virtual-environments.md){.internal-link target=_blank} - »»» - - Wrong translation (German) – Anchor added to the URL. - - ««« - Erstelle eine [virtuelle Umgebung](../virtual-environments.md#create-a-virtual-environment){.internal-link target=_blank} - »»» - - Good translation (German) – URL stays like in the English source. - - ««« - Erstelle eine [Virtuelle Umgebung](../virtual-environments.md){.internal-link target=_blank} - »»» - - -### HTML abbr elements - -Translate HTML abbr elements («text») as follows: - -1) If the text surrounded by the abbr element is an abbreviation (the text may be surrounded by further HTML or Markdown markup or quotes, for example «text» or «`text`» or «"text"», ignore that further markup when deciding if the text is an abbreviation), and if the description (the text inside the title attribute) contains the full phrase for this abbreviation, then append a dash («–») to the full phrase, followed by the translation of the full phrase. - -Conversion scheme: - - Source (English): - - {abbreviation} - - Result: - - {abbreviation} - -Examples: - - Source (English): - - ««« - IoT - CPU - TL;DR: - »»» - - Result (German): - - ««« - IoT - CPU - TL;DR: - »»» - -1.1) If the language to which you translate mostly uses the letters of the ASCII char set (for example Spanish, French, German, but not Russian, Chinese) and if the translation of the full phrase is identical to, or starts with the same letters as the original full phrase, then only give the translation of the full phrase. - -Conversion scheme: - - Source (English): - - {abbreviation} - - Result: - - {abbreviation} - -Examples: - - Source (English): - - ««« - JWT - Enum - ASGI - »»» - - Result (German): - - ««« - JWT - Enum - ASGI - »»» - -2) If the description is not a full phrase for an abbreviation which the abbr element surrounds, but some other information, then just translate the description. - -Conversion scheme: - - Source (English): - - {text} - - Result: - - {translation of text} - -Examples: - - Source (English): - - ««« - path - linter - parsing - 0.95.0 - at the time of writing this - »»» - - Result (German): - - ««« - Pfad - Linter - Parsen - 0.95.0 - zum Zeitpunkt als das hier geschrieben wurde - »»» - - -3) If the text surrounded by the abbr element is an abbreviation and the description contains both the full phrase for that abbreviation, and other information, separated by a colon («:»), then append a dash («–») and the translation of the full phrase to the original full phrase and translate the other information. - -Conversion scheme: - - Source (English): - - {abbreviation} - - Result: - - {abbreviation} - -Examples: - - Source (English): - - ««« - I/O - CDN - IDE - »»» - - Result (German): - - ««« - I/O - CDN - IDE - »»» - -3.1) Like in rule 2.1, you can leave the original full phrase away, if the translated full phrase is identical or starts with the same letters as the original full phrase. - -Conversion scheme: - - Source (English): - - {abbreviation} - - Result: - - {abbreviation} - -Example: - - Source (English): - - ««« - ORM - »»» - - Result (German): - - ««« - ORM - »»» - -4) If there is an existing translation, and it has ADDITIONAL abbr elements in a sentence, and these additional abbr elements do not exist in the related sentence in the English text, then KEEP those additional abbr elements in the translation. Do not remove them. Except when you remove the whole sentence from the translation, because the whole sentence was removed from the English text, then also remove the abbr element. The reasoning for this rule is, that such additional abbr elements are manually added by the human editor of the translation, in order to translate or explain an English word to the human readers of the translation. These additional abbr elements would not make sense in the English text, but they do make sense in the translation. So keep them in the translation, even though they are not part of the English text. This rule only applies to abbr elements. - -5) Apply above rules also when there is an existing translation! Make sure that all title attributes in abbr elements get properly translated or updated, using the schemes given above. However, leave the ADDITIONAL abbr's from rule 4 alone. Do not change their formatting or content. - -""" +general_prompt_path = Path(__file__).absolute().parent / "llm-general-prompt.md" +general_prompt = general_prompt_path.read_text(encoding="utf-8") app = typer.Typer() From 53d2453d1a77f3384a1648d717f8ddafb5e9e460 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 29 Dec 2025 18:54:43 +0000 Subject: [PATCH 038/243] =?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 9c4fe05ea..2eb674624 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,7 @@ hide: ### Internal +* 🌐 Update translation prompts. PR [#14619](https://github.com/fastapi/fastapi/pull/14619) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update LLM translation script to guide reviewers to change the prompt. PR [#14614](https://github.com/fastapi/fastapi/pull/14614) by [@tiangolo](https://github.com/tiangolo). * 👷 Do not run translations on cron while finishing updating existing languages. PR [#14613](https://github.com/fastapi/fastapi/pull/14613) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove test variants for Pydantic v1 in test_request_params. PR [#14612](https://github.com/fastapi/fastapi/pull/14612) by [@tiangolo](https://github.com/tiangolo). From 258deb925d9d65f2d75392a2b941d6b072e305b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 1 Jan 2026 22:40:15 -0800 Subject: [PATCH 039/243] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Contributors=20and=20Translators=20(#14625)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/en/data/contributors.yml | 29 +++-- docs/en/data/translation_reviewers.yml | 146 +++++++++++++------------ docs/en/data/translators.yml | 26 ++--- 3 files changed, 108 insertions(+), 93 deletions(-) diff --git a/docs/en/data/contributors.yml b/docs/en/data/contributors.yml index 163dc68e3..0c144cd4c 100644 --- a/docs/en/data/contributors.yml +++ b/docs/en/data/contributors.yml @@ -1,6 +1,6 @@ tiangolo: login: tiangolo - count: 808 + count: 857 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo dependabot: @@ -10,7 +10,7 @@ dependabot: url: https://github.com/apps/dependabot alejsdev: login: alejsdev - count: 52 + count: 53 avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 url: https://github.com/alejsdev pre-commit-ci: @@ -18,6 +18,11 @@ pre-commit-ci: count: 50 avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 url: https://github.com/apps/pre-commit-ci +YuriiMotov: + login: YuriiMotov + count: 36 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + url: https://github.com/YuriiMotov github-actions: login: github-actions count: 26 @@ -28,26 +33,21 @@ Kludex: count: 25 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex -YuriiMotov: - login: YuriiMotov - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov dmontagu: login: dmontagu count: 17 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu +svlandeg: + login: svlandeg + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg nilslindemann: login: nilslindemann count: 15 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann -svlandeg: - login: svlandeg - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 - url: https://github.com/svlandeg euri10: login: euri10 count: 13 @@ -553,6 +553,11 @@ DanielKusyDev: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/36250676?u=2ea6114ff751fc48b55f231987a0e2582c6b1bd2&v=4 url: https://github.com/DanielKusyDev +Viicos: + login: Viicos + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/65306057?u=fcd677dc1b9bef12aa103613e5ccb3f8ce305af9&v=4 + url: https://github.com/Viicos DanielYang59: login: DanielYang59 count: 2 diff --git a/docs/en/data/translation_reviewers.yml b/docs/en/data/translation_reviewers.yml index c3d3d0388..62db8e805 100644 --- a/docs/en/data/translation_reviewers.yml +++ b/docs/en/data/translation_reviewers.yml @@ -15,7 +15,7 @@ sodaMelon: url: https://github.com/sodaMelon ceb10n: login: ceb10n - count: 116 + count: 117 avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 url: https://github.com/ceb10n tokusumi: @@ -23,16 +23,16 @@ tokusumi: count: 104 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi +hard-coders: + login: hard-coders + count: 96 + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=78d12d1acdf853c817700145e73de7fd9e5d068b&v=4 + url: https://github.com/hard-coders hasansezertasan: login: hasansezertasan count: 95 avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 url: https://github.com/hasansezertasan -hard-coders: - login: hard-coders - count: 93 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 - url: https://github.com/hard-coders alv2017: login: alv2017 count: 88 @@ -40,7 +40,7 @@ alv2017: url: https://github.com/alv2017 nazarepiedady: login: nazarepiedady - count: 86 + count: 87 avatarUrl: https://avatars.githubusercontent.com/u/31008635?u=f69ddc4ea8bda3bdfac7aa0e2ea38de282e6ee2d&v=4 url: https://github.com/nazarepiedady AlertRED: @@ -48,6 +48,11 @@ AlertRED: count: 81 avatarUrl: https://avatars.githubusercontent.com/u/15695000?u=f5a4944c6df443030409c88da7d7fa0b7ead985c&v=4 url: https://github.com/AlertRED +tiangolo: + login: tiangolo + count: 73 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo Alexandrhub: login: Alexandrhub count: 68 @@ -63,21 +68,16 @@ cassiobotaro: count: 62 avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=a08022b191ddbd0a6159b2981d9d878b6d5bb71f&v=4 url: https://github.com/cassiobotaro +mattwang44: + login: mattwang44 + count: 61 + avatarUrl: https://avatars.githubusercontent.com/u/24987826?u=58e37fb3927b9124b458945ac4c97aa0f1062d85&v=4 + url: https://github.com/mattwang44 nilslindemann: login: nilslindemann count: 59 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann -mattwang44: - login: mattwang44 - count: 59 - avatarUrl: https://avatars.githubusercontent.com/u/24987826?u=58e37fb3927b9124b458945ac4c97aa0f1062d85&v=4 - url: https://github.com/mattwang44 -tiangolo: - login: tiangolo - count: 56 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo Laineyzhang55: login: Laineyzhang55 count: 48 @@ -88,6 +88,11 @@ Kludex: count: 47 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex +YuriiMotov: + login: YuriiMotov + count: 46 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + url: https://github.com/YuriiMotov komtaki: login: komtaki count: 45 @@ -118,11 +123,6 @@ Winand: count: 40 avatarUrl: https://avatars.githubusercontent.com/u/53390?u=bb0e71a2fc3910a8e0ee66da67c33de40ea695f8&v=4 url: https://github.com/Winand -YuriiMotov: - login: YuriiMotov - count: 40 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov solomein-sv: login: solomein-sv count: 38 @@ -138,6 +138,11 @@ alejsdev: count: 37 avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 url: https://github.com/alejsdev +mezgoodle: + login: mezgoodle + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/41520940?u=4a9c765af688389d54296845d18b8f6cd6ddf09a&v=4 + url: https://github.com/mezgoodle stlucasgarcia: login: stlucasgarcia count: 36 @@ -153,11 +158,6 @@ timothy-jeong: count: 36 avatarUrl: https://avatars.githubusercontent.com/u/53824764?u=db3d0cea2f5fab64d810113c5039a369699a2774&v=4 url: https://github.com/timothy-jeong -mezgoodle: - login: mezgoodle - count: 35 - avatarUrl: https://avatars.githubusercontent.com/u/41520940?u=4a9c765af688389d54296845d18b8f6cd6ddf09a&v=4 - url: https://github.com/mezgoodle rjNemo: login: rjNemo count: 34 @@ -173,6 +173,11 @@ akarev0: count: 33 avatarUrl: https://avatars.githubusercontent.com/u/53393089?u=6e528bb4789d56af887ce6fe237bea4010885406&v=4 url: https://github.com/akarev0 +Vincy1230: + login: Vincy1230 + count: 33 + avatarUrl: https://avatars.githubusercontent.com/u/81342412?u=ab5e256a4077a4a91f3f9cd2115ba80780454cbe&v=4 + url: https://github.com/Vincy1230 romashevchenko: login: romashevchenko count: 32 @@ -183,11 +188,6 @@ LorhanSohaky: count: 30 avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 url: https://github.com/LorhanSohaky -Vincy1230: - login: Vincy1230 - count: 30 - avatarUrl: https://avatars.githubusercontent.com/u/81342412?u=ab5e256a4077a4a91f3f9cd2115ba80780454cbe&v=4 - url: https://github.com/Vincy1230 black-redoc: login: black-redoc count: 29 @@ -250,7 +250,7 @@ mycaule: url: https://github.com/mycaule Aruelius: login: Aruelius - count: 24 + count: 25 avatarUrl: https://avatars.githubusercontent.com/u/25380989?u=574f8cfcda3ea77a3f81884f6b26a97068e36a9d&v=4 url: https://github.com/Aruelius wisderfin: @@ -263,6 +263,11 @@ OzgunCaglarArslan: count: 24 avatarUrl: https://avatars.githubusercontent.com/u/86166426?v=4 url: https://github.com/OzgunCaglarArslan +ycd: + login: ycd + count: 23 + avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=f1e7bae394a315da950912c92dc861a8eaf95d4c&v=4 + url: https://github.com/ycd sh0nk: login: sh0nk count: 23 @@ -288,11 +293,6 @@ Attsun1031: count: 20 avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 url: https://github.com/Attsun1031 -ycd: - login: ycd - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=f1e7bae394a315da950912c92dc861a8eaf95d4c&v=4 - url: https://github.com/ycd delhi09: login: delhi09 count: 20 @@ -418,6 +418,11 @@ mattkoehne: count: 14 avatarUrl: https://avatars.githubusercontent.com/u/80362153?v=4 url: https://github.com/mattkoehne +maru0123-2004: + login: maru0123-2004 + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/43961566?u=16ed8603a4d6a4665cb6c53a7aece6f31379b769&v=4 + url: https://github.com/maru0123-2004 jovicon: login: jovicon count: 13 @@ -443,6 +448,11 @@ impocode: count: 13 avatarUrl: https://avatars.githubusercontent.com/u/109408819?u=9cdfc5ccb31a2094c520f41b6087012fa9048982&v=4 url: https://github.com/impocode +waketzheng: + login: waketzheng + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/35413830?u=df19e4fd5bb928e7d086e053ef26a46aad23bf84&v=4 + url: https://github.com/waketzheng wesinalves: login: wesinalves count: 13 @@ -538,21 +548,16 @@ Lufa1u: count: 11 avatarUrl: https://avatars.githubusercontent.com/u/112495876?u=087658920ed9e74311597bdd921d8d2de939d276&v=4 url: https://github.com/Lufa1u -waketzheng: - login: waketzheng - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/35413830?u=df19e4fd5bb928e7d086e053ef26a46aad23bf84&v=4 - url: https://github.com/waketzheng KNChiu: login: KNChiu count: 11 avatarUrl: https://avatars.githubusercontent.com/u/36751646?v=4 url: https://github.com/KNChiu -maru0123-2004: - login: maru0123-2004 +Zhongheng-Cheng: + login: Zhongheng-Cheng count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/43961566?u=16ed8603a4d6a4665cb6c53a7aece6f31379b769&v=4 - url: https://github.com/maru0123-2004 + avatarUrl: https://avatars.githubusercontent.com/u/95612344?u=a0f7730a3cc7486827965e01a119ad610bda4b0a&v=4 + url: https://github.com/Zhongheng-Cheng mariacamilagl: login: mariacamilagl count: 10 @@ -608,16 +613,16 @@ nick-cjyx9: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/119087246?u=7227a2de948c68fb8396d5beff1ee5b0e057c42e&v=4 url: https://github.com/nick-cjyx9 +marcelomarkus: + login: marcelomarkus + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/20115018?u=dda090ce9160ef0cd2ff69b1e5ea741283425cba&v=4 + url: https://github.com/marcelomarkus lucasbalieiro: login: lucasbalieiro count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/37416577?u=dad91601ee4f40458d691774ec439aff308344d7&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/37416577?u=d144221c34c08adac8b20e1833d776ffa1c4b1d0&v=4 url: https://github.com/lucasbalieiro -Zhongheng-Cheng: - login: Zhongheng-Cheng - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/95612344?u=a0f7730a3cc7486827965e01a119ad610bda4b0a&v=4 - url: https://github.com/Zhongheng-Cheng RunningIkkyu: login: RunningIkkyu count: 9 @@ -668,11 +673,6 @@ yodai-yodai: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/7031039?u=4f3593f5931892b931a745cfab846eff6e9332e7&v=4 url: https://github.com/yodai-yodai -marcelomarkus: - login: marcelomarkus - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/20115018?u=dda090ce9160ef0cd2ff69b1e5ea741283425cba&v=4 - url: https://github.com/marcelomarkus JoaoGustavoRogel: login: JoaoGustavoRogel count: 9 @@ -683,6 +683,11 @@ Yarous: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/61277193?u=5b462347458a373b2d599c6f416d2b75eddbffad&v=4 url: https://github.com/Yarous +Pyth3rEx: + login: Pyth3rEx + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/26427764?u=087724f74d813c95925d51e354554bd4b6d6bb60&v=4 + url: https://github.com/Pyth3rEx dimaqq: login: dimaqq count: 8 @@ -1023,6 +1028,11 @@ devluisrodrigues: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/21125286?v=4 url: https://github.com/11kkw +EdmilsonRodrigues: + login: EdmilsonRodrigues + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4 + url: https://github.com/EdmilsonRodrigues lpdswing: login: lpdswing count: 4 @@ -1163,6 +1173,11 @@ AbolfazlKameli: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=af8f025278cce0d489007071254e4055df60b78c&v=4 url: https://github.com/AbolfazlKameli +SBillion: + login: SBillion + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/1070649?u=3ab493dfc88b39da0eb1600e3b8e7df1c90a5dee&v=4 + url: https://github.com/SBillion tyronedamasceno: login: tyronedamasceno count: 3 @@ -1211,7 +1226,7 @@ phamquanganh31101998: peebbv6364: login: peebbv6364 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/26784747?u=75583df215ee01a5cd2dc646aecb81e7dbd33d06&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/26784747?u=3bf07017eb4f4fa3639ba8d4ed19980a34bf8f90&v=4 url: https://github.com/peebbv6364 mrparalon: login: mrparalon @@ -1413,11 +1428,6 @@ Mohammad222PR: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/116789737?u=25810a5fe049d2f1618e2e7417cea011cc353ce4&v=4 url: https://github.com/Mohammad222PR -EdmilsonRodrigues: - login: EdmilsonRodrigues - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4 - url: https://github.com/EdmilsonRodrigues blaisep: login: blaisep count: 2 @@ -1838,11 +1848,11 @@ NavesSapnis: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/79222417?u=b5b10291b8e9130ca84fd20f0a641e04ed94b6b1&v=4 url: https://github.com/NavesSapnis -eqsdxr: - login: eqsdxr +isgin01: + login: isgin01 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=7927dc0366995334f9a18c3204a41d3a34d6d96f&v=4 - url: https://github.com/eqsdxr + avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=ddffde10876b50f35dc90d1337f507a630530a6a&v=4 + url: https://github.com/isgin01 syedasamina56: login: syedasamina56 count: 2 diff --git a/docs/en/data/translators.yml b/docs/en/data/translators.yml index c66eff4d4..940b128da 100644 --- a/docs/en/data/translators.yml +++ b/docs/en/data/translators.yml @@ -1,6 +1,6 @@ nilslindemann: login: nilslindemann - count: 125 + count: 130 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann jaystone776: @@ -28,6 +28,11 @@ SwftAlpc: count: 23 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 url: https://github.com/SwftAlpc +tiangolo: + login: tiangolo + count: 22 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo hasansezertasan: login: hasansezertasan count: 22 @@ -46,7 +51,7 @@ AlertRED: hard-coders: login: hard-coders count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=78d12d1acdf853c817700145e73de7fd9e5d068b&v=4 url: https://github.com/hard-coders Joao-Pedro-P-Holanda: login: Joao-Pedro-P-Holanda @@ -103,11 +108,6 @@ pablocm83: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/28315068?u=3310fbb05bb8bfc50d2c48b6cb64ac9ee4a14549&v=4 url: https://github.com/pablocm83 -tiangolo: - login: tiangolo - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo ptt3199: login: ptt3199 count: 7 @@ -126,13 +126,18 @@ batlopes: lucasbalieiro: login: lucasbalieiro count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/37416577?u=dad91601ee4f40458d691774ec439aff308344d7&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/37416577?u=d144221c34c08adac8b20e1833d776ffa1c4b1d0&v=4 url: https://github.com/lucasbalieiro Alexandrhub: login: Alexandrhub count: 6 avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 url: https://github.com/Alexandrhub +YuriiMotov: + login: YuriiMotov + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + url: https://github.com/YuriiMotov Serrones: login: Serrones count: 5 @@ -358,11 +363,6 @@ ruzia: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/24503?v=4 url: https://github.com/ruzia -YuriiMotov: - login: YuriiMotov - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov izaguerreiro: login: izaguerreiro count: 2 From 6854be9ebcef23edf2bd34f6e4833e731031eccc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Jan 2026 06:40:39 +0000 Subject: [PATCH 040/243] =?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 2eb674624..34ed433ce 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,7 @@ hide: ### Internal +* 👥 Update FastAPI People - Contributors and Translators. PR [#14625](https://github.com/fastapi/fastapi/pull/14625) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translation prompts. PR [#14619](https://github.com/fastapi/fastapi/pull/14619) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update LLM translation script to guide reviewers to change the prompt. PR [#14614](https://github.com/fastapi/fastapi/pull/14614) by [@tiangolo](https://github.com/tiangolo). * 👷 Do not run translations on cron while finishing updating existing languages. PR [#14613](https://github.com/fastapi/fastapi/pull/14613) by [@tiangolo](https://github.com/tiangolo). From 31c7ffcdfebe32ca6b2feb7b33260aff3880c268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 1 Jan 2026 22:46:34 -0800 Subject: [PATCH 041/243] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20GitHu?= =?UTF-8?q?b=20topic=20repositories=20(#14630)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/en/data/topic_repos.yml | 476 +++++++++++++++++------------------ 1 file changed, 238 insertions(+), 238 deletions(-) diff --git a/docs/en/data/topic_repos.yml b/docs/en/data/topic_repos.yml index cb7e3c033..d089c7e5a 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: 39475 + stars: 40334 owner_login: fastapi owner_html_url: https://github.com/fastapi - name: Hello-Python html_url: https://github.com/mouredev/Hello-Python - stars: 33090 + stars: 33628 owner_login: mouredev owner_html_url: https://github.com/mouredev - name: serve html_url: https://github.com/jina-ai/serve - stars: 21798 + stars: 21817 owner_login: jina-ai owner_html_url: https://github.com/jina-ai - name: HivisionIDPhotos html_url: https://github.com/Zeyi-Lin/HivisionIDPhotos - stars: 20258 + stars: 20409 owner_login: Zeyi-Lin owner_html_url: https://github.com/Zeyi-Lin - name: sqlmodel html_url: https://github.com/fastapi/sqlmodel - stars: 17212 + stars: 17415 owner_login: fastapi owner_html_url: https://github.com/fastapi -- name: Douyin_TikTok_Download_API - html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API - stars: 15145 - owner_login: Evil0ctal - owner_html_url: https://github.com/Evil0ctal - name: fastapi-best-practices html_url: https://github.com/zhanymkanov/fastapi-best-practices - stars: 14644 + stars: 15776 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov +- name: Douyin_TikTok_Download_API + html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API + stars: 15588 + owner_login: Evil0ctal + owner_html_url: https://github.com/Evil0ctal - name: machine-learning-zoomcamp html_url: https://github.com/DataTalksClub/machine-learning-zoomcamp - stars: 12320 + stars: 12447 owner_login: DataTalksClub owner_html_url: https://github.com/DataTalksClub -- name: fastapi_mcp - html_url: https://github.com/tadata-org/fastapi_mcp - stars: 11174 - owner_login: tadata-org - owner_html_url: https://github.com/tadata-org - name: SurfSense html_url: https://github.com/MODSetter/SurfSense - stars: 10858 + stars: 12128 owner_login: MODSetter owner_html_url: https://github.com/MODSetter +- name: fastapi_mcp + html_url: https://github.com/tadata-org/fastapi_mcp + stars: 11326 + owner_login: tadata-org + owner_html_url: https://github.com/tadata-org - name: awesome-fastapi html_url: https://github.com/mjhea0/awesome-fastapi - stars: 10758 + stars: 10901 owner_login: mjhea0 owner_html_url: https://github.com/mjhea0 - name: XHS-Downloader html_url: https://github.com/JoeanAmier/XHS-Downloader - stars: 9313 + stars: 9584 owner_login: JoeanAmier owner_html_url: https://github.com/JoeanAmier -- name: FastUI - html_url: https://github.com/pydantic/FastUI - stars: 8915 - owner_login: pydantic - owner_html_url: https://github.com/pydantic - name: polar html_url: https://github.com/polarsource/polar - stars: 8339 + stars: 8951 owner_login: polarsource owner_html_url: https://github.com/polarsource +- name: FastUI + html_url: https://github.com/pydantic/FastUI + stars: 8934 + owner_login: pydantic + owner_html_url: https://github.com/pydantic - name: FileCodeBox html_url: https://github.com/vastsa/FileCodeBox - stars: 7721 + stars: 7934 owner_login: vastsa owner_html_url: https://github.com/vastsa - name: nonebot2 html_url: https://github.com/nonebot/nonebot2 - stars: 7170 + stars: 7248 owner_login: nonebot owner_html_url: https://github.com/nonebot - name: hatchet html_url: https://github.com/hatchet-dev/hatchet - stars: 6253 + stars: 6392 owner_login: hatchet-dev owner_html_url: https://github.com/hatchet-dev - name: fastapi-users html_url: https://github.com/fastapi-users/fastapi-users - stars: 5849 + stars: 5899 owner_login: fastapi-users owner_html_url: https://github.com/fastapi-users - name: serge html_url: https://github.com/serge-chat/serge - stars: 5756 + stars: 5754 owner_login: serge-chat owner_html_url: https://github.com/serge-chat - name: strawberry html_url: https://github.com/strawberry-graphql/strawberry - stars: 4569 + stars: 4577 owner_login: strawberry-graphql owner_html_url: https://github.com/strawberry-graphql -- name: chatgpt-web-share - html_url: https://github.com/chatpire/chatgpt-web-share - stars: 4294 - owner_login: chatpire - owner_html_url: https://github.com/chatpire - name: poem html_url: https://github.com/poem-web/poem - stars: 4276 + stars: 4303 owner_login: poem-web owner_html_url: https://github.com/poem-web +- name: chatgpt-web-share + html_url: https://github.com/chatpire/chatgpt-web-share + stars: 4287 + owner_login: chatpire + owner_html_url: https://github.com/chatpire - name: dynaconf html_url: https://github.com/dynaconf/dynaconf - stars: 4202 + stars: 4221 owner_login: dynaconf owner_html_url: https://github.com/dynaconf -- name: atrilabs-engine - html_url: https://github.com/Atri-Labs/atrilabs-engine - stars: 4093 - owner_login: Atri-Labs - owner_html_url: https://github.com/Atri-Labs - name: Kokoro-FastAPI html_url: https://github.com/remsky/Kokoro-FastAPI - stars: 4019 + stars: 4181 owner_login: remsky owner_html_url: https://github.com/remsky +- name: atrilabs-engine + html_url: https://github.com/Atri-Labs/atrilabs-engine + stars: 4090 + owner_login: Atri-Labs + owner_html_url: https://github.com/Atri-Labs +- name: devpush + html_url: https://github.com/hunvreus/devpush + stars: 4037 + owner_login: hunvreus + owner_html_url: https://github.com/hunvreus - name: logfire html_url: https://github.com/pydantic/logfire - stars: 3805 + stars: 3896 owner_login: pydantic owner_html_url: https://github.com/pydantic - name: LitServe html_url: https://github.com/Lightning-AI/LitServe - stars: 3719 + stars: 3756 owner_login: Lightning-AI owner_html_url: https://github.com/Lightning-AI -- name: fastapi-admin - html_url: https://github.com/fastapi-admin/fastapi-admin - stars: 3632 - owner_login: fastapi-admin - owner_html_url: https://github.com/fastapi-admin -- name: datamodel-code-generator - html_url: https://github.com/koxudaxi/datamodel-code-generator - stars: 3609 - owner_login: koxudaxi - owner_html_url: https://github.com/koxudaxi - name: huma html_url: https://github.com/danielgtaylor/huma - stars: 3603 + stars: 3702 owner_login: danielgtaylor owner_html_url: https://github.com/danielgtaylor +- name: Yuxi-Know + html_url: https://github.com/xerrors/Yuxi-Know + stars: 3680 + owner_login: xerrors + owner_html_url: https://github.com/xerrors +- name: datamodel-code-generator + html_url: https://github.com/koxudaxi/datamodel-code-generator + stars: 3675 + owner_login: koxudaxi + owner_html_url: https://github.com/koxudaxi +- name: fastapi-admin + html_url: https://github.com/fastapi-admin/fastapi-admin + stars: 3659 + owner_login: fastapi-admin + owner_html_url: https://github.com/fastapi-admin - name: farfalle html_url: https://github.com/rashadphz/farfalle - stars: 3490 + stars: 3497 owner_login: rashadphz owner_html_url: https://github.com/rashadphz - name: tracecat html_url: https://github.com/TracecatHQ/tracecat - stars: 3379 + stars: 3421 owner_login: TracecatHQ owner_html_url: https://github.com/TracecatHQ - name: opyrator html_url: https://github.com/ml-tooling/opyrator - stars: 3135 + stars: 3136 owner_login: ml-tooling owner_html_url: https://github.com/ml-tooling - name: docarray html_url: https://github.com/docarray/docarray - stars: 3114 + stars: 3111 owner_login: docarray owner_html_url: https://github.com/docarray -- name: devpush - html_url: https://github.com/hunvreus/devpush - stars: 3097 - owner_login: hunvreus - owner_html_url: https://github.com/hunvreus - name: fastapi-realworld-example-app html_url: https://github.com/nsidnev/fastapi-realworld-example-app - stars: 3050 + stars: 3051 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: 2911 - owner_login: tiangolo - owner_html_url: https://github.com/tiangolo - name: mcp-context-forge html_url: https://github.com/IBM/mcp-context-forge - stars: 2899 + stars: 3034 owner_login: IBM owner_html_url: https://github.com/IBM -- name: best-of-web-python - html_url: https://github.com/ml-tooling/best-of-web-python - stars: 2648 - owner_login: ml-tooling - owner_html_url: https://github.com/ml-tooling +- name: uvicorn-gunicorn-fastapi-docker + html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker + stars: 2904 + owner_login: tiangolo + owner_html_url: https://github.com/tiangolo - name: FastAPI-template html_url: https://github.com/s3rius/FastAPI-template - stars: 2637 + stars: 2680 owner_login: s3rius owner_html_url: https://github.com/s3rius +- name: best-of-web-python + html_url: https://github.com/ml-tooling/best-of-web-python + stars: 2662 + owner_login: ml-tooling + owner_html_url: https://github.com/ml-tooling - name: YC-Killer html_url: https://github.com/sahibzada-allahyar/YC-Killer - stars: 2599 + stars: 2614 owner_login: sahibzada-allahyar owner_html_url: https://github.com/sahibzada-allahyar -- name: fastapi-react - html_url: https://github.com/Buuntu/fastapi-react - stars: 2569 - owner_login: Buuntu - owner_html_url: https://github.com/Buuntu -- name: Yuxi-Know - html_url: https://github.com/xerrors/Yuxi-Know - stars: 2563 - owner_login: xerrors - owner_html_url: https://github.com/xerrors - name: sqladmin html_url: https://github.com/aminalaee/sqladmin - stars: 2558 + stars: 2587 owner_login: aminalaee owner_html_url: https://github.com/aminalaee +- name: fastapi-react + html_url: https://github.com/Buuntu/fastapi-react + stars: 2566 + owner_login: Buuntu + owner_html_url: https://github.com/Buuntu - name: RasaGPT html_url: https://github.com/paulpierre/RasaGPT - stars: 2451 + stars: 2456 owner_login: paulpierre owner_html_url: https://github.com/paulpierre - name: supabase-py html_url: https://github.com/supabase/supabase-py - stars: 2344 + stars: 2394 owner_login: supabase owner_html_url: https://github.com/supabase - name: nextpy html_url: https://github.com/dot-agent/nextpy - stars: 2335 + stars: 2338 owner_login: dot-agent owner_html_url: https://github.com/dot-agent - name: fastapi-utils html_url: https://github.com/fastapiutils/fastapi-utils - stars: 2291 + stars: 2289 owner_login: fastapiutils owner_html_url: https://github.com/fastapiutils -- name: 30-Days-of-Python - html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python - stars: 2220 - owner_login: codingforentrepreneurs - owner_html_url: https://github.com/codingforentrepreneurs - name: langserve html_url: https://github.com/langchain-ai/langserve - stars: 2215 + stars: 2234 owner_login: langchain-ai owner_html_url: https://github.com/langchain-ai +- name: 30-Days-of-Python + html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python + stars: 2232 + owner_login: codingforentrepreneurs + owner_html_url: https://github.com/codingforentrepreneurs - name: solara html_url: https://github.com/widgetti/solara - stars: 2122 + stars: 2141 owner_login: widgetti owner_html_url: https://github.com/widgetti - name: mangum html_url: https://github.com/Kludex/mangum - stars: 2029 + stars: 2046 owner_login: Kludex owner_html_url: https://github.com/Kludex +- name: fastapi_best_architecture + html_url: https://github.com/fastapi-practices/fastapi_best_architecture + stars: 1963 + owner_login: fastapi-practices + owner_html_url: https://github.com/fastapi-practices +- name: NoteDiscovery + html_url: https://github.com/gamosoft/NoteDiscovery + stars: 1943 + owner_login: gamosoft + owner_html_url: https://github.com/gamosoft - name: agentkit html_url: https://github.com/BCG-X-Official/agentkit - stars: 1912 + stars: 1936 owner_login: BCG-X-Official owner_html_url: https://github.com/BCG-X-Official +- name: vue-fastapi-admin + html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin + stars: 1909 + owner_login: mizhexiaoxiao + owner_html_url: https://github.com/mizhexiaoxiao - name: manage-fastapi html_url: https://github.com/ycd/manage-fastapi - stars: 1885 + stars: 1887 owner_login: ycd owner_html_url: https://github.com/ycd - name: openapi-python-client html_url: https://github.com/openapi-generators/openapi-python-client - stars: 1862 + stars: 1879 owner_login: openapi-generators owner_html_url: https://github.com/openapi-generators -- name: piccolo - html_url: https://github.com/piccolo-orm/piccolo - stars: 1836 - owner_login: piccolo-orm - owner_html_url: https://github.com/piccolo-orm -- name: vue-fastapi-admin - html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin - stars: 1831 - owner_login: mizhexiaoxiao - owner_html_url: https://github.com/mizhexiaoxiao -- name: python-week-2022 - html_url: https://github.com/rochacbruno/python-week-2022 - stars: 1817 - owner_login: rochacbruno - owner_html_url: https://github.com/rochacbruno - name: slowapi html_url: https://github.com/laurentS/slowapi - stars: 1798 + stars: 1845 owner_login: laurentS owner_html_url: https://github.com/laurentS +- name: piccolo + html_url: https://github.com/piccolo-orm/piccolo + stars: 1843 + owner_login: piccolo-orm + owner_html_url: https://github.com/piccolo-orm +- name: python-week-2022 + html_url: https://github.com/rochacbruno/python-week-2022 + stars: 1813 + owner_login: rochacbruno + owner_html_url: https://github.com/rochacbruno - name: fastapi-cache html_url: https://github.com/long2ice/fastapi-cache - stars: 1789 + stars: 1805 owner_login: long2ice owner_html_url: https://github.com/long2ice - name: ormar html_url: https://github.com/collerek/ormar - stars: 1783 + stars: 1785 owner_login: collerek owner_html_url: https://github.com/collerek -- name: termpair - html_url: https://github.com/cs01/termpair - stars: 1716 - owner_login: cs01 - owner_html_url: https://github.com/cs01 -- name: FastAPI-boilerplate - html_url: https://github.com/benavlabs/FastAPI-boilerplate - stars: 1660 - owner_login: benavlabs - owner_html_url: https://github.com/benavlabs - name: fastapi-langgraph-agent-production-ready-template html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template - stars: 1638 + stars: 1780 owner_login: wassim249 owner_html_url: https://github.com/wassim249 -- name: langchain-serve - html_url: https://github.com/jina-ai/langchain-serve - stars: 1635 - owner_login: jina-ai - owner_html_url: https://github.com/jina-ai -- name: awesome-fastapi-projects - html_url: https://github.com/Kludex/awesome-fastapi-projects - stars: 1589 - owner_login: Kludex - owner_html_url: https://github.com/Kludex -- name: fastapi-pagination - html_url: https://github.com/uriyyo/fastapi-pagination - stars: 1585 - owner_login: uriyyo - owner_html_url: https://github.com/uriyyo -- name: coronavirus-tracker-api - html_url: https://github.com/ExpDev07/coronavirus-tracker-api - stars: 1574 - owner_login: ExpDev07 - owner_html_url: https://github.com/ExpDev07 +- name: FastAPI-boilerplate + html_url: https://github.com/benavlabs/FastAPI-boilerplate + stars: 1734 + owner_login: benavlabs + owner_html_url: https://github.com/benavlabs +- name: termpair + html_url: https://github.com/cs01/termpair + stars: 1724 + owner_login: cs01 + owner_html_url: https://github.com/cs01 - name: fastapi-crudrouter html_url: https://github.com/awtkns/fastapi-crudrouter - stars: 1559 + stars: 1671 owner_login: awtkns owner_html_url: https://github.com/awtkns +- name: langchain-serve + html_url: https://github.com/jina-ai/langchain-serve + stars: 1633 + owner_login: jina-ai + owner_html_url: https://github.com/jina-ai +- name: fastapi-pagination + html_url: https://github.com/uriyyo/fastapi-pagination + stars: 1588 + owner_login: uriyyo + owner_html_url: https://github.com/uriyyo +- name: awesome-fastapi-projects + html_url: https://github.com/Kludex/awesome-fastapi-projects + stars: 1583 + owner_login: Kludex + owner_html_url: https://github.com/Kludex +- name: coronavirus-tracker-api + html_url: https://github.com/ExpDev07/coronavirus-tracker-api + stars: 1571 + owner_login: ExpDev07 + owner_html_url: https://github.com/ExpDev07 - name: bracket html_url: https://github.com/evroon/bracket - stars: 1489 + stars: 1549 owner_login: evroon owner_html_url: https://github.com/evroon - name: fastapi-amis-admin html_url: https://github.com/amisadmin/fastapi-amis-admin - stars: 1475 + stars: 1491 owner_login: amisadmin owner_html_url: https://github.com/amisadmin - name: fastapi-boilerplate html_url: https://github.com/teamhide/fastapi-boilerplate - stars: 1436 + stars: 1452 owner_login: teamhide owner_html_url: https://github.com/teamhide -- name: awesome-python-resources - html_url: https://github.com/DjangoEx/awesome-python-resources - stars: 1426 - owner_login: DjangoEx - owner_html_url: https://github.com/DjangoEx - name: fastcrud html_url: https://github.com/benavlabs/fastcrud - stars: 1414 + stars: 1452 owner_login: benavlabs owner_html_url: https://github.com/benavlabs +- name: awesome-python-resources + html_url: https://github.com/DjangoEx/awesome-python-resources + stars: 1430 + owner_login: DjangoEx + owner_html_url: https://github.com/DjangoEx - name: prometheus-fastapi-instrumentator html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator - stars: 1388 + stars: 1399 owner_login: trallnag owner_html_url: https://github.com/trallnag -- name: fastapi_best_architecture - html_url: https://github.com/fastapi-practices/fastapi_best_architecture - stars: 1378 - owner_login: fastapi-practices - owner_html_url: https://github.com/fastapi-practices - name: fastapi-code-generator html_url: https://github.com/koxudaxi/fastapi-code-generator - stars: 1375 + stars: 1371 owner_login: koxudaxi owner_html_url: https://github.com/koxudaxi +- name: fastapi-tutorial + html_url: https://github.com/liaogx/fastapi-tutorial + stars: 1346 + owner_login: liaogx + owner_html_url: https://github.com/liaogx - name: budgetml html_url: https://github.com/ebhy/budgetml stars: 1345 owner_login: ebhy owner_html_url: https://github.com/ebhy -- name: fastapi-tutorial - html_url: https://github.com/liaogx/fastapi-tutorial - stars: 1327 - owner_login: liaogx - owner_html_url: https://github.com/liaogx -- name: fastapi-alembic-sqlmodel-async - html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async - stars: 1259 - owner_login: jonra1993 - owner_html_url: https://github.com/jonra1993 - name: fastapi-scaff html_url: https://github.com/atpuxiner/fastapi-scaff - stars: 1255 + stars: 1331 owner_login: atpuxiner owner_html_url: https://github.com/atpuxiner -- name: bedrock-chat - html_url: https://github.com/aws-samples/bedrock-chat - stars: 1254 - owner_login: aws-samples - owner_html_url: https://github.com/aws-samples - name: bolt-python html_url: https://github.com/slackapi/bolt-python - stars: 1253 + stars: 1266 owner_login: slackapi owner_html_url: https://github.com/slackapi +- name: bedrock-chat + html_url: https://github.com/aws-samples/bedrock-chat + stars: 1266 + owner_login: aws-samples + owner_html_url: https://github.com/aws-samples +- name: fastapi-alembic-sqlmodel-async + html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async + stars: 1260 + owner_login: jonra1993 + owner_html_url: https://github.com/jonra1993 - name: fastapi_production_template html_url: https://github.com/zhanymkanov/fastapi_production_template - stars: 1217 + stars: 1222 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov - name: langchain-extract html_url: https://github.com/langchain-ai/langchain-extract - stars: 1176 + stars: 1179 owner_login: langchain-ai owner_html_url: https://github.com/langchain-ai - name: restish html_url: https://github.com/rest-sh/restish - stars: 1140 + stars: 1152 owner_login: rest-sh owner_html_url: https://github.com/rest-sh - name: odmantic html_url: https://github.com/art049/odmantic - stars: 1138 + stars: 1143 owner_login: art049 owner_html_url: https://github.com/art049 - name: authx html_url: https://github.com/yezz123/authx - stars: 1119 + stars: 1128 owner_login: yezz123 owner_html_url: https://github.com/yezz123 -- name: NoteDiscovery - html_url: https://github.com/gamosoft/NoteDiscovery - stars: 1107 - owner_login: gamosoft - owner_html_url: https://github.com/gamosoft -- name: flock - html_url: https://github.com/Onelevenvy/flock - stars: 1055 - owner_login: Onelevenvy - owner_html_url: https://github.com/Onelevenvy -- name: fastapi-observability - html_url: https://github.com/blueswen/fastapi-observability - stars: 1038 - owner_login: blueswen - owner_html_url: https://github.com/blueswen +- name: SAG + html_url: https://github.com/Zleap-AI/SAG + stars: 1104 + owner_login: Zleap-AI + owner_html_url: https://github.com/Zleap-AI - name: aktools html_url: https://github.com/akfamily/aktools - stars: 1027 + stars: 1072 owner_login: akfamily owner_html_url: https://github.com/akfamily - name: RuoYi-Vue3-FastAPI html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI - stars: 1016 + stars: 1063 owner_login: insistence owner_html_url: https://github.com/insistence -- name: autollm - html_url: https://github.com/viddexa/autollm - stars: 1002 - owner_login: viddexa - owner_html_url: https://github.com/viddexa -- name: titiler - html_url: https://github.com/developmentseed/titiler - stars: 999 - owner_login: developmentseed - owner_html_url: https://github.com/developmentseed -- name: lanarky - html_url: https://github.com/ajndkr/lanarky - stars: 994 - owner_login: ajndkr - owner_html_url: https://github.com/ajndkr -- name: every-pdf - html_url: https://github.com/DDULDDUCK/every-pdf - stars: 985 - owner_login: DDULDDUCK - owner_html_url: https://github.com/DDULDDUCK +- name: flock + html_url: https://github.com/Onelevenvy/flock + stars: 1059 + owner_login: Onelevenvy + owner_html_url: https://github.com/Onelevenvy +- name: fastapi-observability + html_url: https://github.com/blueswen/fastapi-observability + stars: 1046 + owner_login: blueswen + owner_html_url: https://github.com/blueswen - name: enterprise-deep-research html_url: https://github.com/SalesforceAIResearch/enterprise-deep-research - stars: 973 + stars: 1019 owner_login: SalesforceAIResearch owner_html_url: https://github.com/SalesforceAIResearch -- name: fastapi-mail - html_url: https://github.com/sabuhish/fastapi-mail - stars: 964 - owner_login: sabuhish - owner_html_url: https://github.com/sabuhish +- name: titiler + html_url: https://github.com/developmentseed/titiler + stars: 1016 + owner_login: developmentseed + owner_html_url: https://github.com/developmentseed +- name: every-pdf + html_url: https://github.com/DDULDDUCK/every-pdf + stars: 1004 + owner_login: DDULDDUCK + owner_html_url: https://github.com/DDULDDUCK +- name: autollm + html_url: https://github.com/viddexa/autollm + stars: 1003 + owner_login: viddexa + owner_html_url: https://github.com/viddexa +- name: lanarky + html_url: https://github.com/ajndkr/lanarky + stars: 996 + owner_login: ajndkr + owner_html_url: https://github.com/ajndkr From 052d6e86c21dc83a7ad458a2b620145b91a1b4b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 1 Jan 2026 22:46:57 -0800 Subject: [PATCH 042/243] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Sponsors=20(#14626)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/en/data/github_sponsors.yml | 431 +++++++++++++++---------------- 1 file changed, 211 insertions(+), 220 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 24780603d..971687d8a 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -2,57 +2,51 @@ sponsors: - - login: renderinc avatarUrl: https://avatars.githubusercontent.com/u/36424661?v=4 url: https://github.com/renderinc - - login: andrew-propelauth - avatarUrl: https://avatars.githubusercontent.com/u/89474256?u=c98993dec8553c09d424ede67bbe86e5c35f48c9&v=4 - url: https://github.com/andrew-propelauth - - login: blockbee-io - avatarUrl: https://avatars.githubusercontent.com/u/115143449?u=1b8620c2d6567c4df2111a371b85a51f448f9b85&v=4 - url: https://github.com/blockbee-io - - login: zuplo - avatarUrl: https://avatars.githubusercontent.com/u/85497839?v=4 - url: https://github.com/zuplo - - login: coderabbitai - avatarUrl: https://avatars.githubusercontent.com/u/132028505?v=4 - url: https://github.com/coderabbitai - - login: greptileai - avatarUrl: https://avatars.githubusercontent.com/u/140149887?v=4 - url: https://github.com/greptileai - login: subtotal avatarUrl: https://avatars.githubusercontent.com/u/176449348?v=4 url: https://github.com/subtotal + - login: greptileai + avatarUrl: https://avatars.githubusercontent.com/u/140149887?v=4 + url: https://github.com/greptileai + - login: coderabbitai + avatarUrl: https://avatars.githubusercontent.com/u/132028505?v=4 + url: https://github.com/coderabbitai + - login: zuplo + avatarUrl: https://avatars.githubusercontent.com/u/85497839?v=4 + url: https://github.com/zuplo + - login: blockbee-io + avatarUrl: https://avatars.githubusercontent.com/u/115143449?u=1b8620c2d6567c4df2111a371b85a51f448f9b85&v=4 + url: https://github.com/blockbee-io + - login: andrew-propelauth + avatarUrl: https://avatars.githubusercontent.com/u/89474256?u=c98993dec8553c09d424ede67bbe86e5c35f48c9&v=4 + url: https://github.com/andrew-propelauth - login: railwayapp avatarUrl: https://avatars.githubusercontent.com/u/66716858?v=4 url: https://github.com/railwayapp -- - login: dribia - avatarUrl: https://avatars.githubusercontent.com/u/41189616?v=4 - url: https://github.com/dribia - - login: svix - avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4 - url: https://github.com/svix +- - login: speakeasy-api + avatarUrl: https://avatars.githubusercontent.com/u/91446104?v=4 + url: https://github.com/speakeasy-api - login: stainless-api avatarUrl: https://avatars.githubusercontent.com/u/88061651?v=4 url: https://github.com/stainless-api - - login: speakeasy-api - avatarUrl: https://avatars.githubusercontent.com/u/91446104?v=4 - url: https://github.com/speakeasy-api - - login: databento - avatarUrl: https://avatars.githubusercontent.com/u/64141749?v=4 - url: https://github.com/databento + - login: svix + avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4 + url: https://github.com/svix - login: permitio avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4 url: https://github.com/permitio -- - login: Ponte-Energy-Partners - avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4 - url: https://github.com/Ponte-Energy-Partners - - login: LambdaTest-Inc + - login: databento + avatarUrl: https://avatars.githubusercontent.com/u/64141749?v=4 + url: https://github.com/databento +- - login: LambdaTest-Inc avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4 url: https://github.com/LambdaTest-Inc + - login: Ponte-Energy-Partners + avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4 + url: https://github.com/Ponte-Energy-Partners - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP - - login: requestly - avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4 - url: https://github.com/requestly - login: acsone avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 url: https://github.com/acsone @@ -68,9 +62,6 @@ sponsors: - login: Doist avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4 url: https://github.com/Doist - - login: bholagabbar - avatarUrl: https://avatars.githubusercontent.com/u/11693595?v=4 - url: https://github.com/bholagabbar - - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries @@ -86,6 +77,9 @@ sponsors: - login: ChargeStorm avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4 url: https://github.com/ChargeStorm + - login: ibrahimpelumi6142 + avatarUrl: https://avatars.githubusercontent.com/u/113442282?v=4 + url: https://github.com/ibrahimpelumi6142 - login: nilslindemann avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann @@ -116,124 +110,127 @@ sponsors: - login: Leay15 avatarUrl: https://avatars.githubusercontent.com/u/32212558?u=c4aa9c1737e515959382a5515381757b1fd86c53&v=4 url: https://github.com/Leay15 - - login: Karine-Bauch - avatarUrl: https://avatars.githubusercontent.com/u/90465103?u=7feb1018abb1a5631cfd9a91fea723d1ceb5f49b&v=4 - url: https://github.com/Karine-Bauch - login: jugeeem avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4 url: https://github.com/jugeeem - - login: patsatsia - avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 - url: https://github.com/patsatsia - - login: anthonycepeda - avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=60bdf46240cff8fca482ff0fc07d963fd5e1a27c&v=4 - url: https://github.com/anthonycepeda - - login: patricioperezv - avatarUrl: https://avatars.githubusercontent.com/u/73832292?u=5f471f156e19ee7920e62ae0f4a47b95580e61cf&v=4 - url: https://github.com/patricioperezv - - login: chickenandstats - avatarUrl: https://avatars.githubusercontent.com/u/79477966?u=ae2b894aa954070db1d7830dab99b49eba4e4567&v=4 - url: https://github.com/chickenandstats + - login: Karine-Bauch + avatarUrl: https://avatars.githubusercontent.com/u/90465103?u=7feb1018abb1a5631cfd9a91fea723d1ceb5f49b&v=4 + url: https://github.com/Karine-Bauch - login: kaoru0310 avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 url: https://github.com/kaoru0310 - - login: jstanden - avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 - url: https://github.com/jstanden - - login: knallgelb - avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 - url: https://github.com/knallgelb - - login: dblackrun - avatarUrl: https://avatars.githubusercontent.com/u/3528486?v=4 - url: https://github.com/dblackrun - - login: zsinx6 - avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 - url: https://github.com/zsinx6 - - login: kennywakeland - avatarUrl: https://avatars.githubusercontent.com/u/3631417?u=7c8f743f1ae325dfadea7c62bbf1abd6a824fc55&v=4 - url: https://github.com/kennywakeland - - login: aacayaco - avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 - url: https://github.com/aacayaco - - login: anomaly - avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 - url: https://github.com/anomaly - - login: mj0331 - avatarUrl: https://avatars.githubusercontent.com/u/3890353?u=1c627ac1a024515b4871de5c3ebbfaa1a57f65d4&v=4 - url: https://github.com/mj0331 - - 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=679ff84cb7b988c5795a5fa583857f574a055763&v=4 - url: https://github.com/Ryandaydev - - login: jaredtrog - avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 - url: https://github.com/jaredtrog + - login: chickenandstats + avatarUrl: https://avatars.githubusercontent.com/u/79477966?u=ae2b894aa954070db1d7830dab99b49eba4e4567&v=4 + url: https://github.com/chickenandstats + - login: patricioperezv + avatarUrl: https://avatars.githubusercontent.com/u/73832292?u=5f471f156e19ee7920e62ae0f4a47b95580e61cf&v=4 + url: https://github.com/patricioperezv + - login: anthonycepeda + avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=60bdf46240cff8fca482ff0fc07d963fd5e1a27c&v=4 + url: https://github.com/anthonycepeda + - login: AalbatrossGuy + avatarUrl: https://avatars.githubusercontent.com/u/68378354?u=0bdeea9356d24f638244131f6d8d1e2d2f3601ca&v=4 + url: https://github.com/AalbatrossGuy + - login: patsatsia + avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 + url: https://github.com/patsatsia - login: oliverxchen avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 url: https://github.com/oliverxchen - - login: paulcwatts - avatarUrl: https://avatars.githubusercontent.com/u/150269?u=1819e145d573b44f0ad74b87206d21cd60331d4e&v=4 - url: https://github.com/paulcwatts - - login: robintw - avatarUrl: https://avatars.githubusercontent.com/u/296686?v=4 - url: https://github.com/robintw - - login: pamelafox - avatarUrl: https://avatars.githubusercontent.com/u/297042?v=4 - url: https://github.com/pamelafox - - login: wshayes - avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 - url: https://github.com/wshayes - - login: koxudaxi - avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4 - url: https://github.com/koxudaxi - - login: falkben - avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 - url: https://github.com/falkben - - login: mintuhouse - avatarUrl: https://avatars.githubusercontent.com/u/769950?u=ecfbd79a97d33177e0d093ddb088283cf7fe8444&v=4 - url: https://github.com/mintuhouse + - login: jaredtrog + avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 + url: https://github.com/jaredtrog + - login: Ryandaydev + avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=679ff84cb7b988c5795a5fa583857f574a055763&v=4 + url: https://github.com/Ryandaydev + - login: gorhack + avatarUrl: https://avatars.githubusercontent.com/u/4141690?u=ec119ebc4bdf00a7bc84657a71aa17834f4f27f3&v=4 + url: https://github.com/gorhack + - login: mj0331 + avatarUrl: https://avatars.githubusercontent.com/u/3890353?u=1c627ac1a024515b4871de5c3ebbfaa1a57f65d4&v=4 + url: https://github.com/mj0331 + - login: anomaly + avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 + url: https://github.com/anomaly + - login: aacayaco + avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 + url: https://github.com/aacayaco + - login: kennywakeland + avatarUrl: https://avatars.githubusercontent.com/u/3631417?u=7c8f743f1ae325dfadea7c62bbf1abd6a824fc55&v=4 + url: https://github.com/kennywakeland + - login: zsinx6 + avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 + url: https://github.com/zsinx6 + - login: dblackrun + avatarUrl: https://avatars.githubusercontent.com/u/3528486?v=4 + url: https://github.com/dblackrun + - login: knallgelb + avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 + url: https://github.com/knallgelb - login: dodo5522 avatarUrl: https://avatars.githubusercontent.com/u/1362607?u=9bf1e0e520cccc547c046610c468ce6115bbcf9f&v=4 url: https://github.com/dodo5522 - - login: wdwinslow - avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=371272f2c69e680e0559a7b0a57385e83a5dc728&v=4 - url: https://github.com/wdwinslow - - login: jsoques - avatarUrl: https://avatars.githubusercontent.com/u/12414216?u=620921d94196546cc8b9eae2cc4cbc3f95bab42f&v=4 - url: https://github.com/jsoques - - 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: mjohnsey - avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 - url: https://github.com/mjohnsey - - login: ashi-agrawal - avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4 - url: https://github.com/ashi-agrawal + - login: mintuhouse + avatarUrl: https://avatars.githubusercontent.com/u/769950?u=ecfbd79a97d33177e0d093ddb088283cf7fe8444&v=4 + url: https://github.com/mintuhouse + - login: falkben + avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 + url: https://github.com/falkben + - login: koxudaxi + avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4 + url: https://github.com/koxudaxi + - login: wshayes + avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 + url: https://github.com/wshayes + - login: pamelafox + avatarUrl: https://avatars.githubusercontent.com/u/297042?v=4 + url: https://github.com/pamelafox + - login: robintw + avatarUrl: https://avatars.githubusercontent.com/u/296686?v=4 + url: https://github.com/robintw + - login: jstanden + avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 + url: https://github.com/jstanden - login: RaamEEIL avatarUrl: https://avatars.githubusercontent.com/u/20320552?v=4 url: https://github.com/RaamEEIL - - login: ternaus - avatarUrl: https://avatars.githubusercontent.com/u/5481618?u=513a26b02a39e7a28d587cd37c6cc877ea368e6e&v=4 - url: https://github.com/ternaus - - login: eseglem - avatarUrl: https://avatars.githubusercontent.com/u/5920492?u=208d419cf667b8ac594c82a8db01932c7e50d057&v=4 - url: https://github.com/eseglem - - login: FernandoCelmer - avatarUrl: https://avatars.githubusercontent.com/u/6262214?u=58ba6d5888fa7f355934e52db19f950e20b38162&v=4 - url: https://github.com/FernandoCelmer - - login: Rehket - avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 - url: https://github.com/Rehket + - login: ashi-agrawal + avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4 + url: https://github.com/ashi-agrawal + - login: mjohnsey + avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 + url: https://github.com/mjohnsey + - login: khadrawy + avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4 + url: https://github.com/khadrawy + - login: dannywade + avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 + url: https://github.com/dannywade + - login: jsoques + avatarUrl: https://avatars.githubusercontent.com/u/12414216?u=620921d94196546cc8b9eae2cc4cbc3f95bab42f&v=4 + url: https://github.com/jsoques + - login: wdwinslow + avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=371272f2c69e680e0559a7b0a57385e83a5dc728&v=4 + url: https://github.com/wdwinslow - login: hiancdtrsnm avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4 url: https://github.com/hiancdtrsnm -- - login: manoelpqueiroz + - login: Rehket + avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 + url: https://github.com/Rehket + - login: FernandoCelmer + avatarUrl: https://avatars.githubusercontent.com/u/6262214?u=58ba6d5888fa7f355934e52db19f950e20b38162&v=4 + url: https://github.com/FernandoCelmer + - login: eseglem + avatarUrl: https://avatars.githubusercontent.com/u/5920492?u=208d419cf667b8ac594c82a8db01932c7e50d057&v=4 + url: https://github.com/eseglem + - login: ternaus + avatarUrl: https://avatars.githubusercontent.com/u/5481618?u=513a26b02a39e7a28d587cd37c6cc877ea368e6e&v=4 + url: https://github.com/ternaus +- - login: Artur-Galstyan + avatarUrl: https://avatars.githubusercontent.com/u/63471891?u=e8691f386037e51a737cc0ba866cd8c89e5cf109&v=4 + url: https://github.com/Artur-Galstyan + - login: manoelpqueiroz avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4 url: https://github.com/manoelpqueiroz - - login: pawamoy @@ -254,9 +251,12 @@ sponsors: - login: hgalytoby avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=6cc9028f3db63f8f60ad21c17b1ce4b88c4e2e60&v=4 url: https://github.com/hgalytoby - - login: nisutec - avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 - url: https://github.com/nisutec + - login: johnl28 + avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 + url: https://github.com/johnl28 + - login: danielunderwood + avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 + url: https://github.com/danielunderwood - login: hoenie-ams avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 url: https://github.com/hoenie-ams @@ -267,93 +267,87 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 - login: bnkc - avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=db5e6f4f87836cad26c2aa90ce390ce49041c5a9&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=4771ac4e64066f0847d40e5b29910adabd9b2372&v=4 url: https://github.com/bnkc - login: petercool avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4 url: https://github.com/petercool - - login: johnl28 - avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 - url: https://github.com/johnl28 - - login: PunRabbit - avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4 - url: https://github.com/PunRabbit - login: PelicanQ avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 url: https://github.com/PelicanQ - - login: WillHogan - avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 - url: https://github.com/WillHogan + - login: PunRabbit + avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4 + url: https://github.com/PunRabbit - login: my3 avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 url: https://github.com/my3 - - login: danielunderwood - avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 - url: https://github.com/danielunderwood - - login: ddanier - avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 - url: https://github.com/ddanier - - login: bryanculbertson - avatarUrl: https://avatars.githubusercontent.com/u/144028?u=defda4f90e93429221cc667500944abde60ebe4a&v=4 - url: https://github.com/bryanculbertson - - login: slafs - avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 - url: https://github.com/slafs - - login: ceb10n - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n - - login: tochikuji - avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 - url: https://github.com/tochikuji + - login: WillHogan + avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 + url: https://github.com/WillHogan - login: miguelgr avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 url: https://github.com/miguelgr - - login: xncbf - avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 - url: https://github.com/xncbf - - login: DMantis - avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4 - url: https://github.com/DMantis - - login: hard-coders - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 - url: https://github.com/hard-coders - - login: mntolia - avatarUrl: https://avatars.githubusercontent.com/u/10390224?v=4 - url: https://github.com/mntolia - - login: Zuzah - avatarUrl: https://avatars.githubusercontent.com/u/10934846?u=1ef43e075ddc87bd1178372bf4d95ee6175cae27&v=4 - url: https://github.com/Zuzah - - login: TheR1D - avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4 - url: https://github.com/TheR1D + - login: tochikuji + avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 + url: https://github.com/tochikuji + - login: ceb10n + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n + - login: slafs + avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 + url: https://github.com/slafs + - login: bryanculbertson + avatarUrl: https://avatars.githubusercontent.com/u/144028?u=defda4f90e93429221cc667500944abde60ebe4a&v=4 + url: https://github.com/bryanculbertson + - login: ddanier + avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 + url: https://github.com/ddanier + - login: nisutec + avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 + url: https://github.com/nisutec - login: joshuatz avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 url: https://github.com/joshuatz - - login: rangulvers - avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 - url: https://github.com/rangulvers - - login: sdevkota - avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 - url: https://github.com/sdevkota - - login: Baghdady92 - avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 - url: https://github.com/Baghdady92 - - login: KentShikama - avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4 - url: https://github.com/KentShikama - - login: katnoria - avatarUrl: https://avatars.githubusercontent.com/u/7674948?u=09767eb13e07e09496c5fee4e5ce21d9eac34a56&v=4 - url: https://github.com/katnoria - - login: harsh183 - avatarUrl: https://avatars.githubusercontent.com/u/7780198?v=4 - url: https://github.com/harsh183 + - login: TheR1D + avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4 + url: https://github.com/TheR1D + - login: Zuzah + avatarUrl: https://avatars.githubusercontent.com/u/10934846?u=1ef43e075ddc87bd1178372bf4d95ee6175cae27&v=4 + url: https://github.com/Zuzah + - login: mntolia + avatarUrl: https://avatars.githubusercontent.com/u/10390224?v=4 + url: https://github.com/mntolia + - login: hard-coders + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=78d12d1acdf853c817700145e73de7fd9e5d068b&v=4 + url: https://github.com/hard-coders + - login: DMantis + avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4 + url: https://github.com/DMantis + - login: xncbf + avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 + url: https://github.com/xncbf - login: moonape1226 avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 url: https://github.com/moonape1226 -- - login: andrecorumba - avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4 - url: https://github.com/andrecorumba - - login: KOZ39 + - login: harsh183 + avatarUrl: https://avatars.githubusercontent.com/u/7780198?v=4 + url: https://github.com/harsh183 + - login: katnoria + avatarUrl: https://avatars.githubusercontent.com/u/7674948?u=09767eb13e07e09496c5fee4e5ce21d9eac34a56&v=4 + url: https://github.com/katnoria + - login: KentShikama + avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4 + url: https://github.com/KentShikama + - login: Baghdady92 + avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 + url: https://github.com/Baghdady92 + - login: sdevkota + avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 + url: https://github.com/sdevkota + - login: rangulvers + avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 + url: https://github.com/rangulvers +- - login: KOZ39 avatarUrl: https://avatars.githubusercontent.com/u/38822500?u=9dfc0a697df1c9628f08e20dc3fb17b1afc4e5a7&v=4 url: https://github.com/KOZ39 - login: rwxd @@ -365,27 +359,24 @@ sponsors: - login: Olegt0rr avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4 url: https://github.com/Olegt0rr - - login: dinoz0rg - avatarUrl: https://avatars.githubusercontent.com/u/32940067?u=739cda1eb123a2dd5e1db45c361396f239e23f8b&v=4 - url: https://github.com/dinoz0rg - login: larsyngvelundin avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4 url: https://github.com/larsyngvelundin - - login: hippoley - avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4 - url: https://github.com/hippoley - - login: 4anklee - avatarUrl: https://avatars.githubusercontent.com/u/144109238?u=a79c0d581b2a3d8f3897e7ef4c012640a6c1eb3a&v=4 - url: https://github.com/4anklee + - login: andrecorumba + avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4 + url: https://github.com/andrecorumba - login: CoderDeltaLAN avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4 url: https://github.com/CoderDeltaLAN - - login: onestn - avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4 - url: https://github.com/onestn + - login: hippoley + avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4 + url: https://github.com/hippoley - login: nayasinghania avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4 url: https://github.com/nayasinghania + - login: onestn + avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4 + url: https://github.com/onestn - login: Toothwitch avatarUrl: https://avatars.githubusercontent.com/u/1710406?u=5eebb23b46cd26e48643b9e5179536cad491c17a&v=4 url: https://github.com/Toothwitch From 862c3f4f94bcffdedb62589632fe650adce92f6a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Jan 2026 06:46:59 +0000 Subject: [PATCH 043/243] =?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 34ed433ce..520554e25 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,7 @@ hide: ### Internal +* 👥 Update FastAPI GitHub topic repositories. PR [#14630](https://github.com/fastapi/fastapi/pull/14630) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Contributors and Translators. PR [#14625](https://github.com/fastapi/fastapi/pull/14625) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translation prompts. PR [#14619](https://github.com/fastapi/fastapi/pull/14619) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update LLM translation script to guide reviewers to change the prompt. PR [#14614](https://github.com/fastapi/fastapi/pull/14614) by [@tiangolo](https://github.com/tiangolo). From f2687dc1bb01f4cb62eee90e656b61c38c2aaf6a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Jan 2026 06:47:25 +0000 Subject: [PATCH 044/243] =?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 520554e25..9bd794f03 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,7 @@ hide: ### Internal +* 👥 Update FastAPI People - Sponsors. PR [#14626](https://github.com/fastapi/fastapi/pull/14626) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI GitHub topic repositories. PR [#14630](https://github.com/fastapi/fastapi/pull/14630) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Contributors and Translators. PR [#14625](https://github.com/fastapi/fastapi/pull/14625) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translation prompts. PR [#14619](https://github.com/fastapi/fastapi/pull/14619) by [@tiangolo](https://github.com/tiangolo). From b1db1395b6847b84690966182356f99a5bb595ff Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:35:09 +0300 Subject: [PATCH 045/243] =?UTF-8?q?=F0=9F=93=9D=20Specify=20language=20cod?= =?UTF-8?q?e=20for=20code=20block=20(#14656)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/bigger-applications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index 3cc9d7ecf..f6cee8036 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -56,7 +56,7 @@ from app.routers import items The same file structure with comments: -``` +```bash . ├── app # "app" is a Python package │   ├── __init__.py # this file makes "app" a "Python package" From 18762e38a9000b91564d938f7b38f8fc86d4b8bb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 21:35:32 +0000 Subject: [PATCH 046/243] =?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 9bd794f03..f9b694e2e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Specify language code for code block. PR [#14656](https://github.com/fastapi/fastapi/pull/14656) by [@YuriiMotov](https://github.com/YuriiMotov). + ### Translations * 🔧 Add LLM prompt file for Turkish, generated from the existing translations. PR [#14547](https://github.com/fastapi/fastapi/pull/14547) by [@tiangolo](https://github.com/tiangolo). From d1c67c0055af1c0d187a55cf8ba3f020a73303c1 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:48:08 +0300 Subject: [PATCH 047/243] =?UTF-8?q?=F0=9F=94=A8=20Add=20LLM=20translations?= =?UTF-8?q?=20tool=20fixer=20(#14652)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] Co-authored-by: Sebastián Ramírez --- scripts/doc_parsing_utils.py | 729 ++++++++++++++++++ .../tests/test_translation_fixer/conftest.py | 32 + .../test_code_blocks/data/en_doc.md | 44 ++ .../data/translated_doc_lines_number_gt.md | 45 ++ .../data/translated_doc_lines_number_lt.md | 45 ++ .../translated_doc_mermaid_not_translated.md | 44 ++ .../data/translated_doc_mermaid_translated.md | 44 ++ .../data/translated_doc_number_gt.md | 50 ++ .../data/translated_doc_number_lt.md | 41 + .../data/translated_doc_wrong_lang_code.md | 46 ++ .../data/translated_doc_wrong_lang_code_2.md | 46 ++ .../test_code_blocks_lines_number_mismatch.py | 58 ++ .../test_code_blocks_mermaid.py | 59 ++ .../test_code_blocks_number_mismatch.py | 56 ++ .../test_code_blocks_wrong_lang_code.py | 58 ++ .../test_code_includes/data/en_doc.md | 13 + .../data/translated_doc_number_gt.md | 15 + .../data/translated_doc_number_lt.md | 13 + .../test_number_mismatch.py | 56 ++ .../test_complex_doc/data/en_doc.md | 244 ++++++ .../test_complex_doc/data/translated_doc.md | 240 ++++++ .../data/translated_doc_expected.md | 240 ++++++ .../test_complex_doc/test_compex_doc.py | 30 + .../test_header_permalinks/data/en_doc.md | 19 + .../data/translated_doc_level_mismatch_1.md | 19 + .../data/translated_doc_level_mismatch_2.md | 19 + .../data/translated_doc_number_gt.md | 19 + .../data/translated_doc_number_lt.md | 19 + .../test_header_level_mismatch.py | 60 ++ .../test_header_number_mismatch.py | 56 ++ .../test_html_links/data/en_doc.md | 19 + .../data/translated_doc_number_gt.md | 21 + .../data/translated_doc_number_lt.md | 19 + .../test_html_links_number_mismatch.py | 54 ++ .../test_markdown_links/data/en_doc.md | 19 + .../data/translated_doc.md | 19 + .../data/translated_doc_number_gt.md | 21 + .../data/translated_doc_number_lt.md | 19 + .../test_mkd_links_number_mismatch.py | 56 ++ scripts/translation_fixer.py | 132 ++++ 40 files changed, 2838 insertions(+) create mode 100644 scripts/doc_parsing_utils.py create mode 100644 scripts/tests/test_translation_fixer/conftest.py create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/data/en_doc.md create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_gt.md create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_lt.md create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_not_translated.md create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_translated.md create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_gt.md create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_lt.md create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code.md create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code_2.md create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_lines_number_mismatch.py create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_mermaid.py create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_number_mismatch.py create mode 100644 scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_wrong_lang_code.py create mode 100644 scripts/tests/test_translation_fixer/test_code_includes/data/en_doc.md create mode 100644 scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_gt.md create mode 100644 scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_lt.md create mode 100644 scripts/tests/test_translation_fixer/test_code_includes/test_number_mismatch.py create mode 100644 scripts/tests/test_translation_fixer/test_complex_doc/data/en_doc.md create mode 100644 scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc.md create mode 100644 scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc_expected.md create mode 100644 scripts/tests/test_translation_fixer/test_complex_doc/test_compex_doc.py create mode 100644 scripts/tests/test_translation_fixer/test_header_permalinks/data/en_doc.md create mode 100644 scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_1.md create mode 100644 scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_2.md create mode 100644 scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_gt.md create mode 100644 scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_lt.md create mode 100644 scripts/tests/test_translation_fixer/test_header_permalinks/test_header_level_mismatch.py create mode 100644 scripts/tests/test_translation_fixer/test_header_permalinks/test_header_number_mismatch.py create mode 100644 scripts/tests/test_translation_fixer/test_html_links/data/en_doc.md create mode 100644 scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_gt.md create mode 100644 scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_lt.md create mode 100644 scripts/tests/test_translation_fixer/test_html_links/test_html_links_number_mismatch.py create mode 100644 scripts/tests/test_translation_fixer/test_markdown_links/data/en_doc.md create mode 100644 scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc.md create mode 100644 scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_gt.md create mode 100644 scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_lt.md create mode 100644 scripts/tests/test_translation_fixer/test_markdown_links/test_mkd_links_number_mismatch.py create mode 100644 scripts/translation_fixer.py diff --git a/scripts/doc_parsing_utils.py b/scripts/doc_parsing_utils.py new file mode 100644 index 000000000..857d808aa --- /dev/null +++ b/scripts/doc_parsing_utils.py @@ -0,0 +1,729 @@ +import re +from typing import TypedDict + +CODE_INCLUDE_RE = re.compile(r"^\{\*\s*(\S+)\s*(.*)\*\}$") +CODE_INCLUDE_PLACEHOLDER = "" + +HEADER_WITH_PERMALINK_RE = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})?\s*$") +HEADER_LINE_RE = re.compile(r"^(#{1,6}) (.+?)(?:\s*\{\s*(#.*)\s*\})?\s*$") + +TIANGOLO_COM = "https://fastapi.tiangolo.com" +ASSETS_URL_PREFIXES = ("/img/", "/css/", "/js/") + +MARKDOWN_LINK_RE = re.compile( + r"(?.*?)\]" # link text (non-greedy) + r"\(" + r"(?P[^)\s]+)" # url (no spaces and `)`) + r'(?:\s+["\'](?P.*?)["\'])?' # optional title in "" or '' + r"\)" + r"(?:\s*\{(?P<attrs>[^}]*)\})?" # optional attributes in {} +) + +HTML_LINK_RE = re.compile(r"<a\s+[^>]*>.*?</a>") +HTML_LINK_TEXT_RE = re.compile(r"<a\b([^>]*)>(.*?)</a>") +HTML_LINK_OPEN_TAG_RE = re.compile(r"<a\b([^>]*)>") +HTML_ATTR_RE = re.compile(r'(\w+)\s*=\s*([\'"])(.*?)\2') + +CODE_BLOCK_LANG_RE = re.compile(r"^`{3,4}([\w-]*)", re.MULTILINE) + +SLASHES_COMMENT_RE = re.compile( + r"^(?P<code>.*?)(?P<comment>(?:(?<= )// .*)|(?:^// .*))?$" +) + +HASH_COMMENT_RE = re.compile(r"^(?P<code>.*?)(?P<comment>(?:(?<= )# .*)|(?:^# .*))?$") + + +class CodeIncludeInfo(TypedDict): + line_no: int + line: str + + +class HeaderPermalinkInfo(TypedDict): + line_no: int + hashes: str + title: str + permalink: str + + +class MarkdownLinkInfo(TypedDict): + line_no: int + url: str + text: str + title: str | None + attributes: str | None + full_match: str + + +class HTMLLinkAttribute(TypedDict): + name: str + quote: str + value: str + + +class HtmlLinkInfo(TypedDict): + line_no: int + full_tag: str + attributes: list[HTMLLinkAttribute] + text: str + + +class MultilineCodeBlockInfo(TypedDict): + lang: str + start_line_no: int + content: list[str] + + +# Code includes +# -------------------------------------------------------------------------------------- + + +def extract_code_includes(lines: list[str]) -> list[CodeIncludeInfo]: + """ + Extract lines that contain code includes. + + Return list of CodeIncludeInfo, where each dict contains: + - `line_no` - line number (1-based) + - `line` - text of the line + """ + + includes: list[CodeIncludeInfo] = [] + for line_no, line in enumerate(lines, start=1): + if CODE_INCLUDE_RE.match(line): + includes.append(CodeIncludeInfo(line_no=line_no, line=line)) + return includes + + +def replace_code_includes_with_placeholders(text: list[str]) -> list[str]: + """ + Replace code includes with placeholders. + """ + + modified_text = text.copy() + includes = extract_code_includes(text) + for include in includes: + modified_text[include["line_no"] - 1] = CODE_INCLUDE_PLACEHOLDER + return modified_text + + +def replace_placeholders_with_code_includes( + text: list[str], original_includes: list[CodeIncludeInfo] +) -> list[str]: + """ + Replace code includes placeholders with actual code includes from the original (English) document. + Fail if the number of placeholders does not match the number of original includes. + """ + + code_include_lines = [ + line_no + for line_no, line in enumerate(text) + if line.strip() == CODE_INCLUDE_PLACEHOLDER + ] + + if len(code_include_lines) != len(original_includes): + raise ValueError( + "Number of code include placeholders does not match the number of code includes " + "in the original document " + f"({len(code_include_lines)} vs {len(original_includes)})" + ) + + modified_text = text.copy() + for i, line_no in enumerate(code_include_lines): + modified_text[line_no] = original_includes[i]["line"] + + return modified_text + + +# Header permalinks +# -------------------------------------------------------------------------------------- + + +def extract_header_permalinks(lines: list[str]) -> list[HeaderPermalinkInfo]: + """ + Extract list of header permalinks from the given lines. + + Return list of HeaderPermalinkInfo, where each dict contains: + - `line_no` - line number (1-based) + - `hashes` - string of hashes representing header level (e.g., "###") + - `permalink` - permalink string (e.g., "{#permalink}") + """ + + headers: list[HeaderPermalinkInfo] = [] + in_code_block3 = False + in_code_block4 = False + + for line_no, line in enumerate(lines, start=1): + if not (in_code_block3 or in_code_block4): + if line.startswith("```"): + count = len(line) - len(line.lstrip("`")) + if count == 3: + in_code_block3 = True + continue + elif count >= 4: + in_code_block4 = True + continue + + header_match = HEADER_WITH_PERMALINK_RE.match(line) + if header_match: + hashes, title, permalink = header_match.groups() + headers.append( + HeaderPermalinkInfo( + hashes=hashes, line_no=line_no, permalink=permalink, title=title + ) + ) + + elif in_code_block3: + if line.startswith("```"): + count = len(line) - len(line.lstrip("`")) + if count == 3: + in_code_block3 = False + continue + + elif in_code_block4: + if line.startswith("````"): + count = len(line) - len(line.lstrip("`")) + if count >= 4: + in_code_block4 = False + continue + + return headers + + +def remove_header_permalinks(lines: list[str]) -> list[str]: + """ + Remove permalinks from headers in the given lines. + """ + + modified_lines: list[str] = [] + for line in lines: + header_match = HEADER_WITH_PERMALINK_RE.match(line) + if header_match: + hashes, title, _permalink = header_match.groups() + modified_line = f"{hashes} {title}" + modified_lines.append(modified_line) + else: + modified_lines.append(line) + return modified_lines + + +def replace_header_permalinks( + text: list[str], + header_permalinks: list[HeaderPermalinkInfo], + original_header_permalinks: list[HeaderPermalinkInfo], +) -> list[str]: + """ + Replace permalinks in the given text with the permalinks from the original document. + + Fail if the number or level of headers does not match the original. + """ + + modified_text: list[str] = text.copy() + + if len(header_permalinks) != len(original_header_permalinks): + raise ValueError( + "Number of headers with permalinks does not match the number in the " + "original document " + f"({len(header_permalinks)} vs {len(original_header_permalinks)})" + ) + + for header_no in range(len(header_permalinks)): + header_info = header_permalinks[header_no] + original_header_info = original_header_permalinks[header_no] + + if header_info["hashes"] != original_header_info["hashes"]: + raise ValueError( + "Header levels do not match between document and original document" + f" (found {header_info['hashes']}, expected {original_header_info['hashes']})" + f" for header №{header_no + 1} in line {header_info['line_no']}" + ) + line_no = header_info["line_no"] - 1 + hashes = header_info["hashes"] + title = header_info["title"] + permalink = original_header_info["permalink"] + modified_text[line_no] = f"{hashes} {title}{permalink}" + + return modified_text + + +# Markdown links +# -------------------------------------------------------------------------------------- + + +def extract_markdown_links(lines: list[str]) -> list[MarkdownLinkInfo]: + """ + Extract all markdown links from the given lines. + + Return list of MarkdownLinkInfo, where each dict contains: + - `line_no` - line number (1-based) + - `url` - link URL + - `text` - link text + - `title` - link title (if any) + """ + + links: list[MarkdownLinkInfo] = [] + for line_no, line in enumerate(lines, start=1): + for m in MARKDOWN_LINK_RE.finditer(line): + links.append( + MarkdownLinkInfo( + line_no=line_no, + url=m.group("url"), + text=m.group("text"), + title=m.group("title"), + attributes=m.group("attrs"), + full_match=m.group(0), + ) + ) + return links + + +def _add_lang_code_to_url(url: str, lang_code: str) -> str: + if url.startswith(TIANGOLO_COM): + rel_url = url[len(TIANGOLO_COM) :] + if not rel_url.startswith(ASSETS_URL_PREFIXES): + url = url.replace(TIANGOLO_COM, f"{TIANGOLO_COM}/{lang_code}") + return url + + +def _construct_markdown_link( + url: str, text: str, title: str | None, attributes: str | None, lang_code: str +) -> str: + """ + Construct a markdown link, adjusting the URL for the given language code if needed. + """ + url = _add_lang_code_to_url(url, lang_code) + + if title: + link = f'[{text}]({url} "{title}")' + else: + link = f"[{text}]({url})" + + if attributes: + link += f"{{{attributes}}}" + + return link + + +def replace_markdown_links( + text: list[str], + links: list[MarkdownLinkInfo], + original_links: list[MarkdownLinkInfo], + lang_code: str, +) -> list[str]: + """ + Replace markdown links in the given text with the original links. + + Fail if the number of links does not match the original. + """ + + if len(links) != len(original_links): + raise ValueError( + "Number of markdown links does not match the number in the " + "original document " + f"({len(links)} vs {len(original_links)})" + ) + + modified_text = text.copy() + for i, link_info in enumerate(links): + link_text = link_info["text"] + link_title = link_info["title"] + original_link_info = original_links[i] + + # Replace + replacement_link = _construct_markdown_link( + url=original_link_info["url"], + text=link_text, + title=link_title, + attributes=original_link_info["attributes"], + lang_code=lang_code, + ) + line_no = link_info["line_no"] - 1 + modified_line = modified_text[line_no] + modified_line = modified_line.replace( + link_info["full_match"], replacement_link, 1 + ) + modified_text[line_no] = modified_line + + return modified_text + + +# HTML links +# -------------------------------------------------------------------------------------- + + +def extract_html_links(lines: list[str]) -> list[HtmlLinkInfo]: + """ + Extract all HTML links from the given lines. + + Return list of HtmlLinkInfo, where each dict contains: + - `line_no` - line number (1-based) + - `full_tag` - full HTML link tag + - `attributes` - list of HTMLLinkAttribute (name, quote, value) + - `text` - link text + """ + + links = [] + for line_no, line in enumerate(lines, start=1): + for html_link in HTML_LINK_RE.finditer(line): + link_str = html_link.group(0) + + link_text_match = HTML_LINK_TEXT_RE.match(link_str) + assert link_text_match is not None + link_text = link_text_match.group(2) + assert isinstance(link_text, str) + + link_open_tag_match = HTML_LINK_OPEN_TAG_RE.match(link_str) + assert link_open_tag_match is not None + link_open_tag = link_open_tag_match.group(1) + assert isinstance(link_open_tag, str) + + attributes: list[HTMLLinkAttribute] = [] + for attr_name, attr_quote, attr_value in re.findall( + HTML_ATTR_RE, link_open_tag + ): + assert isinstance(attr_name, str) + assert isinstance(attr_quote, str) + assert isinstance(attr_value, str) + attributes.append( + HTMLLinkAttribute( + name=attr_name, quote=attr_quote, value=attr_value + ) + ) + links.append( + HtmlLinkInfo( + line_no=line_no, + full_tag=link_str, + attributes=attributes, + text=link_text, + ) + ) + return links + + +def _construct_html_link( + link_text: str, + attributes: list[HTMLLinkAttribute], + lang_code: str, +) -> str: + """ + Reconstruct HTML link, adjusting the URL for the given language code if needed. + """ + + attributes_upd: list[HTMLLinkAttribute] = [] + for attribute in attributes: + if attribute["name"] == "href": + original_url = attribute["value"] + url = _add_lang_code_to_url(original_url, lang_code) + attributes_upd.append( + HTMLLinkAttribute(name="href", quote=attribute["quote"], value=url) + ) + else: + attributes_upd.append(attribute) + + attrs_str = " ".join( + f"{attribute['name']}={attribute['quote']}{attribute['value']}{attribute['quote']}" + for attribute in attributes_upd + ) + return f"<a {attrs_str}>{link_text}</a>" + + +def replace_html_links( + text: list[str], + links: list[HtmlLinkInfo], + original_links: list[HtmlLinkInfo], + lang_code: str, +) -> list[str]: + """ + Replace HTML links in the given text with the links from the original document. + + Adjust URLs for the given language code. + Fail if the number of links does not match the original. + """ + + if len(links) != len(original_links): + raise ValueError( + "Number of HTML links does not match the number in the " + "original document " + f"({len(links)} vs {len(original_links)})" + ) + + modified_text = text.copy() + for link_index, link in enumerate(links): + original_link_info = original_links[link_index] + + # Replace in the document text + replacement_link = _construct_html_link( + link_text=link["text"], + attributes=original_link_info["attributes"], + lang_code=lang_code, + ) + line_no = link["line_no"] - 1 + modified_text[line_no] = modified_text[line_no].replace( + link["full_tag"], replacement_link, 1 + ) + + return modified_text + + +# Multiline code blocks +# -------------------------------------------------------------------------------------- + + +def get_code_block_lang(line: str) -> str: + match = CODE_BLOCK_LANG_RE.match(line) + if match: + return match.group(1) + return "" + + +def extract_multiline_code_blocks(text: list[str]) -> list[MultilineCodeBlockInfo]: + blocks: list[MultilineCodeBlockInfo] = [] + + in_code_block3 = False + in_code_block4 = False + current_block_lang = "" + current_block_start_line = -1 + current_block_lines = [] + + for line_no, line in enumerate(text, start=1): + stripped = line.lstrip() + + # --- Detect opening fence --- + if not (in_code_block3 or in_code_block4): + if stripped.startswith("```"): + current_block_start_line = line_no + count = len(stripped) - len(stripped.lstrip("`")) + if count == 3: + in_code_block3 = True + current_block_lang = get_code_block_lang(stripped) + current_block_lines = [line] + continue + elif count >= 4: + in_code_block4 = True + current_block_lang = get_code_block_lang(stripped) + current_block_lines = [line] + continue + + # --- Detect closing fence --- + elif in_code_block3: + if stripped.startswith("```"): + count = len(stripped) - len(stripped.lstrip("`")) + if count == 3: + current_block_lines.append(line) + blocks.append( + MultilineCodeBlockInfo( + lang=current_block_lang, + start_line_no=current_block_start_line, + content=current_block_lines, + ) + ) + in_code_block3 = False + current_block_lang = "" + current_block_start_line = -1 + current_block_lines = [] + continue + current_block_lines.append(line) + + elif in_code_block4: + if stripped.startswith("````"): + count = len(stripped) - len(stripped.lstrip("`")) + if count >= 4: + current_block_lines.append(line) + blocks.append( + MultilineCodeBlockInfo( + lang=current_block_lang, + start_line_no=current_block_start_line, + content=current_block_lines, + ) + ) + in_code_block4 = False + current_block_lang = "" + current_block_start_line = -1 + current_block_lines = [] + continue + current_block_lines.append(line) + + return blocks + + +def _split_hash_comment(line: str) -> tuple[str, str | None]: + match = HASH_COMMENT_RE.match(line) + if match: + code = match.group("code").rstrip() + comment = match.group("comment") + return code, comment + return line.rstrip(), None + + +def _split_slashes_comment(line: str) -> tuple[str, str | None]: + match = SLASHES_COMMENT_RE.match(line) + if match: + code = match.group("code").rstrip() + comment = match.group("comment") + return code, comment + return line, None + + +def replace_multiline_code_block( + block_a: MultilineCodeBlockInfo, block_b: MultilineCodeBlockInfo +) -> list[str]: + """ + Replace multiline code block `a` with block `b` leaving comments intact. + + Syntax of comments depends on the language of the code block. + Raises ValueError if the blocks are not compatible (different languages or different number of lines). + """ + + start_line = block_a["start_line_no"] + end_line_no = start_line + len(block_a["content"]) - 1 + + if block_a["lang"] != block_b["lang"]: + raise ValueError( + f"Code block (lines {start_line}-{end_line_no}) " + "has different language than the original block " + f"('{block_a['lang']}' vs '{block_b['lang']}')" + ) + if len(block_a["content"]) != len(block_b["content"]): + raise ValueError( + f"Code block (lines {start_line}-{end_line_no}) " + "has different number of lines than the original block " + f"({len(block_a['content'])} vs {len(block_b['content'])})" + ) + + block_language = block_a["lang"].lower() + if block_language in {"mermaid"}: + if block_a != block_b: + print( + f"Skipping mermaid code block replacement (lines {start_line}-{end_line_no}). " + "This should be checked manually." + ) + return block_a["content"].copy() # We don't handle mermaid code blocks for now + + code_block: list[str] = [] + for line_a, line_b in zip(block_a["content"], block_b["content"]): + line_a_comment: str | None = None + line_b_comment: str | None = None + + # Handle comments based on language + if block_language in { + "python", + "py", + "sh", + "bash", + "dockerfile", + "requirements", + "gitignore", + "toml", + "yaml", + "yml", + "hash-style-comments", + }: + _line_a_code, line_a_comment = _split_hash_comment(line_a) + _line_b_code, line_b_comment = _split_hash_comment(line_b) + res_line = line_b + if line_b_comment: + res_line = res_line.replace(line_b_comment, line_a_comment, 1) + code_block.append(res_line) + elif block_language in {"console", "json", "slash-style-comments"}: + _line_a_code, line_a_comment = _split_slashes_comment(line_a) + _line_b_code, line_b_comment = _split_slashes_comment(line_b) + res_line = line_b + if line_b_comment: + res_line = res_line.replace(line_b_comment, line_a_comment, 1) + code_block.append(res_line) + else: + code_block.append(line_b) + + return code_block + + +def replace_multiline_code_blocks_in_text( + text: list[str], + code_blocks: list[MultilineCodeBlockInfo], + original_code_blocks: list[MultilineCodeBlockInfo], +) -> list[str]: + """ + Update each code block in `text` with the corresponding code block from + `original_code_blocks` with comments taken from `code_blocks`. + + Raises ValueError if the number, language, or shape of code blocks do not match. + """ + + if len(code_blocks) != len(original_code_blocks): + raise ValueError( + "Number of code blocks does not match the number in the original document " + f"({len(code_blocks)} vs {len(original_code_blocks)})" + ) + + modified_text = text.copy() + for block, original_block in zip(code_blocks, original_code_blocks): + updated_content = replace_multiline_code_block(block, original_block) + + start_line_index = block["start_line_no"] - 1 + for i, updated_line in enumerate(updated_content): + modified_text[start_line_index + i] = updated_line + + return modified_text + + +# All checks +# -------------------------------------------------------------------------------------- + + +def check_translation( + doc_lines: list[str], + en_doc_lines: list[str], + lang_code: str, + auto_fix: bool, + path: str, +) -> list[str]: + # Fix code includes + en_code_includes = extract_code_includes(en_doc_lines) + doc_lines_with_placeholders = replace_code_includes_with_placeholders(doc_lines) + fixed_doc_lines = replace_placeholders_with_code_includes( + doc_lines_with_placeholders, en_code_includes + ) + if auto_fix and (fixed_doc_lines != doc_lines): + print(f"Fixing code includes in: {path}") + doc_lines = fixed_doc_lines + + # Fix permalinks + en_permalinks = extract_header_permalinks(en_doc_lines) + doc_permalinks = extract_header_permalinks(doc_lines) + fixed_doc_lines = replace_header_permalinks( + doc_lines, doc_permalinks, en_permalinks + ) + if auto_fix and (fixed_doc_lines != doc_lines): + print(f"Fixing header permalinks in: {path}") + doc_lines = fixed_doc_lines + + # Fix markdown links + en_markdown_links = extract_markdown_links(en_doc_lines) + doc_markdown_links = extract_markdown_links(doc_lines) + fixed_doc_lines = replace_markdown_links( + doc_lines, doc_markdown_links, en_markdown_links, lang_code + ) + if auto_fix and (fixed_doc_lines != doc_lines): + print(f"Fixing markdown links in: {path}") + doc_lines = fixed_doc_lines + + # Fix HTML links + en_html_links = extract_html_links(en_doc_lines) + doc_html_links = extract_html_links(doc_lines) + fixed_doc_lines = replace_html_links( + doc_lines, doc_html_links, en_html_links, lang_code + ) + if auto_fix and (fixed_doc_lines != doc_lines): + print(f"Fixing HTML links in: {path}") + doc_lines = fixed_doc_lines + + # Fix multiline code blocks + en_code_blocks = extract_multiline_code_blocks(en_doc_lines) + doc_code_blocks = extract_multiline_code_blocks(doc_lines) + fixed_doc_lines = replace_multiline_code_blocks_in_text( + doc_lines, doc_code_blocks, en_code_blocks + ) + if auto_fix and (fixed_doc_lines != doc_lines): + print(f"Fixing multiline code blocks in: {path}") + doc_lines = fixed_doc_lines + + return doc_lines diff --git a/scripts/tests/test_translation_fixer/conftest.py b/scripts/tests/test_translation_fixer/conftest.py new file mode 100644 index 000000000..b2c745de1 --- /dev/null +++ b/scripts/tests/test_translation_fixer/conftest.py @@ -0,0 +1,32 @@ +import shutil +from pathlib import Path + +import pytest +from typer.testing import CliRunner + + +@pytest.fixture(name="runner") +def get_runner(): + runner = CliRunner() + with runner.isolated_filesystem(): + yield runner + + +@pytest.fixture(name="root_dir") +def prepare_paths(runner): + docs_dir = Path("docs") + en_docs_dir = docs_dir / "en" / "docs" + lang_docs_dir = docs_dir / "lang" / "docs" + en_docs_dir.mkdir(parents=True, exist_ok=True) + lang_docs_dir.mkdir(parents=True, exist_ok=True) + yield Path.cwd() + + +@pytest.fixture +def copy_test_files(root_dir: Path, request: pytest.FixtureRequest): + en_file_path = Path(request.param[0]) + translation_file_path = Path(request.param[1]) + shutil.copy(str(en_file_path), str(root_dir / "docs" / "en" / "docs" / "doc.md")) + shutil.copy( + str(translation_file_path), str(root_dir / "docs" / "lang" / "docs" / "doc.md") + ) diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/en_doc.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/en_doc.md new file mode 100644 index 000000000..cad20e2c7 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/en_doc.md @@ -0,0 +1,44 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +```toml +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Mermaid diagram + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_gt.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_gt.md new file mode 100644 index 000000000..f46070156 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_gt.md @@ -0,0 +1,45 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +```toml +# Extra line +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_lt.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_lt.md new file mode 100644 index 000000000..e08baa70b --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_lines_number_lt.md @@ -0,0 +1,45 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +The following block is missing first line: + +```toml +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_not_translated.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_not_translated.md new file mode 100644 index 000000000..cacb9546d --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_not_translated.md @@ -0,0 +1,44 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +```toml +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_translated.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_translated.md new file mode 100644 index 000000000..d03dca53e --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_mermaid_translated.md @@ -0,0 +1,44 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +```toml +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|требует| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_gt.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_gt.md new file mode 100644 index 000000000..e77050cc9 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_gt.md @@ -0,0 +1,50 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +```toml +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +Extra code block + +``` +$ cd my_project +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_lt.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_lt.md new file mode 100644 index 000000000..918cb883f --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_number_lt.md @@ -0,0 +1,41 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +Missing code block... + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code.md new file mode 100644 index 000000000..88aed900d --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code.md @@ -0,0 +1,46 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +The following block has wrong language code (should be TOML): + +```yaml +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code_2.md b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code_2.md new file mode 100644 index 000000000..a7fbb39f5 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/data/translated_doc_wrong_lang_code_2.md @@ -0,0 +1,46 @@ +# Code blocks { #code-blocks } + +Some text + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +Some more text + +The following block has wrong language code (should be TOML): + +``` +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +And more text + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +And even more text + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + +Диаграма Mermaid + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_lines_number_mismatch.py b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_lines_number_mismatch.py new file mode 100644 index 000000000..906c8a560 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_lines_number_mismatch.py @@ -0,0 +1,58 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_code_blocks/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_lines_number_gt.md")], + indirect=True, +) +def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path( + f"{data_path}/translated_doc_lines_number_gt.md" + ).read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Code block (lines 14-18) has different number of lines than the original block (5 vs 4)" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_lines_number_lt.md")], + indirect=True, +) +def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + # assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path( + f"{data_path}/translated_doc_lines_number_lt.md" + ).read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Code block (lines 16-18) has different number of lines than the original block (3 vs 4)" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_mermaid.py b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_mermaid.py new file mode 100644 index 000000000..75c589fb5 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_mermaid.py @@ -0,0 +1,59 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_code_blocks/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_mermaid_translated.md")], + indirect=True, +) +def test_translated(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 0, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path( + f"{data_path}/translated_doc_mermaid_translated.md" + ).read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert ( + "Skipping mermaid code block replacement (lines 41-44). This should be checked manually." + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [ + ( + f"{data_path}/en_doc.md", + f"{data_path}/translated_doc_mermaid_not_translated.md", + ) + ], + indirect=True, +) +def test_not_translated(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 0, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path( + f"{data_path}/translated_doc_mermaid_not_translated.md" + ).read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert ("Skipping mermaid code block replacement") not in result.output diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_number_mismatch.py b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_number_mismatch.py new file mode 100644 index 000000000..b05dac900 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_number_mismatch.py @@ -0,0 +1,56 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_code_blocks/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_gt.md")], + indirect=True, +) +def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of code blocks does not match the number " + "in the original document (6 vs 5)" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_lt.md")], + indirect=True, +) +def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + # assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of code blocks does not match the number " + "in the original document (4 vs 5)" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_wrong_lang_code.py b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_wrong_lang_code.py new file mode 100644 index 000000000..6c2b18c89 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_wrong_lang_code.py @@ -0,0 +1,58 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_code_blocks/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_wrong_lang_code.md")], + indirect=True, +) +def test_wrong_lang_code_1(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path( + f"{data_path}/translated_doc_wrong_lang_code.md" + ).read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Code block (lines 16-19) has different language than the original block ('yaml' vs 'toml')" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_wrong_lang_code_2.md")], + indirect=True, +) +def test_wrong_lang_code_2(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path( + f"{data_path}/translated_doc_wrong_lang_code_2.md" + ).read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Code block (lines 16-19) has different language than the original block ('' vs 'toml')" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_code_includes/data/en_doc.md b/scripts/tests/test_translation_fixer/test_code_includes/data/en_doc.md new file mode 100644 index 000000000..593da0b32 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_includes/data/en_doc.md @@ -0,0 +1,13 @@ +# Header + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +Some text + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +Some more text + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +And even more text diff --git a/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_gt.md b/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_gt.md new file mode 100644 index 000000000..c1ad94d27 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_gt.md @@ -0,0 +1,15 @@ +# Header + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +Some text + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +Some more text + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +And even more text + +{* ../../docs_src/python_types/tutorial001_py39.py *} diff --git a/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_lt.md b/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_lt.md new file mode 100644 index 000000000..07eaf2c23 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_lt.md @@ -0,0 +1,13 @@ +# Header + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +Some text + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +Some more text + +... + +And even more text diff --git a/scripts/tests/test_translation_fixer/test_code_includes/test_number_mismatch.py b/scripts/tests/test_translation_fixer/test_code_includes/test_number_mismatch.py new file mode 100644 index 000000000..5e3eee51a --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_code_includes/test_number_mismatch.py @@ -0,0 +1,56 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_code_includes/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_gt.md")], + indirect=True, +) +def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1 + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of code include placeholders does not match the number of code includes " + "in the original document (4 vs 3)" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_lt.md")], + indirect=True, +) +def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1 + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of code include placeholders does not match the number of code includes " + "in the original document (2 vs 3)" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_complex_doc/data/en_doc.md b/scripts/tests/test_translation_fixer/test_complex_doc/data/en_doc.md new file mode 100644 index 000000000..69cd3f3fd --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_complex_doc/data/en_doc.md @@ -0,0 +1,244 @@ +# Test translation fixer tool { #test-translation-fixer } + +## Code blocks with and without comments { #code-blocks-with-and-without-comments } + +This is a test page for the translation fixer tool. + +### Code blocks with comments { #code-blocks-with-comments } + +The following code blocks include comments in different styles. +Fixer tool should fix content, but preserve comments correctly. + +```python +# This is a sample Python code block +def hello_world(): + # Comment with indentation + print("Hello, world!") # Print greeting +``` + +```toml +# This is a sample TOML code block +title = "TOML Example" # Title of the document +``` + +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +```json +{ + // This is a sample JSON code block + "greeting": "Hello, world!" // Greeting +} +``` + + +### Code blocks with comments where language uses different comment styles { #code-blocks-with-different-comment-styles } + +The following code blocks include comments in different styles based on the language. +Fixer tool will not preserve comments in these blocks. + +```json +{ + # This is a sample JSON code block + "greeting": "Hello, world!" # Print greeting +} +``` + +```console +# This is a sample console code block +$ echo "Hello, world!" # Print greeting +``` + +```toml +// This is a sample TOML code block +title = "TOML Example" // Title of the document +``` + + +### Code blocks with comments with unsupported languages or without language specified { #code-blocks-with-unsupported-languages } + +The following code blocks use unsupported languages for comment preservation. +Fixer tool will not preserve comments in these blocks. + +```javascript +// This is a sample JavaScript code block +console.log("Hello, world!"); // Print greeting +``` + +``` +# This is a sample console code block +$ echo "Hello, world!" # Print greeting +``` + +``` +// This is a sample console code block +$ echo "Hello, world!" // Print greeting +``` + + +### Code blocks with comments that don't follow pattern { #code-blocks-with-comments-without-pattern } + +Fixer tool expects comments that follow specific pattern: + +- For hash-style comments: comment starts with `# ` (hash following by whitespace) in the beginning of the string or after a whitespace. +- For slash-style comments: comment starts with `// ` (two slashes following by whitespace) in the beginning of the string or after a whitespace. + +If comment doesn't follow this pattern, fixer tool will not preserve it. + +```python +#Function declaration +def hello_world():# Print greeting + print("Hello, world!") #Print greeting without space after hash +``` + +```console +//Function declaration +def hello_world():// Print greeting + print("Hello, world!") //Print greeting without space after slashes +``` + +## Code blocks with quadruple backticks { #code-blocks-with-quadruple-backticks } + +The following code block uses quadruple backticks. + +````python +# Hello world function +def hello_world(): + print("Hello, world!") # Print greeting +```` + +### Backticks number mismatch is fixable { #backticks-number-mismatch-is-fixable } + +The following code block has triple backticks in the original document, but quadruple backticks in the translated document. +It will be fixed by the fixer tool (will convert to triple backticks). + +```Python +# Some Python code +``` + +### Triple backticks inside quadruple backticks { #triple-backticks-inside-quadruple-backticks } + +Comments inside nested code block will NOT be preserved. + +```` +Here is a code block with quadruple backticks that contains triple backticks inside: + +```python +# This is a sample Python code block +def hello_world(): + print("Hello, world!") # Print greeting +``` + +```` + +# Code includes { #code-includes } + +## Simple code includes { #simple-code-includes } + +{* ../../docs_src/python_types/tutorial001_py39.py *} + +{* ../../docs_src/python_types/tutorial002_py39.py *} + + +## Code includes with highlighting { #code-includes-with-highlighting } + +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} + +{* ../../docs_src/python_types/tutorial006_py39.py hl[10] *} + + +## Code includes with line ranges { #code-includes-with-line-ranges } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] *} + + +## Code includes with line ranges and highlighting { #code-includes-with-line-ranges-and-highlighting } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +{* ../../docs_src/dependencies/tutorial015_an_py310.py ln[10:15] hl[12:14] *} + + +## Code includes qith title { #code-includes-with-title } + +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +## Code includes with unknown attributes { #code-includes-with-unknown-attributes } + +{* ../../docs_src/python_types/tutorial001_py39.py unknown[123] *} + +## Some more code includes to test fixing { #some-more-code-includes-to-test-fixing } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + + + +# Links { #links } + +## Markdown-style links { #markdown-style-links } + +This is a [Markdown link](https://example.com) to an external site. + +This is a link with attributes: [**FastAPI** Project Generators](project-generation.md){.internal-link target=_blank} + +This is a link to the main FastAPI site: [FastAPI](https://fastapi.tiangolo.com) - tool should add language code to the URL. + +This is a link to one of the pages on FastAPI site: [How to](https://fastapi.tiangolo.com/how-to/) - tool should add language code to the URL. + +Link to test wrong attribute: [**FastAPI** Project Generators](project-generation.md){.internal-link} - tool should fix the attribute. + +Link with a title: [Example](https://example.com "Example site") - URL will be fixed, title preserved. + +### Markdown link to static assets { #markdown-link-to-static-assets } + +These are links to static assets: + +* [FastAPI Logo](https://fastapi.tiangolo.com/img/fastapi-logo.png) +* [FastAPI CSS](https://fastapi.tiangolo.com/css/fastapi.css) +* [FastAPI JS](https://fastapi.tiangolo.com/js/fastapi.js) + +Tool should NOT add language code to their URLs. + +## HTML-style links { #html-style-links } + +This is an <a href="https://example.com" target="_blank" class="external-link">HTML link</a> to an external site. + +This is an <a href="https://fastapi.tiangolo.com">link to the main FastAPI site</a> - tool should add language code to the URL. + +This is an <a href="https://fastapi.tiangolo.com/how-to/">link to one of the pages on FastAPI site</a> - tool should add language code to the URL. + +Link to test wrong attribute: <a href="project-generation.md" class="internal-link">**FastAPI** Project Generators</a> - tool should fix the attribute. + +### HTML links to static assets { #html-links-to-static-assets } + +These are links to static assets: + +* <a href="https://fastapi.tiangolo.com/img/fastapi-logo.png">FastAPI Logo</a> +* <a href="https://fastapi.tiangolo.com/css/fastapi.css">FastAPI CSS</a> +* <a href="https://fastapi.tiangolo.com/js/fastapi.js">FastAPI JS</a> + +Tool should NOT add language code to their URLs. + +# Header (with HTML link to <a href="https://tiangolo.com">tiangolo.com</a>) { #header-with-html-link-to-tiangolo-com } + +#Not a header + +```Python +# Also not a header +``` + +Some text diff --git a/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc.md b/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc.md new file mode 100644 index 000000000..c922d7b13 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc.md @@ -0,0 +1,240 @@ +# Тестовый инструмент исправления переводов { #test-translation-fixer } + +## Блоки кода с комментариями и без комментариев { #code-blocks-with-and-without-comments } + +Это тестовая страница для инструмента исправления переводов. + +### Блоки кода с комментариями { #code-blocks-with-comments } + +Следующие блоки кода содержат комментарии в разных стилях. +Инструмент исправления должен исправлять содержимое, но корректно сохранять комментарии. + +```python +# Это пример блока кода на Python +def hello_world(): + # Комментарий с отступом + print("Hello, world!") # Печать приветствия +``` + +```toml +# Это пример блока кода на TOML +title = "TOML Example" # Заголовок документа +``` + +```console +// Используйте команду "live" и передайте код языка в качестве аргумента CLI +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +```json +{ + // Это пример блока кода на JSON + "greeting": "Hello, world!" // Печать приветствия +} +``` + + +### Блоки кода с комментариями, где язык использует другие стили комментариев { #code-blocks-with-different-comment-styles } + +Следующие блоки кода содержат комментарии в разных стилях в зависимости от языка. +Инструмент исправления не будет сохранять комментарии в этих блоках. + +```json +{ + # Это пример блока кода на JSON + "greeting": "Hello, world!" # Печать приветствия +} +``` + +```console +# Это пример блока кода консоли +$ echo "Hello, world!" # Печать приветствия +``` + +```toml +// Это пример блока кода на TOML +title = "TOML Example" // Заголовок документа +``` + +### Блоки кода с комментариями на неподдерживаемых языках или без указания языка { #code-blocks-with-unsupported-languages } + +Следующие блоки кода используют неподдерживаемые языки для сохранения комментариев. +Инструмент исправления не будет сохранять комментарии в этих блоках. + +```javascript +// Это пример блока кода на JavaScript +console.log("Hello, world!"); // Печать приветствия +``` + +``` +# Это пример блока кода консоли +$ echo "Hello, world!" # Печать приветствия +``` + +``` +// Это пример блока кода консоли +$ echo "Hello, world!" // Печать приветствия +``` + +### Блоки кода с комментариями, которые не соответствуют шаблону { #code-blocks-with-comments-without-pattern } + +Инструмент исправления ожидает комментарии, которые соответствуют определённому шаблону: + +- Для комментариев в стиле с решёткой: комментарий начинается с `# ` (решётка, затем пробел) в начале строки или после пробела. +- Для комментариев в стиле со слешами: комментарий начинается с `// ` (два слеша, затем пробел) в начале строки или после пробела. + +Если комментарий не соответствует этому шаблону, инструмент исправления не будет его сохранять. + +```python +#Объявление функции +def hello_world():# Печать приветствия + print("Hello, world!") #Печать приветствия без пробела после решётки +``` + +```console +//Объявление функции +def hello_world():// Печать приветствия + print("Hello, world!") //Печать приветствия без пробела после слешей +``` + +## Блок кода с четырёхкратными обратными кавычками { #code-blocks-with-quadruple-backticks } + +Следующий блок кода содержит четырёхкратные обратные кавычки. + +````python +# Функция приветствия +def hello_world(): + print("Hello, world") # Печать приветствия +```` + +### Несоответствие обратных кавычек фиксится { #backticks-number-mismatch-is-fixable } + +Следующий блок кода имеет тройные обратные кавычки в оригинальном документе, но четырёхкратные обратные кавычки в переведённом документе. +Это будет исправлено инструментом исправления (будет преобразовано в тройные обратные кавычки). + +````Python +# Немного кода на Python +```` + +### Блок кода в тройных обратных кавычка внутри блока кода в четырёхкратных обратных кавычках { #triple-backticks-inside-quadruple-backticks } + +Комментарии внутри вложенного блока кода в тройных обратных кавычках НЕ БУДУТ сохранены. + +```` +Here is a code block with quadruple backticks that contains triple backticks inside: + +```python +# Этот комментарий НЕ будет сохранён +def hello_world(): + print("Hello, world") # Как и этот комментарий +``` + +```` + +# Включения кода { #code-includes } + +## Простые включения кода { #simple-code-includes } + +{* ../../docs_src/python_types/tutorial001_py39.py *} + +{* ../../docs_src/python_types/tutorial002_py39.py *} + + +## Включения кода с подсветкой { #code-includes-with-highlighting } + +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} + +{* ../../docs_src/python_types/tutorial006_py39.py hl[10] *} + + +## Включения кода с диапазонами строк { #code-includes-with-line-ranges } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] *} + + +## Включения кода с диапазонами строк и подсветкой { #code-includes-with-line-ranges-and-highlighting } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +{* ../../docs_src/dependencies/tutorial015_an_py310.py ln[10:15] hl[12:14] *} + + +## Включения кода с заголовком { #code-includes-with-title } + +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +## Включения кода с неизвестными атрибутами { #code-includes-with-unknown-attributes } + +{* ../../docs_src/python_types/tutorial001_py39.py unknown[123] *} + +## Ещё включения кода для тестирования исправления { #some-more-code-includes-to-test-fixing } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19 : 21] *} + +{* ../../docs_src/bigger_applications/app_an_py39/wrong.py hl[3] title["app/internal/admin.py"] *} + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[1:30] hl[1:10] *} + +# Ссылки { #links } + +## Ссылки в стиле Markdown { #markdown-style-links } + +Это [Markdown-ссылка](https://example.com) на внешний сайт. + +Это ссылка с атрибутами: [**FastAPI** генераторы проектов](project-generation.md){.internal-link target=_blank} + +Это ссылка на основной сайт FastAPI: [FastAPI](https://fastapi.tiangolo.com) — инструмент должен добавить код языка в URL. + +Это ссылка на одну из страниц на сайте FastAPI: [How to](https://fastapi.tiangolo.com/how-to) — инструмент должен добавить код языка в URL. + +Ссылка для тестирования неправильного атрибута: [**FastAPI** генераторы проектов](project-generation.md){.external-link} - инструмент должен исправить атрибут. + +Ссылка с заголовком: [Пример](http://example.com/ "Сайт для примера") - URL будет исправлен инструментом, заголовок сохранится. + +### Markdown ссылки на статические ресурсы { #markdown-link-to-static-assets } + +Это ссылки на статические ресурсы: + +* [FastAPI Logo](https://fastapi.tiangolo.com/img/fastapi-logo.png) +* [FastAPI CSS](https://fastapi.tiangolo.com/css/fastapi.css) +* [FastAPI JS](https://fastapi.tiangolo.com/js/fastapi.js) + +Инструмент НЕ должен добавлять код языка в их URL. + +## Ссылки в стиле HTML { #html-style-links } + +Это <a href="https://example.com" target="_blank" class="external-link">HTML-ссылка</a> на внешний сайт. + +Это <a href="https://fastapi.tiangolo.com">ссылка на основной сайт FastAPI</a> — инструмент должен добавить код языка в URL. + +Это <a href="https://fastapi.tiangolo.com/how-to/">ссылка на одну из страниц на сайте FastAPI</a> — инструмент должен добавить код языка в URL. + +Ссылка для тестирования неправильного атрибута: <a href="project-generation.md" class="external-link">**FastAPI** генераторы проектов</a> - инструмент должен исправить атрибут. + +### HTML ссылки на статические ресурсы { #html-links-to-static-assets } + +Это ссылки на статические ресурсы: + +* <a href="https://fastapi.tiangolo.com/img/fastapi-logo.png">FastAPI Logo</a> +* <a href="https://fastapi.tiangolo.com/css/fastapi.css">FastAPI CSS</a> +* <a href="https://fastapi.tiangolo.com/js/fastapi.js">FastAPI JS</a> + +Инструмент НЕ должен добавлять код языка в их URL. + +# Заголовок (с HTML ссылкой на <a href="https://tiangolo.com">tiangolo.com</a>) { #header-5 } + +#Не заголовок + +```Python +# Также не заголовок +``` + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc_expected.md b/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc_expected.md new file mode 100644 index 000000000..b33f36e77 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc_expected.md @@ -0,0 +1,240 @@ +# Тестовый инструмент исправления переводов { #test-translation-fixer } + +## Блоки кода с комментариями и без комментариев { #code-blocks-with-and-without-comments } + +Это тестовая страница для инструмента исправления переводов. + +### Блоки кода с комментариями { #code-blocks-with-comments } + +Следующие блоки кода содержат комментарии в разных стилях. +Инструмент исправления должен исправлять содержимое, но корректно сохранять комментарии. + +```python +# Это пример блока кода на Python +def hello_world(): + # Комментарий с отступом + print("Hello, world!") # Печать приветствия +``` + +```toml +# Это пример блока кода на TOML +title = "TOML Example" # Заголовок документа +``` + +```console +// Используйте команду "live" и передайте код языка в качестве аргумента CLI +$ python ./scripts/docs.py live es + +<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 +<span style="color: green;">[INFO]</span> Start watching changes +<span style="color: green;">[INFO]</span> Start detecting changes +``` + +```json +{ + // Это пример блока кода на JSON + "greeting": "Hello, world!" // Печать приветствия +} +``` + + +### Блоки кода с комментариями, где язык использует другие стили комментариев { #code-blocks-with-different-comment-styles } + +Следующие блоки кода содержат комментарии в разных стилях в зависимости от языка. +Инструмент исправления не будет сохранять комментарии в этих блоках. + +```json +{ + # This is a sample JSON code block + "greeting": "Hello, world!" # Print greeting +} +``` + +```console +# This is a sample console code block +$ echo "Hello, world!" # Print greeting +``` + +```toml +// This is a sample TOML code block +title = "TOML Example" // Title of the document +``` + +### Блоки кода с комментариями на неподдерживаемых языках или без указания языка { #code-blocks-with-unsupported-languages } + +Следующие блоки кода используют неподдерживаемые языки для сохранения комментариев. +Инструмент исправления не будет сохранять комментарии в этих блоках. + +```javascript +// This is a sample JavaScript code block +console.log("Hello, world!"); // Print greeting +``` + +``` +# This is a sample console code block +$ echo "Hello, world!" # Print greeting +``` + +``` +// This is a sample console code block +$ echo "Hello, world!" // Print greeting +``` + +### Блоки кода с комментариями, которые не соответствуют шаблону { #code-blocks-with-comments-without-pattern } + +Инструмент исправления ожидает комментарии, которые соответствуют определённому шаблону: + +- Для комментариев в стиле с решёткой: комментарий начинается с `# ` (решётка, затем пробел) в начале строки или после пробела. +- Для комментариев в стиле со слешами: комментарий начинается с `// ` (два слеша, затем пробел) в начале строки или после пробела. + +Если комментарий не соответствует этому шаблону, инструмент исправления не будет его сохранять. + +```python +#Function declaration +def hello_world():# Print greeting + print("Hello, world!") #Print greeting without space after hash +``` + +```console +//Function declaration +def hello_world():// Print greeting + print("Hello, world!") //Print greeting without space after slashes +``` + +## Блок кода с четырёхкратными обратными кавычками { #code-blocks-with-quadruple-backticks } + +Следующий блок кода содержит четырёхкратные обратные кавычки. + +````python +# Функция приветствия +def hello_world(): + print("Hello, world!") # Печать приветствия +```` + +### Несоответствие обратных кавычек фиксится { #backticks-number-mismatch-is-fixable } + +Следующий блок кода имеет тройные обратные кавычки в оригинальном документе, но четырёхкратные обратные кавычки в переведённом документе. +Это будет исправлено инструментом исправления (будет преобразовано в тройные обратные кавычки). + +```Python +# Немного кода на Python +``` + +### Блок кода в тройных обратных кавычка внутри блока кода в четырёхкратных обратных кавычках { #triple-backticks-inside-quadruple-backticks } + +Комментарии внутри вложенного блока кода в тройных обратных кавычках НЕ БУДУТ сохранены. + +```` +Here is a code block with quadruple backticks that contains triple backticks inside: + +```python +# This is a sample Python code block +def hello_world(): + print("Hello, world!") # Print greeting +``` + +```` + +# Включения кода { #code-includes } + +## Простые включения кода { #simple-code-includes } + +{* ../../docs_src/python_types/tutorial001_py39.py *} + +{* ../../docs_src/python_types/tutorial002_py39.py *} + + +## Включения кода с подсветкой { #code-includes-with-highlighting } + +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} + +{* ../../docs_src/python_types/tutorial006_py39.py hl[10] *} + + +## Включения кода с диапазонами строк { #code-includes-with-line-ranges } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] *} + + +## Включения кода с диапазонами строк и подсветкой { #code-includes-with-line-ranges-and-highlighting } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +{* ../../docs_src/dependencies/tutorial015_an_py310.py ln[10:15] hl[12:14] *} + + +## Включения кода с заголовком { #code-includes-with-title } + +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +## Включения кода с неизвестными атрибутами { #code-includes-with-unknown-attributes } + +{* ../../docs_src/python_types/tutorial001_py39.py unknown[123] *} + +## Ещё включения кода для тестирования исправления { #some-more-code-includes-to-test-fixing } + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +# Ссылки { #links } + +## Ссылки в стиле Markdown { #markdown-style-links } + +Это [Markdown-ссылка](https://example.com) на внешний сайт. + +Это ссылка с атрибутами: [**FastAPI** генераторы проектов](project-generation.md){.internal-link target=_blank} + +Это ссылка на основной сайт FastAPI: [FastAPI](https://fastapi.tiangolo.com/lang) — инструмент должен добавить код языка в URL. + +Это ссылка на одну из страниц на сайте FastAPI: [How to](https://fastapi.tiangolo.com/lang/how-to/) — инструмент должен добавить код языка в URL. + +Ссылка для тестирования неправильного атрибута: [**FastAPI** генераторы проектов](project-generation.md){.internal-link} - инструмент должен исправить атрибут. + +Ссылка с заголовком: [Пример](https://example.com "Сайт для примера") - URL будет исправлен инструментом, заголовок сохранится. + +### Markdown ссылки на статические ресурсы { #markdown-link-to-static-assets } + +Это ссылки на статические ресурсы: + +* [FastAPI Logo](https://fastapi.tiangolo.com/img/fastapi-logo.png) +* [FastAPI CSS](https://fastapi.tiangolo.com/css/fastapi.css) +* [FastAPI JS](https://fastapi.tiangolo.com/js/fastapi.js) + +Инструмент НЕ должен добавлять код языка в их URL. + +## Ссылки в стиле HTML { #html-style-links } + +Это <a href="https://example.com" target="_blank" class="external-link">HTML-ссылка</a> на внешний сайт. + +Это <a href="https://fastapi.tiangolo.com/lang">ссылка на основной сайт FastAPI</a> — инструмент должен добавить код языка в URL. + +Это <a href="https://fastapi.tiangolo.com/lang/how-to/">ссылка на одну из страниц на сайте FastAPI</a> — инструмент должен добавить код языка в URL. + +Ссылка для тестирования неправильного атрибута: <a href="project-generation.md" class="internal-link">**FastAPI** генераторы проектов</a> - инструмент должен исправить атрибут. + +### HTML ссылки на статические ресурсы { #html-links-to-static-assets } + +Это ссылки на статические ресурсы: + +* <a href="https://fastapi.tiangolo.com/img/fastapi-logo.png">FastAPI Logo</a> +* <a href="https://fastapi.tiangolo.com/css/fastapi.css">FastAPI CSS</a> +* <a href="https://fastapi.tiangolo.com/js/fastapi.js">FastAPI JS</a> + +Инструмент НЕ должен добавлять код языка в их URL. + +# Заголовок (с HTML ссылкой на <a href="https://tiangolo.com">tiangolo.com</a>) { #header-with-html-link-to-tiangolo-com } + +#Не заголовок + +```Python +# Также не заголовок +``` + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_complex_doc/test_compex_doc.py b/scripts/tests/test_translation_fixer/test_complex_doc/test_compex_doc.py new file mode 100644 index 000000000..86cc4d5eb --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_complex_doc/test_compex_doc.py @@ -0,0 +1,30 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_complex_doc/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc.md")], + indirect=True, +) +def test_fix(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 0, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = (data_path / "translated_doc_expected.md").read_text() + assert fixed_content == expected_content + + assert "Fixing multiline code blocks in" in result.output + assert "Fixing markdown links in" in result.output diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/data/en_doc.md b/scripts/tests/test_translation_fixer/test_header_permalinks/data/en_doc.md new file mode 100644 index 000000000..878e16fb5 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/data/en_doc.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text + +## Header 2 { #header-2 } + +Some more text + +### Header 3 { #header-3 } + +Even more text + +# Header 4 { #header-4 } + +A bit more text + +#Not a header + +Final portion of text diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_1.md b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_1.md new file mode 100644 index 000000000..feefd7770 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_1.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text + +# Header 2 { #header-2 } + +Some more text + +### Header 3 { #header-3 } + +Even more text + +# Header 4 { #header-4 } + +A bit more text + +#Not a header + +Final portion of text diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_2.md b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_2.md new file mode 100644 index 000000000..ad53a660c --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_level_mismatch_2.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text + +## Header 2 { #header-2 } + +Some more text + +### Header 3 { #header-3 } + +Even more text + +## Header 4 { #header-4 } + +A bit more text + +#Not a header + +Final portion of text diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_gt.md b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_gt.md new file mode 100644 index 000000000..9c517c9bb --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_gt.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text + +## Header 2 { #header-2 } + +Some more text + +### Header 3 { #header-3 } + +Even more text + +# Header 4 { #header-4 } + +A bit more text + +# Extra header + +Final portion of text diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_lt.md b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_lt.md new file mode 100644 index 000000000..8a308728b --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/data/translated_doc_number_lt.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text + +## Header 2 { #header-2 } + +Some more text + +### Header 3 { #header-3 } + +Even more text + +Header 4 is missing + +A bit more text + +#Not a header + +Final portion of text diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_level_mismatch.py b/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_level_mismatch.py new file mode 100644 index 000000000..9fe2f7ba7 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_level_mismatch.py @@ -0,0 +1,60 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_header_permalinks/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_level_mismatch_1.md")], + indirect=True, +) +def test_level_mismatch_1(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1 + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path( + f"{data_path}/translated_doc_level_mismatch_1.md" + ).read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Header levels do not match between document and original document" + " (found #, expected ##) for header №2 in line 5" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_level_mismatch_2.md")], + indirect=True, +) +def test_level_mismatch_2(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1 + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path( + f"{data_path}/translated_doc_level_mismatch_2.md" + ).read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Header levels do not match between document and original document" + " (found ##, expected #) for header №4 in line 13" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_number_mismatch.py b/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_number_mismatch.py new file mode 100644 index 000000000..c0e78d030 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_number_mismatch.py @@ -0,0 +1,56 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_header_permalinks/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_gt.md")], + indirect=True, +) +def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1 + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of headers with permalinks does not match the number " + "in the original document (5 vs 4)" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_lt.md")], + indirect=True, +) +def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1 + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of headers with permalinks does not match the number " + "in the original document (3 vs 4)" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_html_links/data/en_doc.md b/scripts/tests/test_translation_fixer/test_html_links/data/en_doc.md new file mode 100644 index 000000000..4c4d104cf --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_html_links/data/en_doc.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text with a link to <a href="https://fastapi.tiangolo.com">FastAPI</a>. + +## Header 2 { #header-2 } + +Two links here: <a href="https://fastapi.tiangolo.com/how-to/">How to</a> and <a href="project-generation.md" class="internal-link" target="_blank">Project Generators</a>. + +### Header 3 { #header-3 } + +Another link: <a href="project-generation.md" class="internal-link" target="_blank" title="Link title">**FastAPI** Project Generators</a> with title. + +# Header 4 { #header-4 } + +Link to anchor: <a href="#header-2">Header 2</a> + +# Header with <a href="http://example.com">link</a> { #header-with-link } + +Some text diff --git a/scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_gt.md b/scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_gt.md new file mode 100644 index 000000000..bac40242b --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_gt.md @@ -0,0 +1,21 @@ +# Заголовок 1 { #header-1 } + +Немного текста со ссылкой на <a href="https://fastapi.tiangolo.com">FastAPI</a>. + +## Заголовок 2 { #header-2 } + +Две ссылки здесь: <a href="https://fastapi.tiangolo.com/how-to/">How to</a> и <a href="project-generation.md" class="internal-link" target="_blank">Project Generators</a>. + +### Заголовок 3 { #header-3 } + +Ещё ссылка: <a href="project-generation.md" class="internal-link" target="_blank" title="Тайтл">**FastAPI** Генераторы Проектов</a> с тайтлом. + +И ещё одна <a href="https://github.com">экстра ссылка</a>. + +# Заголовок 4 { #header-4 } + +Ссылка на якорь: <a href="#header-2">Заголовок 2</a> + +# Заголовок со <a href="http://example.com">ссылкой</a> { #header-with-link } + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_lt.md b/scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_lt.md new file mode 100644 index 000000000..e2b36b688 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_html_links/data/translated_doc_number_lt.md @@ -0,0 +1,19 @@ +# Заголовок 1 { #header-1 } + +Немного текста со ссылкой на <a href="https://fastapi.tiangolo.com">FastAPI</a>. + +## Заголовок 2 { #header-2 } + +Две ссылки здесь: <a href="https://fastapi.tiangolo.com/how-to/">How to</a> и <a href="project-generation.md" class="internal-link" target="_blank">Project Generators</a>. + +### Заголовок 3 { #header-3 } + +Ещё ссылка: <a href="project-generation.md" class="internal-link" target="_blank" title="Тайтл">**FastAPI** Генераторы Проектов</a> с тайтлом. + +# Заголовок 4 { #header-4 } + +Ссылка на якорь: <a href="#header-2">Заголовок 2</a> + +# Заголовок с потерянной ссылкой { #header-with-link } + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_html_links/test_html_links_number_mismatch.py b/scripts/tests/test_translation_fixer/test_html_links/test_html_links_number_mismatch.py new file mode 100644 index 000000000..271e5d205 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_html_links/test_html_links_number_mismatch.py @@ -0,0 +1,54 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path("scripts/tests/test_translation_fixer/test_html_links/data").absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_gt.md")], + indirect=True, +) +def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of HTML links does not match the number " + "in the original document (7 vs 6)" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_lt.md")], + indirect=True, +) +def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + # assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of HTML links does not match the number " + "in the original document (5 vs 6)" + ) in result.output diff --git a/scripts/tests/test_translation_fixer/test_markdown_links/data/en_doc.md b/scripts/tests/test_translation_fixer/test_markdown_links/data/en_doc.md new file mode 100644 index 000000000..9d965b0d6 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_markdown_links/data/en_doc.md @@ -0,0 +1,19 @@ +# Header 1 { #header-1 } + +Some text with a link to [FastAPI](https://fastapi.tiangolo.com). + +## Header 2 { #header-2 } + +Two links here: [How to](https://fastapi.tiangolo.com/how-to/) and [Project Generators](project-generation.md){.internal-link target=_blank}. + +### Header 3 { #header-3 } + +Another link: [**FastAPI** Project Generators](project-generation.md "Link title"){.internal-link target=_blank} with title. + +# Header 4 { #header-4 } + +Link to anchor: [Header 2](#header-2) + +# Header with [link](http://example.com) { #header-with-link } + +Some text diff --git a/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc.md b/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc.md new file mode 100644 index 000000000..804a7e60a --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc.md @@ -0,0 +1,19 @@ +# Заголовок 1 { #header-1 } + +Немного текста со ссылкой на [FastAPI](https://fastapi.tiangolo.com). + +## Заголовок 2 { #header-2 } + +Две ссылки здесь: [How to](https://fastapi.tiangolo.com/how-to/) и [Project Generators](project-generation.md){.internal-link target=_blank}. + +### Заголовок 3 { #header-3 } + +Ещё ссылка: [**FastAPI** Генераторы Проектов](project-generation.md "Тайтл"){.internal-link target=_blank} с тайтлом. + +# Заголовок 4 { #header-4 } + +Ссылка на якорь: [Заголовок 2](#header-2) + +# Заголовок со [ссылкой](http://example.com) { #header-with-link } + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_gt.md b/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_gt.md new file mode 100644 index 000000000..9cbedb676 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_gt.md @@ -0,0 +1,21 @@ +# Заголовок 1 { #header-1 } + +Немного текста со ссылкой на [FastAPI](https://fastapi.tiangolo.com). + +## Заголовок 2 { #header-2 } + +Две ссылки здесь: [How to](https://fastapi.tiangolo.com/how-to/) и [Project Generators](project-generation.md){.internal-link target=_blank}. + +### Заголовок 3 { #header-3 } + +Ещё ссылка: [**FastAPI** Генераторы Проектов](project-generation.md "Тайтл"){.internal-link target=_blank} с тайтлом. + +И ещё одна [экстра ссылка](https://github.com). + +# Заголовок 4 { #header-4 } + +Ссылка на якорь: [Заголовок 2](#header-2) + +# Заголовок со [ссылкой](http://example.com) { #header-with-link } + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_lt.md b/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_lt.md new file mode 100644 index 000000000..4e9e6ccf7 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_markdown_links/data/translated_doc_number_lt.md @@ -0,0 +1,19 @@ +# Заголовок 1 { #header-1 } + +Немного текста со ссылкой на [FastAPI](https://fastapi.tiangolo.com). + +## Заголовок 2 { #header-2 } + +Две ссылки здесь: [How to](https://fastapi.tiangolo.com/how-to/) и [Project Generators](project-generation.md){.internal-link target=_blank}. + +### Заголовок 3 { #header-3 } + +Ещё ссылка: [**FastAPI** Генераторы Проектов](project-generation.md "Тайтл"){.internal-link target=_blank} с тайтлом. + +# Заголовок 4 { #header-4 } + +Ссылка на якорь: [Заголовок 2](#header-2) + +# Заголовок с потерянной ссылкой { #header-with-link } + +Немного текста diff --git a/scripts/tests/test_translation_fixer/test_markdown_links/test_mkd_links_number_mismatch.py b/scripts/tests/test_translation_fixer/test_markdown_links/test_mkd_links_number_mismatch.py new file mode 100644 index 000000000..0f4952f35 --- /dev/null +++ b/scripts/tests/test_translation_fixer/test_markdown_links/test_mkd_links_number_mismatch.py @@ -0,0 +1,56 @@ +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from scripts.translation_fixer import cli + +data_path = Path( + "scripts/tests/test_translation_fixer/test_markdown_links/data" +).absolute() + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_gt.md")], + indirect=True, +) +def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of markdown links does not match the number " + "in the original document (7 vs 6)" + ) in result.output + + +@pytest.mark.parametrize( + "copy_test_files", + [(f"{data_path}/en_doc.md", f"{data_path}/translated_doc_number_lt.md")], + indirect=True, +) +def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): + result = runner.invoke( + cli, + ["fix-pages", "docs/lang/docs/doc.md"], + ) + # assert result.exit_code == 1, result.output + + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text() + + assert fixed_content == expected_content # Translated doc remains unchanged + assert "Error processing docs/lang/docs/doc.md" in result.output + assert ( + "Number of markdown links does not match the number " + "in the original document (5 vs 6)" + ) in result.output diff --git a/scripts/translation_fixer.py b/scripts/translation_fixer.py new file mode 100644 index 000000000..3e1f42d51 --- /dev/null +++ b/scripts/translation_fixer.py @@ -0,0 +1,132 @@ +import os +from collections.abc import Iterable +from pathlib import Path +from typing import Annotated + +import typer + +from scripts.doc_parsing_utils import check_translation + +non_translated_sections = ( + f"reference{os.sep}", + "release-notes.md", + "fastapi-people.md", + "external-links.md", + "newsletter.md", + "management-tasks.md", + "management.md", + "contributing.md", +) + + +cli = typer.Typer() + + +@cli.callback() +def callback(): + pass + + +def iter_all_lang_paths(lang_path_root: Path) -> Iterable[Path]: + """ + Iterate on the markdown files to translate in order of priority. + """ + + first_dirs = [ + lang_path_root / "learn", + lang_path_root / "tutorial", + lang_path_root / "advanced", + lang_path_root / "about", + lang_path_root / "how-to", + ] + first_parent = lang_path_root + yield from first_parent.glob("*.md") + for dir_path in first_dirs: + yield from dir_path.rglob("*.md") + first_dirs_str = tuple(str(d) for d in first_dirs) + for path in lang_path_root.rglob("*.md"): + if str(path).startswith(first_dirs_str): + continue + if path.parent == first_parent: + continue + yield path + + +def get_all_paths(lang: str): + res: list[str] = [] + lang_docs_root = Path("docs") / lang / "docs" + for path in iter_all_lang_paths(lang_docs_root): + relpath = path.relative_to(lang_docs_root) + if not str(relpath).startswith(non_translated_sections): + res.append(str(relpath)) + return res + + +def process_one_page(path: Path) -> bool: + """ + Fix one translated document by comparing it to the English version. + + Returns True if processed successfully, False otherwise. + """ + + try: + lang_code = path.parts[1] + if lang_code == "en": + print(f"Skipping English document: {path}") + return True + + en_doc_path = Path("docs") / "en" / Path(*path.parts[2:]) + + doc_lines = path.read_text(encoding="utf-8").splitlines() + en_doc_lines = en_doc_path.read_text(encoding="utf-8").splitlines() + + doc_lines = check_translation( + doc_lines=doc_lines, + en_doc_lines=en_doc_lines, + lang_code=lang_code, + auto_fix=True, + path=str(path), + ) + + # Write back the fixed document + doc_lines.append("") # Ensure file ends with a newline + path.write_text("\n".join(doc_lines), encoding="utf-8") + + except ValueError as e: + print(f"Error processing {path}: {e}") + return False + return True + + +@cli.command() +def fix_all(ctx: typer.Context, language: str): + docs = get_all_paths(language) + + all_good = True + for page in docs: + doc_path = Path("docs") / language / "docs" / page + res = process_one_page(doc_path) + all_good = all_good and res + + if not all_good: + raise typer.Exit(code=1) + + +@cli.command() +def fix_pages( + doc_paths: Annotated[ + list[Path], + typer.Argument(help="List of paths to documents."), + ], +): + all_good = True + for path in doc_paths: + res = process_one_page(path) + all_good = all_good and res + + if not all_good: + raise typer.Exit(code=1) + + +if __name__ == "__main__": + cli() From c35e1fd4b42c7937e5414e33bba60991f750f1d1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 21:48:32 +0000 Subject: [PATCH 048/243] =?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 f9b694e2e..c1d24561c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,6 +19,7 @@ hide: ### Internal +* 🔨 Add LLM translations tool fixer. PR [#14652](https://github.com/fastapi/fastapi/pull/14652) by [@YuriiMotov](https://github.com/YuriiMotov). * 👥 Update FastAPI People - Sponsors. PR [#14626](https://github.com/fastapi/fastapi/pull/14626) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI GitHub topic repositories. PR [#14630](https://github.com/fastapi/fastapi/pull/14630) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Contributors and Translators. PR [#14625](https://github.com/fastapi/fastapi/pull/14625) by [@tiangolo](https://github.com/tiangolo). From b4ba7f46522352cf7ae7c74a9030a35679da3491 Mon Sep 17 00:00:00 2001 From: Jonathan Ehwald <github@ehwald.info> Date: Sat, 10 Jan 2026 23:02:57 +0100 Subject: [PATCH 049/243] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20=20Migrate=20to=20?= =?UTF-8?q?uv=20(#14676)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dependabot.yml | 2 +- .github/workflows/build-docs.yml | 25 +- .github/workflows/contributors.yml | 12 +- .github/workflows/deploy-docs.yml | 16 +- .github/workflows/label-approved.yml | 12 +- .github/workflows/notify-translations.yml | 12 +- .github/workflows/people.yml | 12 +- .github/workflows/pre-commit.yml | 7 +- .github/workflows/publish.yml | 15 +- .github/workflows/smokeshow.yml | 11 +- .github/workflows/sponsors.yml | 12 +- .github/workflows/test-redistribute.yml | 4 +- .github/workflows/test.yml | 26 +- .github/workflows/topic-repos.yml | 12 +- .github/workflows/translate.yml | 23 +- .gitignore | 3 - .pre-commit-config.yaml | 1 + .python-version | 1 + docs/en/docs/contributing.md | 32 +- pyproject.toml | 65 +- requirements-docs-tests.txt | 4 - requirements-docs.txt | 21 - requirements-github-actions.txt | 6 - requirements-tests.txt | 18 - requirements-translations.txt | 3 - requirements.txt | 7 - uv.lock | 5339 +++++++++++++++++++++ 27 files changed, 5486 insertions(+), 215 deletions(-) create mode 100644 .python-version delete mode 100644 requirements-docs-tests.txt delete mode 100644 requirements-docs.txt delete mode 100644 requirements-github-actions.txt delete mode 100644 requirements-tests.txt delete mode 100644 requirements-translations.txt delete mode 100644 requirements.txt create mode 100644 uv.lock diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8979aabf8..fdca00387 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,7 @@ updates: commit-message: prefix: ⬆ # Python - - package-ecosystem: "pip" + - package-ecosystem: "uv" directory: "/" schedule: interval: "monthly" diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index cd27179f5..2198fe668 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -8,9 +8,6 @@ on: - opened - synchronize -env: - UV_SYSTEM_PYTHON: 1 - jobs: changes: runs-on: ubuntu-latest @@ -31,8 +28,8 @@ jobs: - README.md - docs/** - docs_src/** - - requirements-docs.txt - pyproject.toml + - uv.lock - mkdocs.yml - mkdocs.env.yml - .github/workflows/build-docs.yml @@ -49,21 +46,20 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install docs extras - run: uv pip install -r requirements-docs.txt + run: uv sync --locked --no-dev --group docs - name: Export Language Codes id: show-langs run: | - echo "langs=$(python ./scripts/docs.py langs-json)" >> $GITHUB_OUTPUT + echo "langs=$(uv run ./scripts/docs.py langs-json)" >> $GITHUB_OUTPUT build-docs: needs: @@ -83,25 +79,24 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install docs extras - run: uv pip install -r requirements-docs.txt + run: uv sync --locked --no-dev --group docs - name: Update Languages - run: python ./scripts/docs.py update-languages + run: uv run ./scripts/docs.py update-languages - uses: actions/cache@v4 with: key: mkdocs-cards-${{ matrix.lang }}-${{ github.ref }} path: docs/${{ matrix.lang }}/.cache - name: Build Docs - run: python ./scripts/docs.py build-lang ${{ matrix.lang }} + run: uv run ./scripts/docs.py build-lang ${{ matrix.lang }} - uses: actions/upload-artifact@v5 with: name: docs-site-${{ matrix.lang }} diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml index 2abd2fdcf..f3ced6aa3 100644 --- a/.github/workflows/contributors.yml +++ b/.github/workflows/contributors.yml @@ -10,9 +10,6 @@ on: required: false default: "false" -env: - UV_SYSTEM_PYTHON: 1 - jobs: job: if: github.repository_owner == 'fastapi' @@ -28,17 +25,16 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -48,6 +44,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} - name: FastAPI People Contributors - run: python ./scripts/contributors.py + run: uv run ./scripts/contributors.py env: GITHUB_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 50662a190..79d7391c4 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -12,9 +12,6 @@ permissions: pull-requests: write statuses: write -env: - UV_SYSTEM_PYTHON: 1 - jobs: deploy-docs: runs-on: ubuntu-latest @@ -27,19 +24,18 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install GitHub Actions dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions - name: Deploy Docs Status Pending - run: python ./scripts/deploy_docs_status.py + run: uv run ./scripts/deploy_docs_status.py env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} @@ -70,14 +66,14 @@ jobs: command: pages deploy ./site --project-name=${{ env.PROJECT_NAME }} --branch=${{ env.BRANCH }} - name: Deploy Docs Status Error if: failure() - run: python ./scripts/deploy_docs_status.py + run: uv run ./scripts/deploy_docs_status.py env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} RUN_ID: ${{ github.run_id }} STATE: "error" - name: Comment Deploy - run: python ./scripts/deploy_docs_status.py + run: uv run ./scripts/deploy_docs_status.py env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index 7f16254db..1307fb8c2 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -8,9 +8,6 @@ on: permissions: pull-requests: write -env: - UV_SYSTEM_PYTHON: 1 - jobs: label-approved: if: github.repository_owner == 'fastapi' @@ -24,19 +21,18 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install GitHub Actions dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions - name: Label Approved - run: python ./scripts/label_approved.py + run: uv run ./scripts/label_approved.py env: TOKEN: ${{ secrets.GITHUB_TOKEN }} CONFIG: > diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 971e6bbd8..31f3eb402 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -15,9 +15,6 @@ on: required: false default: 'false' -env: - UV_SYSTEM_PYTHON: 1 - jobs: job: runs-on: ubuntu-latest @@ -32,17 +29,16 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -52,7 +48,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Notify Translations - run: python ./scripts/notify_translations.py + run: uv run ./scripts/notify_translations.py env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NUMBER: ${{ github.event.inputs.number || null }} diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index 9b35a3d7e..cb3b74278 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -10,9 +10,6 @@ on: required: false default: "false" -env: - UV_SYSTEM_PYTHON: 1 - jobs: job: if: github.repository_owner == 'fastapi' @@ -28,17 +25,16 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -48,7 +44,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.FASTAPI_PEOPLE }} - name: FastAPI People Experts - run: python ./scripts/people.py + run: uv run ./scripts/people.py env: GITHUB_TOKEN: ${{ secrets.FASTAPI_PEOPLE }} SLEEP_INTERVAL: ${{ vars.PEOPLE_SLEEP_INTERVAL }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index b397912e6..f027140ed 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -40,18 +40,15 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.14" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: cache-dependency-glob: | - requirements**.txt pyproject.toml uv.lock - name: Install Dependencies - run: | - uv venv - uv pip install -r requirements.txt + run: uv sync --locked --extra all - name: Run prek - pre-commit id: precommit run: uvx prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6d9a00b49..2232498cb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,6 +15,7 @@ jobs: - fastapi-slim permissions: id-token: write + contents: read steps: - name: Dump GitHub context env: @@ -24,19 +25,15 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version-file: ".python-version" # Issue ref: https://github.com/actions/setup-python/issues/436 # cache: "pip" # cache-dependency-path: pyproject.toml - - name: Install build dependencies - run: pip install build + - name: Install uv + uses: astral-sh/setup-uv@v7 - name: Build distribution + run: uv build env: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} - run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.13.0 - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" + run: uv publish diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 2ae7ef233..1a45174dc 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -8,9 +8,6 @@ on: permissions: statuses: write -env: - UV_SYSTEM_PYTHON: 1 - jobs: smokeshow: runs-on: ubuntu-latest @@ -23,14 +20,14 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.13' + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: cache-dependency-glob: | - requirements**.txt pyproject.toml - - run: uv pip install -r requirements-github-actions.txt + uv.lock + - run: uv sync --locked --no-dev --group github-actions - uses: actions/download-artifact@v6 with: name: coverage-html @@ -41,7 +38,7 @@ jobs: - name: Upload coverage to Smokeshow run: | for i in 1 2 3 4 5; do - if smokeshow upload htmlcov; then + if uv run smokeshow upload htmlcov; then echo "Smokeshow upload success!" break fi diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 8b0249001..88590ffa6 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -10,9 +10,6 @@ on: required: false default: "false" -env: - UV_SYSTEM_PYTHON: 1 - jobs: job: if: github.repository_owner == 'fastapi' @@ -28,17 +25,16 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -46,7 +42,7 @@ jobs: with: limit-access-to-actor: true - name: FastAPI People Sponsors - run: python ./scripts/sponsors.py + run: uv run ./scripts/sponsors.py env: SPONSORS_TOKEN: ${{ secrets.SPONSORS_TOKEN }} PR_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index 653ab2a74..0491d33ba 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -26,7 +26,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version-file: ".python-version" - name: Install build dependencies run: pip install build - name: Build source distribution @@ -40,7 +40,7 @@ jobs: - name: Install test dependencies run: | cd dist/fastapi*/ - pip install -r requirements-tests.txt + pip install --group tests --editable .[all] env: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} - name: Run source distribution tests diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ad630d94..4d45c20eb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ on: - cron: "0 0 * * 1" env: - UV_SYSTEM_PYTHON: 1 + UV_NO_SYNC: true jobs: test: @@ -44,6 +44,8 @@ jobs: coverage: coverage fail-fast: false runs-on: ${{ matrix.os }} + env: + UV_PYTHON: ${{ matrix.python-version }} steps: - name: Dump GitHub context env: @@ -57,17 +59,16 @@ jobs: - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-tests.txt + run: uv sync --locked --no-dev --group tests --extra all - run: mkdir coverage - name: Test if: matrix.codspeed != 'codspeed' - run: bash scripts/test.sh + run: uv run bash scripts/test.sh env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} @@ -79,7 +80,7 @@ jobs: CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} with: mode: simulation - run: coverage run -m pytest tests/ --codspeed + run: uv run coverage run -m pytest tests/ --codspeed # Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow - name: Store coverage files if: matrix.coverage == 'coverage' @@ -100,17 +101,16 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-tests.txt + run: uv sync --locked --no-dev --group tests --extra all - name: Get coverage files uses: actions/download-artifact@v6 with: @@ -118,15 +118,15 @@ jobs: path: coverage merge-multiple: true - run: ls -la coverage - - run: coverage combine coverage - - run: coverage html --title "Coverage for ${{ github.sha }}" + - run: uv run coverage combine coverage + - run: uv run coverage html --title "Coverage for ${{ github.sha }}" - name: Store coverage HTML uses: actions/upload-artifact@v5 with: name: coverage-html path: htmlcov include-hidden-files: true - - run: coverage report --fail-under=100 + - run: uv run coverage report --fail-under=100 # https://github.com/marketplace/actions/alls-green#why check: # This job does nothing and is only used for the branch protection diff --git a/.github/workflows/topic-repos.yml b/.github/workflows/topic-repos.yml index 41dabee1e..46f6d6084 100644 --- a/.github/workflows/topic-repos.yml +++ b/.github/workflows/topic-repos.yml @@ -5,9 +5,6 @@ on: - cron: "0 12 1 * *" workflow_dispatch: -env: - UV_SYSTEM_PYTHON: 1 - jobs: topic-repos: if: github.repository_owner == 'fastapi' @@ -23,18 +20,17 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" enable-cache: true cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install GitHub Actions dependencies - run: uv pip install -r requirements-github-actions.txt + run: uv sync --locked --no-dev --group github-actions - name: Update Topic Repos - run: python ./scripts/topic_repos.py + run: uv run ./scripts/topic_repos.py env: GITHUB_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index f1267d21f..e6edc5aab 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -31,9 +31,6 @@ on: required: false default: "" -env: - UV_SYSTEM_PYTHON: 1 - jobs: langs: runs-on: ubuntu-latest @@ -45,20 +42,20 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-github-actions.txt -r requirements-translations.txt + run: uv sync --locked --no-dev --group github-actions --group translations - name: Export Language Codes id: show-langs run: | - echo "langs=$(python ./scripts/translate.py llm-translatable-json)" >> $GITHUB_OUTPUT - echo "commands=$(python ./scripts/translate.py commands-json)" >> $GITHUB_OUTPUT + echo "langs=$(uv run ./scripts/translate.py llm-translatable-json)" >> $GITHUB_OUTPUT + echo "commands=$(uv run ./scripts/translate.py commands-json)" >> $GITHUB_OUTPUT env: LANGUAGE: ${{ github.event.inputs.language }} COMMAND: ${{ github.event.inputs.command }} @@ -84,15 +81,15 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@v7 with: cache-dependency-glob: | - requirements**.txt pyproject.toml + uv.lock - name: Install Dependencies - run: uv pip install -r requirements-github-actions.txt -r requirements-translations.txt + run: uv sync --locked --no-dev --group github-actions --group translations # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -104,8 +101,8 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: FastAPI Translate run: | - python ./scripts/translate.py ${{ matrix.command }} - python ./scripts/translate.py make-pr + uv run ./scripts/translate.py ${{ matrix.command }} + uv run ./scripts/translate.py make-pr env: GITHUB_TOKEN: ${{ secrets.FASTAPI_TRANSLATIONS }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} diff --git a/.gitignore b/.gitignore index 3dc12ca95..243cdb93a 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,4 @@ archive.zip # macOS .DS_Store -# Ignore while the setup still depends on requirements.txt files -uv.lock - .codspeed diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10a0949e4..5da3cccf1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,7 @@ repos: hooks: - id: check-added-large-files args: ['--maxkb=750'] + exclude: ^uv.lock$ - id: check-toml - id: check-yaml args: diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..2c0733315 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index ae99059f4..18b7f60f7 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -6,44 +6,20 @@ First, you might want to see the basic ways to [help FastAPI and get help](help- If you already cloned the <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">fastapi repository</a> and you want to deep dive in the code, here are some guidelines to set up your environment. -### Virtual environment - -Follow the instructions to create and activate a [virtual environment](virtual-environments.md){.internal-link target=_blank} for the internal code of `fastapi`. - ### Install requirements -After activating the environment, install the required packages: - -//// tab | `pip` +Create a virtual environment and install the required packages with <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>: <div class="termy"> ```console -$ pip install -r requirements.txt +$ uv sync ---> 100% ``` </div> -//// - -//// tab | `uv` - -If you have <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>: - -<div class="termy"> - -```console -$ uv pip install -r requirements.txt - ----> 100% -``` - -</div> - -//// - It will install all the dependencies and your local FastAPI in your local environment. ### Using your local FastAPI @@ -56,9 +32,9 @@ That way, you don't have to "install" your local version to be able to test ever /// note | Technical Details -This only happens when you install using this included `requirements.txt` instead of running `pip install fastapi` directly. +This only happens when you install using `uv sync` instead of running `pip install fastapi` directly. -That is because inside the `requirements.txt` file, the local version of FastAPI is marked to be installed in "editable" mode, with the `-e` option. +That is because `uv sync` will install the local version of FastAPI in "editable" mode by default. /// diff --git a/pyproject.toml b/pyproject.toml index 9c2c35a9f..fe9b3a7ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,6 +123,68 @@ all = [ [project.scripts] fastapi = "fastapi.cli:main" +[dependency-groups] +dev = [ + { include-group = "tests" }, + { include-group = "docs" }, + { include-group = "translations" }, + "playwright>=1.57.0", + "prek==0.2.22", +] +docs = [ + { include-group = "docs-tests" }, + "black==25.1.0", + "cairosvg==2.8.2", + "griffe-typingdoc==0.3.0", + "griffe-warnings-deprecated==1.1.0", + "jieba==0.42.1", + "markdown-include-variants==0.0.8", + "mdx-include>=1.4.1,<2.0.0", + "mkdocs-macros-plugin==1.4.1", + "mkdocs-material==9.7.0", + "mkdocs-redirects>=1.2.1,<1.3.0", + "mkdocstrings[python]==0.30.1", + "pillow==11.3.0", + "python-slugify==8.0.4", + "pyyaml>=5.3.1,<7.0.0", + "typer==0.16.0", +] +docs-tests = [ + "httpx>=0.23.0,<1.0.0", + "ruff==0.14.3", +] +github-actions = [ + "httpx>=0.27.0,<1.0.0", + "pydantic>=2.5.3,<3.0.0", + "pydantic-settings>=2.1.0,<3.0.0", + "pygithub>=2.3.0,<3.0.0", + "pyyaml>=5.3.1,<7.0.0", + "smokeshow>=0.5.0", +] +tests = [ + { include-group = "docs-tests" }, + "anyio[trio]>=3.2.1,<5.0.0", + "coverage[toml]>=6.5.0,<8.0", + "dirty-equals==0.9.0", + "flask>=1.1.2,<4.0.0", + "inline-snapshot>=0.21.1", + "mypy==1.14.1", + "pwdlib[argon2]>=0.2.1", + "pyjwt==2.9.0", + "pytest>=7.1.3,<9.0.0", + "pytest-codspeed==4.2.0", + "pyyaml>=5.3.1,<7.0.0", + "sqlmodel==0.0.27", + "strawberry-graphql>=0.200.0,<1.0.0", + "types-orjson==3.6.2", + "types-ujson==5.10.0.20240515", +] +translations = [ + "gitpython==3.1.45", + "pydantic-ai==0.4.10", + "pygithub==2.8.1", +] + [tool.pdm] version = { source = "file", path = "fastapi/__init__.py" } distribution = true @@ -131,11 +193,10 @@ distribution = true source-includes = [ "tests/", "docs_src/", - "requirements*.txt", "scripts/", # For a test "docs/en/docs/img/favicon.png", - ] +] [tool.tiangolo._internal-slim-build.packages.fastapi-slim.project] name = "fastapi-slim" diff --git a/requirements-docs-tests.txt b/requirements-docs-tests.txt deleted file mode 100644 index 9350f3ee4..000000000 --- a/requirements-docs-tests.txt +++ /dev/null @@ -1,4 +0,0 @@ -# For mkdocstrings and tests -httpx >=0.23.0,<1.0.0 -# For linting and generating docs versions -ruff ==0.14.3 diff --git a/requirements-docs.txt b/requirements-docs.txt deleted file mode 100644 index beefc654f..000000000 --- a/requirements-docs.txt +++ /dev/null @@ -1,21 +0,0 @@ --e . --r requirements-docs-tests.txt -mkdocs-material==9.7.0 -mdx-include >=1.4.1,<2.0.0 -mkdocs-redirects>=1.2.1,<1.3.0 -typer == 0.16.0 -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.3.0 -# For image processing by Material for MkDocs -cairosvg==2.8.2 -mkdocstrings[python]==0.30.1 -griffe-typingdoc==0.3.0 -griffe-warnings-deprecated==1.1.0 -# For griffe, it formats with black -black==25.1.0 -mkdocs-macros-plugin==1.4.1 -markdown-include-variants==0.0.8 -python-slugify==8.0.4 diff --git a/requirements-github-actions.txt b/requirements-github-actions.txt deleted file mode 100644 index a6a733447..000000000 --- a/requirements-github-actions.txt +++ /dev/null @@ -1,6 +0,0 @@ -PyGithub>=2.3.0,<3.0.0 -pydantic>=2.5.3,<3.0.0 -pydantic-settings>=2.1.0,<3.0.0 -httpx>=0.27.0,<1.0.0 -pyyaml >=5.3.1,<7.0.0 -smokeshow diff --git a/requirements-tests.txt b/requirements-tests.txt deleted file mode 100644 index 1604a2858..000000000 --- a/requirements-tests.txt +++ /dev/null @@ -1,18 +0,0 @@ --e .[all] --r requirements-docs-tests.txt -pytest >=7.1.3,<9.0.0 -coverage[toml] >= 6.5.0,< 8.0 -mypy ==1.14.1 -dirty-equals ==0.9.0 -sqlmodel==0.0.27 -flask >=1.1.2,<4.0.0 -strawberry-graphql >=0.200.0,< 1.0.0 -anyio[trio] >=3.2.1,<5.0.0 -PyJWT==2.9.0 -pyyaml >=5.3.1,<7.0.0 -pwdlib[argon2] >=0.2.1 -inline-snapshot>=0.21.1 -pytest-codspeed==4.2.0 -# types -types-ujson ==5.10.0.20240515 -types-orjson ==3.6.2 diff --git a/requirements-translations.txt b/requirements-translations.txt deleted file mode 100644 index 76bfae167..000000000 --- a/requirements-translations.txt +++ /dev/null @@ -1,3 +0,0 @@ -pydantic-ai==0.4.10 -GitPython==3.1.45 -pygithub==2.8.1 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3bbab64a4..000000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ --e .[all] --r requirements-tests.txt --r requirements-docs.txt --r requirements-translations.txt -prek==0.2.22 -# For generating screenshots -playwright diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..9ae2220e0 --- /dev/null +++ b/uv.lock @@ -0,0 +1,5339 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", + "python_full_version < '3.10'", +] + +[[package]] +name = "ag-ui-protocol" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/bb/5a5ec893eea5805fb9a3db76a9888c3429710dfb6f24bbb37568f2cf7320/ag_ui_protocol-0.1.10.tar.gz", hash = "sha256:3213991c6b2eb24bb1a8c362ee270c16705a07a4c5962267a083d0959ed894f4", size = 6945, upload-time = "2025-11-06T15:17:17.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889, upload-time = "2025-11-06T15:17:15.325Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[package.optional-dependencies] +trio = [ + { name = "trio", version = "0.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "trio", version = "0.32.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "argon2-cffi-bindings", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/fa/57ec2c6d16ecd2ba0cf15f3c7d1c3c2e7b5fcb83555ff56d7ab10888ec8f/argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08", size = 42798, upload-time = "2023-08-15T14:13:12.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea", size = 15124, upload-time = "2023-08-15T14:13:10.752Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "argon2-cffi-bindings", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/ba4e4ca8d149f8dcc0d952ac0967089e1d759c7e5fcf0865a317eb680fbb/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e", size = 24549, upload-time = "2025-07-30T10:02:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/9b2386cc75ac0bd3210e12a44bfc7fd1632065ed8b80d573036eecb10442/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d", size = 25539, upload-time = "2025-07-30T10:02:00.929Z" }, + { url = "https://files.pythonhosted.org/packages/31/db/740de99a37aa727623730c90d92c22c9e12585b3c98c54b7960f7810289f/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584", size = 28467, upload-time = "2025-07-30T10:02:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/47c4509ea18d755f44e2b92b7178914f0c113946d11e16e626df8eaa2b0b/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690", size = 27355, upload-time = "2025-07-30T10:02:02.867Z" }, + { url = "https://files.pythonhosted.org/packages/ee/82/82745642d3c46e7cea25e1885b014b033f4693346ce46b7f47483cf5d448/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520", size = 29187, upload-time = "2025-07-30T10:02:03.674Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, +] + +[[package]] +name = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, + { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b6/ae7507470a4830dbbfe875c701e84a4a5fb9183d1497834871a715716a92/black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", size = 1628593, upload-time = "2025-01-29T05:37:23.672Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/ae36fa59a59f9363017ed397750a0cd79a470490860bc7713967d89cdd31/black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f", size = 1460000, upload-time = "2025-01-29T05:37:25.829Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b6/98f832e7a6c49aa3a464760c67c7856363aa644f2f3c74cf7d624168607e/black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", size = 1765963, upload-time = "2025-01-29T04:18:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e9/2cb0a017eb7024f70e0d2e9bdb8c5a5b078c5740c7f8816065d06f04c557/black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", size = 1419419, upload-time = "2025-01-29T04:18:30.191Z" }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "boto3" +version = "1.42.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/21/8be0e3685c3a4868be48d8d2f6e5b4641727e1d8a5d396b8b401d2b5f06e/boto3-1.42.24.tar.gz", hash = "sha256:c47a2f40df933e3861fc66fd8d6b87ee36d4361663a7e7ba39a87f5a78b2eae1", size = 112788, upload-time = "2026-01-07T20:30:51.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/75/bbfccb268f9faa4f59030888e859dca9797a980b77d6a074113af73bd4bf/boto3-1.42.24-py3-none-any.whl", hash = "sha256:8ed6ad670a5a2d7f66c1b0d3362791b48392c7a08f78479f5d8ab319a4d9118f", size = 140572, upload-time = "2026-01-07T20:30:49.431Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/d7/bb4a4e839b238ffb67b002d7326b328ebe5eb23ed5180f2ca10399a802de/botocore-1.42.24.tar.gz", hash = "sha256:be8d1bea64fb91eea08254a1e5fea057e4428d08e61f4e11083a02cafc1f8cc6", size = 14878455, upload-time = "2026-01-07T20:30:40.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/d4/f2655d777eed8b069ecab3761454cb83f830f8be8b5b0d292e4b3a980d00/botocore-1.42.24-py3-none-any.whl", hash = "sha256:8fca9781d7c84f7ad070fceffaff7179c4aa7a5ffb27b43df9d1d957801e0a8d", size = 14551806, upload-time = "2026-01-07T20:30:38.103Z" }, +] + +[[package]] +name = "cairocffi" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" }, +] + +[[package]] +name = "cairosvg" +version = "2.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cairocffi" }, + { name = "cssselect2" }, + { name = "defusedxml" }, + { name = "pillow" }, + { name = "tinycss2", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tinycss2", version = "1.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/5106168bd43d7cd8b7cc2a2ee465b385f14b63f4c092bb89eee2d48c8e67/cairosvg-2.8.2.tar.gz", hash = "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", size = 8398590, upload-time = "2025-05-15T06:56:32.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/48/816bd4aaae93dbf9e408c58598bc32f4a8c65f4b86ab560864cb3ee60adb/cairosvg-2.8.2-py3-none-any.whl", hash = "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5", size = 45773, upload-time = "2025-05-15T06:56:28.552Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cohere" +version = "5.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastavro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "tokenizers" }, + { name = "types-requests", version = "2.31.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "types-requests", version = "2.32.4.20260107", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/ed/bb02083654bdc089ae4ef1cd7691fd2233f1fd9f32bcbfacc80ff57d9775/cohere-5.20.1.tar.gz", hash = "sha256:50973f63d2c6138ff52ce37d8d6f78ccc539af4e8c43865e960d68e0bf835b6f", size = 180820, upload-time = "2025-12-18T16:39:50.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/e3/94eb11ac3ebaaa3a6afb5d2ff23db95d58bc468ae538c388edf49f2f20b5/cohere-5.20.1-py3-none-any.whl", hash = "sha256:d230fd13d95ba92ae927fce3dd497599b169883afc7954fe29b39fb8d5df5fc7", size = 318973, upload-time = "2025-12-18T16:39:49.504Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "cross-web" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/4f/bdb62e969649ee76d4741ef8eee34384ec2bc21cc66eb7fd244e6ad62be8/cross_web-0.4.0.tar.gz", hash = "sha256:4ae65619ddfcd06d6803432c0366342d7e8aeba10194b4e144d73a662e75370c", size = 157111, upload-time = "2025-12-25T20:45:21.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/d6/6c6a036655e5091b26b9f350dcf43821895325aa4727396b14c67679a957/cross_web-0.4.0-py3-none-any.whl", hash = "sha256:0c675bd26e91428cab31e3e927929b42da94aa96da92974e57c78f9a732d0e9b", size = 14200, upload-time = "2025-12-25T20:45:23.075Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "cssselect2" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tinycss2", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tinycss2", version = "1.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" }, +] + +[[package]] +name = "cyclic" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/9f/becc4fea44301f232e4eba17752001bd708e3c042fef37a72b9af7ddf4b5/cyclic-1.0.0.tar.gz", hash = "sha256:ecddd56cb831ee3e6b79f61ecb0ad71caee606c507136867782911aa01c3e5eb", size = 2167, upload-time = "2018-09-26T16:47:07.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/c0/9f59d2ebd9d585e1681c51767eb138bcd9d0ea770f6fc003cd875c7f5e62/cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed", size = 2547, upload-time = "2018-09-26T16:47:05.609Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "dirty-equals" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/99/133892f401ced5a27e641a473c547d5fbdb39af8f85dac8a9d633ea3e7a7/dirty_equals-0.9.0.tar.gz", hash = "sha256:17f515970b04ed7900b733c95fd8091f4f85e52f1fb5f268757f25c858eb1f7b", size = 50412, upload-time = "2025-01-11T23:23:40.491Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/0c/03cc99bf3b6328604b10829de3460f2b2ad3373200c45665c38508e550c6/dirty_equals-0.9.0-py3-none-any.whl", hash = "sha256:ff4d027f5cfa1b69573af00f7ba9043ea652dbdce3fe5cbe828e478c7346db9c", size = 28226, upload-time = "2025-01-11T23:23:37.489Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "dnspython", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "eval-type-backport" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/cafafb4558fd638aadfe4121dc6cefb8d743368c085acb2f521df0f3d9d7/eval_type_backport-0.3.1.tar.gz", hash = "sha256:57e993f7b5b69d271e37482e62f74e76a0276c82490cf8e4f0dffeb6b332d5ed", size = 9445, upload-time = "2025-12-02T11:51:42.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8", size = 6063, upload-time = "2025-12-02T11:51:41.665Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastapi" +source = { editable = "." } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +all = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "orjson" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "python-multipart", version = "0.0.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml" }, + { name = "ujson" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "python-multipart", version = "0.0.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] +standard-no-fastapi-cloud-cli = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard-no-fastapi-cloud-cli"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "python-multipart", version = "0.0.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "anyio", extra = ["trio"] }, + { name = "black" }, + { name = "cairosvg" }, + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "dirty-equals" }, + { name = "flask" }, + { name = "gitpython" }, + { name = "griffe-typingdoc" }, + { name = "griffe-warnings-deprecated" }, + { name = "httpx" }, + { name = "inline-snapshot" }, + { name = "jieba" }, + { name = "markdown-include-variants" }, + { name = "mdx-include" }, + { name = "mkdocs-macros-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocs-redirects" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "mypy" }, + { name = "pillow" }, + { name = "playwright" }, + { name = "prek" }, + { name = "pwdlib", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, extra = ["argon2"], marker = "python_full_version < '3.10'" }, + { name = "pwdlib", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, extra = ["argon2"], marker = "python_full_version >= '3.10'" }, + { name = "pydantic-ai" }, + { name = "pygithub" }, + { name = "pyjwt" }, + { name = "pytest" }, + { name = "pytest-codspeed" }, + { name = "python-slugify" }, + { name = "pyyaml" }, + { name = "ruff" }, + { name = "sqlmodel" }, + { name = "strawberry-graphql", version = "0.283.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "strawberry-graphql", version = "0.288.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typer" }, + { name = "types-orjson" }, + { name = "types-ujson" }, +] +docs = [ + { name = "black" }, + { name = "cairosvg" }, + { name = "griffe-typingdoc" }, + { name = "griffe-warnings-deprecated" }, + { name = "httpx" }, + { name = "jieba" }, + { name = "markdown-include-variants" }, + { name = "mdx-include" }, + { name = "mkdocs-macros-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocs-redirects" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "pillow" }, + { name = "python-slugify" }, + { name = "pyyaml" }, + { name = "ruff" }, + { name = "typer" }, +] +docs-tests = [ + { name = "httpx" }, + { name = "ruff" }, +] +github-actions = [ + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygithub" }, + { name = "pyyaml" }, + { name = "smokeshow" }, +] +tests = [ + { name = "anyio", extra = ["trio"] }, + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "dirty-equals" }, + { name = "flask" }, + { name = "httpx" }, + { name = "inline-snapshot" }, + { name = "mypy" }, + { name = "pwdlib", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, extra = ["argon2"], marker = "python_full_version < '3.10'" }, + { name = "pwdlib", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, extra = ["argon2"], marker = "python_full_version >= '3.10'" }, + { name = "pyjwt" }, + { name = "pytest" }, + { name = "pytest-codspeed" }, + { name = "pyyaml" }, + { name = "ruff" }, + { name = "sqlmodel" }, + { name = "strawberry-graphql", version = "0.283.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "strawberry-graphql", version = "0.288.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "types-orjson" }, + { name = "types-ujson" }, +] +translations = [ + { name = "gitpython" }, + { name = "pydantic-ai" }, + { name = "pygithub" }, +] + +[package.metadata] +requires-dist = [ + { name = "annotated-doc", specifier = ">=0.0.2" }, + { name = "email-validator", marker = "extra == 'all'", specifier = ">=2.0.0" }, + { name = "email-validator", marker = "extra == 'standard'", specifier = ">=2.0.0" }, + { name = "email-validator", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=2.0.0" }, + { name = "fastapi-cli", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.0.8" }, + { name = "fastapi-cli", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.0.8" }, + { name = "fastapi-cli", extras = ["standard-no-fastapi-cloud-cli"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.0.8" }, + { name = "httpx", marker = "extra == 'all'", specifier = ">=0.23.0,<1.0.0" }, + { name = "httpx", marker = "extra == 'standard'", specifier = ">=0.23.0,<1.0.0" }, + { name = "httpx", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.23.0,<1.0.0" }, + { name = "itsdangerous", marker = "extra == 'all'", specifier = ">=1.1.0" }, + { name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" }, + { name = "jinja2", marker = "extra == 'standard'", specifier = ">=3.1.5" }, + { name = "jinja2", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=3.1.5" }, + { name = "orjson", marker = "extra == 'all'", specifier = ">=3.2.1" }, + { name = "pydantic", specifier = ">=2.7.0" }, + { name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" }, + { name = "pydantic-extra-types", marker = "extra == 'standard'", specifier = ">=2.0.0" }, + { name = "pydantic-extra-types", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=2.0.0" }, + { name = "pydantic-settings", marker = "extra == 'all'", specifier = ">=2.0.0" }, + { name = "pydantic-settings", marker = "extra == 'standard'", specifier = ">=2.0.0" }, + { name = "pydantic-settings", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=2.0.0" }, + { name = "python-multipart", marker = "extra == 'all'", specifier = ">=0.0.18" }, + { name = "python-multipart", marker = "extra == 'standard'", specifier = ">=0.0.18" }, + { name = "python-multipart", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.0.18" }, + { name = "pyyaml", marker = "extra == 'all'", specifier = ">=5.3.1" }, + { name = "starlette", specifier = ">=0.40.0,<0.51.0" }, + { name = "typing-extensions", specifier = ">=4.8.0" }, + { name = "ujson", marker = "extra == 'all'", specifier = ">=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.12.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.12.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.12.0" }, +] +provides-extras = ["standard", "standard-no-fastapi-cloud-cli", "all"] + +[package.metadata.requires-dev] +dev = [ + { name = "anyio", extras = ["trio"], specifier = ">=3.2.1,<5.0.0" }, + { name = "black", specifier = "==25.1.0" }, + { name = "cairosvg", specifier = "==2.8.2" }, + { name = "coverage", extras = ["toml"], specifier = ">=6.5.0,<8.0" }, + { name = "dirty-equals", specifier = "==0.9.0" }, + { name = "flask", specifier = ">=1.1.2,<4.0.0" }, + { name = "gitpython", specifier = "==3.1.45" }, + { name = "griffe-typingdoc", specifier = "==0.3.0" }, + { name = "griffe-warnings-deprecated", specifier = "==1.1.0" }, + { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, + { name = "inline-snapshot", specifier = ">=0.21.1" }, + { name = "jieba", specifier = "==0.42.1" }, + { name = "markdown-include-variants", specifier = "==0.0.8" }, + { name = "mdx-include", specifier = ">=1.4.1,<2.0.0" }, + { name = "mkdocs-macros-plugin", specifier = "==1.4.1" }, + { name = "mkdocs-material", specifier = "==9.7.0" }, + { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = "==0.30.1" }, + { name = "mypy", specifier = "==1.14.1" }, + { name = "pillow", specifier = "==11.3.0" }, + { name = "playwright", specifier = ">=1.57.0" }, + { name = "prek", specifier = "==0.2.22" }, + { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" }, + { name = "pydantic-ai", specifier = "==0.4.10" }, + { name = "pygithub", specifier = "==2.8.1" }, + { name = "pyjwt", specifier = "==2.9.0" }, + { name = "pytest", specifier = ">=7.1.3,<9.0.0" }, + { name = "pytest-codspeed", specifier = "==4.2.0" }, + { name = "python-slugify", specifier = "==8.0.4" }, + { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, + { name = "ruff", specifier = "==0.14.3" }, + { name = "sqlmodel", specifier = "==0.0.27" }, + { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, + { name = "typer", specifier = "==0.16.0" }, + { name = "types-orjson", specifier = "==3.6.2" }, + { name = "types-ujson", specifier = "==5.10.0.20240515" }, +] +docs = [ + { name = "black", specifier = "==25.1.0" }, + { name = "cairosvg", specifier = "==2.8.2" }, + { name = "griffe-typingdoc", specifier = "==0.3.0" }, + { name = "griffe-warnings-deprecated", specifier = "==1.1.0" }, + { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, + { name = "jieba", specifier = "==0.42.1" }, + { name = "markdown-include-variants", specifier = "==0.0.8" }, + { name = "mdx-include", specifier = ">=1.4.1,<2.0.0" }, + { name = "mkdocs-macros-plugin", specifier = "==1.4.1" }, + { name = "mkdocs-material", specifier = "==9.7.0" }, + { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = "==0.30.1" }, + { name = "pillow", specifier = "==11.3.0" }, + { name = "python-slugify", specifier = "==8.0.4" }, + { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, + { name = "ruff", specifier = "==0.14.3" }, + { name = "typer", specifier = "==0.16.0" }, +] +docs-tests = [ + { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, + { name = "ruff", specifier = "==0.14.3" }, +] +github-actions = [ + { name = "httpx", specifier = ">=0.27.0,<1.0.0" }, + { name = "pydantic", specifier = ">=2.5.3,<3.0.0" }, + { name = "pydantic-settings", specifier = ">=2.1.0,<3.0.0" }, + { name = "pygithub", specifier = ">=2.3.0,<3.0.0" }, + { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, + { name = "smokeshow", specifier = ">=0.5.0" }, +] +tests = [ + { name = "anyio", extras = ["trio"], specifier = ">=3.2.1,<5.0.0" }, + { name = "coverage", extras = ["toml"], specifier = ">=6.5.0,<8.0" }, + { name = "dirty-equals", specifier = "==0.9.0" }, + { name = "flask", specifier = ">=1.1.2,<4.0.0" }, + { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, + { name = "inline-snapshot", specifier = ">=0.21.1" }, + { name = "mypy", specifier = "==1.14.1" }, + { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" }, + { name = "pyjwt", specifier = "==2.9.0" }, + { name = "pytest", specifier = ">=7.1.3,<9.0.0" }, + { name = "pytest-codspeed", specifier = "==4.2.0" }, + { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, + { name = "ruff", specifier = "==0.14.3" }, + { name = "sqlmodel", specifier = "==0.0.27" }, + { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, + { name = "types-orjson", specifier = "==3.6.2" }, + { name = "types-ujson", specifier = "==5.10.0.20240515" }, +] +translations = [ + { name = "gitpython", specifier = "==3.1.45" }, + { name = "pydantic-ai", specifier = "==0.4.10" }, + { name = "pygithub", specifier = "==2.8.1" }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typer" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/ca/d90fb3bfbcbd6e56c77afd9d114dd6ce8955d8bb90094399d1c70e659e40/fastapi_cli-0.0.20.tar.gz", hash = "sha256:d17c2634f7b96b6b560bc16b0035ed047d523c912011395f49f00a421692bc3a", size = 19786, upload-time = "2025-12-22T17:13:33.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/89/5c4eef60524d0fd704eb0706885b82cd5623a43396b94e4a5b17d3a3f516/fastapi_cli-0.0.20-py3-none-any.whl", hash = "sha256:e58b6a0038c0b1532b7a0af690656093dee666201b6b19d3c87175b358e9f783", size = 12390, upload-time = "2025-12-22T17:13:31.708Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] +standard-no-fastapi-cloud-cli = [ + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/5d/3b33438de35521fab4968b232caa9a4bd568a5078f2b2dfb7bb8a4528603/fastapi_cloud_cli-0.8.0.tar.gz", hash = "sha256:cf07c502528bfd9e6b184776659f05d9212811d76bbec9fbb6bf34bed4c7456f", size = 30257, upload-time = "2025-12-23T12:08:33.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/8e/abb95ef59e91bb5adaa2d18fbf9ea70fd524010bb03f406a2dd2a4775ef9/fastapi_cloud_cli-0.8.0-py3-none-any.whl", hash = "sha256:e9f40bee671d985fd25d7a5409b56d4f103777bf8a0c6d746ea5fbf97a8186d9", size = 22306, upload-time = "2025-12-23T12:08:32.68Z" }, +] + +[[package]] +name = "fastar" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e7/f89d54fb04104114dd0552836dc2b47914f416cc0e200b409dd04a33de5e/fastar-0.8.0.tar.gz", hash = "sha256:f4d4d68dbf1c4c2808f0e730fac5843493fc849f70fe3ad3af60dfbaf68b9a12", size = 68524, upload-time = "2025-11-26T02:36:00.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/e2/51d9ee443aabcd5aa581d45b18b6198ced364b5cd97e5504c5d782ceb82c/fastar-0.8.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c9f930cff014cf79d396d0541bd9f3a3f170c9b5e45d10d634d98f9ed08788c3", size = 708536, upload-time = "2025-11-26T02:34:35.236Z" }, + { url = "https://files.pythonhosted.org/packages/07/2a/edfc6274768b8a3859a5ca4f8c29cb7f614d7f27d2378e2c88aa91cda54e/fastar-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07b70f712d20622346531a4b46bb332569bea621f61314c0b7e80903a16d14cf", size = 632235, upload-time = "2025-11-26T02:34:19.367Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1e/3cfbaaec464caef196700ee2ffae1c03f94f7c5e2a85d0ec0ea9cdd1da81/fastar-0.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:330639db3bfba4c6d132421a2a4aeb81e7bea8ce9159cdb6e247fbc5fae97686", size = 871386, upload-time = "2025-11-26T02:33:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/82/50/224a674ad541054179e4e6e0b54bb6e162f04f698a2512b42a8085fc6b6f/fastar-0.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ea7ceb6231e48d7bb0d7dc13e946baa29c7f6873eaf4afb69725d6da349033", size = 764955, upload-time = "2025-11-26T02:32:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/4d/5e/4608184aa57cb6a54f62c1eb3e5133ba8d461fc7f13193c0255effbec12a/fastar-0.8.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a90695a601a78bbca910fdf2efcdf3103c55d0de5a5c6e93556d707bf886250b", size = 765987, upload-time = "2025-11-26T02:32:59.701Z" }, + { url = "https://files.pythonhosted.org/packages/e0/53/6afd2b680dddfa10df9a16bbcf6cabfee0d92435d5c7e3f4cfe3b1712662/fastar-0.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d0bf655ff4c9320b0ca8a5b128063d5093c0c8c1645a2b5f7167143fd8531aa", size = 930900, upload-time = "2025-11-26T02:33:16.059Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1e/b7a304bfcc1d06845cbfa4b464516f6fff9c8c6692f6ef80a3a86b04e199/fastar-0.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8df22cdd8d58e7689aa89b2e4a07e8e5fa4f88d2d9c2621f0e88a49be97ccea", size = 821523, upload-time = "2025-11-26T02:33:30.897Z" }, + { url = "https://files.pythonhosted.org/packages/1d/da/9ef8605c6d233cd6ca3a95f7f518ac22aa064903afe6afa57733bfb7c31b/fastar-0.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8a5e6ad722685128521c8fb44cf25bd38669650ba3a4b466b8903e5aa28e1a0", size = 821268, upload-time = "2025-11-26T02:34:04.003Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/ed37c78a6b4420de1677d82e79742787975c34847229c33dc376334c7283/fastar-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:31cd541231a2456e32104da891cf9962c3b40234d0465cbf9322a6bc8a1b05d5", size = 986286, upload-time = "2025-11-26T02:34:50.279Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a6/366b15f432d85d4089e6e4b52a09cc2a2bcf4d7a1f0771e3d3194deccb1e/fastar-0.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:175db2a98d67ced106468e8987975484f8bbbd5ad99201da823b38bafb565ed5", size = 1041921, upload-time = "2025-11-26T02:35:07.292Z" }, + { url = "https://files.pythonhosted.org/packages/f4/45/45f8e6991e3ce9f8aeefdc8d4c200daada41097a36808643d1703464c3e2/fastar-0.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada877ab1c65197d772ce1b1c2e244d4799680d8b3f136a4308360f3d8661b23", size = 1047302, upload-time = "2025-11-26T02:35:24.995Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/a587796111a3cd4b78cd61ec3fc1252d8517d81f763f4164ed5680f84810/fastar-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:01084cb75f13ca6a8e80bd41584322523189f8e81b472053743d6e6c3062b5a6", size = 995141, upload-time = "2025-11-26T02:35:42.449Z" }, + { url = "https://files.pythonhosted.org/packages/89/c0/7a8ec86695b0b77168e220cf2af1aa30592f5ecdbd0ce6d641d29c4a8bae/fastar-0.8.0-cp310-cp310-win32.whl", hash = "sha256:ca639b9909805e44364ea13cca2682b487e74826e4ad75957115ec693228d6b6", size = 456544, upload-time = "2025-11-26T02:36:23.801Z" }, + { url = "https://files.pythonhosted.org/packages/be/a9/8da4deb840121c59deabd939ce2dca3d6beec85576f3743d1144441938b5/fastar-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:fbc0f2ed0f4add7fb58034c576584d44d7eaaf93dee721dfb26dbed6e222dbac", size = 490701, upload-time = "2025-11-26T02:36:09.625Z" }, + { url = "https://files.pythonhosted.org/packages/cd/15/1c764530b81b266f6d27d78d49b6bef22a73b3300cd83a280bfd244908c5/fastar-0.8.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cd9c0d3ebf7a0a6f642f771cf41b79f7c98d40a3072a8abe1174fbd9bd615bd3", size = 708427, upload-time = "2025-11-26T02:34:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/41/fc/75d42c008516543219e4293e4d8ac55da57a5c63147484f10468bd1bc24e/fastar-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2875a077340fe4f8099bd3ed8fa90d9595e1ac3cd62ae19ab690d5bf550eeb35", size = 631740, upload-time = "2025-11-26T02:34:20.718Z" }, + { url = "https://files.pythonhosted.org/packages/50/8d/9632984f7824ed2210157dcebd8e9821ef6d4f2b28510d0516db6625ff9b/fastar-0.8.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a999263d9f87184bf2801833b2ecf105e03c0dd91cac78685673b70da564fd64", size = 871628, upload-time = "2025-11-26T02:33:49.279Z" }, + { url = "https://files.pythonhosted.org/packages/05/97/3eb6ea71b7544d45cd29cacb764ca23cde8ce0aed1a6a02251caa4c0a818/fastar-0.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c41111da56430f638cbfc498ebdcc7d30f63416e904b27b7695c29bd4889cb8", size = 765005, upload-time = "2025-11-26T02:32:45.833Z" }, + { url = "https://files.pythonhosted.org/packages/d6/45/3eb0ee945a0b5d5f9df7e7c25c037ce7fa441cd0b4d44f76d286e2f4396a/fastar-0.8.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3719541a12bb09ab1eae91d2c987a9b2b7d7149c52e7109ba6e15b74aabc49b1", size = 765587, upload-time = "2025-11-26T02:33:01.174Z" }, + { url = "https://files.pythonhosted.org/packages/51/bb/7defd6ec0d9570b1987d8ebde52d07d97f3f26e10b592fb3e12738eba39a/fastar-0.8.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a9b0fff8079b18acdface7ef1b7f522fd9a589f65ca4a1a0dd7c92a0886c2a2", size = 931150, upload-time = "2025-11-26T02:33:17.374Z" }, + { url = "https://files.pythonhosted.org/packages/28/54/62e51e684dab347c61878afbf09e177029c1a91eb1e39ef244e6b3ef9efa/fastar-0.8.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac073576c1931959191cb20df38bab21dd152f66c940aa3ca8b22e39f753b2f3", size = 821354, upload-time = "2025-11-26T02:33:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/53/a8/12708ea4d21e3cf9f485b2a67d44ce84d949a6eddcc9aa5b3d324585ab43/fastar-0.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003b59a7c3e405b6a7bff8fab17d31e0ccbc7f06730a8f8ca1694eeea75f3c76", size = 821626, upload-time = "2025-11-26T02:34:05.685Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/1b4d3347c7a759853f963410bf6baf42fe014d587c50c39c8e145f4bf1a0/fastar-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a7b96748425efd9fc155cd920d65088a1b0d754421962418ea73413d02ff515a", size = 986187, upload-time = "2025-11-26T02:34:52.047Z" }, + { url = "https://files.pythonhosted.org/packages/dc/59/2dbe0dc2570764475e60030403738faa261a9d3bff16b08629c378ab939a/fastar-0.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:90957a30e64418b02df5b4d525bea50403d98a4b1f29143ce5914ddfa7e54ee4", size = 1041536, upload-time = "2025-11-26T02:35:08.926Z" }, + { url = "https://files.pythonhosted.org/packages/d9/0f/639b295669c7ca6fbc2b4be2a7832aaeac1a5e06923f15a8a6d6daecbc7d/fastar-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f6e784a8015623fbb7ccca1af372fd82cb511b408ddd2348dc929fc6e415df73", size = 1047149, upload-time = "2025-11-26T02:35:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/23e3a19e06d261d1894f98eca9458f98c090c505a0c712dafc0ff1fc2965/fastar-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a03eaf287bbc93064688a1220580ce261e7557c8898f687f4d0b281c85b28d3c", size = 994992, upload-time = "2025-11-26T02:35:44.009Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7a/3ea4726bae3ac9358d02107ae48f3e10ee186dbed554af79e00b7b498c44/fastar-0.8.0-cp311-cp311-win32.whl", hash = "sha256:661a47ed90762f419406c47e802f46af63a08254ba96abd1c8191e4ce967b665", size = 456449, upload-time = "2025-11-26T02:36:25.291Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3c/0142bee993c431ee91cf5535e6e4b079ad491f620c215fcd79b7e5ffeb2b/fastar-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b48abd6056fef7bc3d414aafb453c5b07fdf06d2df5a2841d650288a3aa1e9d3", size = 490863, upload-time = "2025-11-26T02:36:11.114Z" }, + { url = "https://files.pythonhosted.org/packages/3b/18/d119944f6bdbf6e722e204e36db86390ea45684a1bf6be6e3aa42abd471f/fastar-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:50c18788b3c6ffb85e176dcb8548bb8e54616a0519dcdbbfba66f6bbc4316933", size = 462230, upload-time = "2025-11-26T02:36:01.917Z" }, + { url = "https://files.pythonhosted.org/packages/58/f1/5b2ff898abac7f1a418284aad285e3a4f68d189c572ab2db0f6c9079dd16/fastar-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f10d2adfe40f47ff228f4efaa32d409d732ded98580e03ed37c9535b5fc923d", size = 706369, upload-time = "2025-11-26T02:34:37.783Z" }, + { url = "https://files.pythonhosted.org/packages/23/60/8046a386dca39154f80c927cbbeeb4b1c1267a3271bffe61552eb9995757/fastar-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b930da9d598e3bc69513d131f397e6d6be4643926ef3de5d33d1e826631eb036", size = 629097, upload-time = "2025-11-26T02:34:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/22/7e/1ae005addc789924a9268da2394d3bb5c6f96836f7e37b7e3d23c2362675/fastar-0.8.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d210da2de733ca801de83e931012349d209f38b92d9630ccaa94bd445bdc9b8", size = 868938, upload-time = "2025-11-26T02:33:51.119Z" }, + { url = "https://files.pythonhosted.org/packages/a6/77/290a892b073b84bf82e6b2259708dfe79c54f356e252c2dd40180b16fe07/fastar-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa02270721517078a5bd61a38719070ac2537a4aa6b6c48cf369cf2abc59174a", size = 765204, upload-time = "2025-11-26T02:32:47.02Z" }, + { url = "https://files.pythonhosted.org/packages/d0/00/c3155171b976003af3281f5258189f1935b15d1221bfc7467b478c631216/fastar-0.8.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83c391e5b789a720e4d0029b9559f5d6dee3226693c5b39c0eab8eaece997e0f", size = 764717, upload-time = "2025-11-26T02:33:02.453Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/405b7ad76207b2c11b7b59335b70eac19e4a2653977f5588a1ac8fed54f4/fastar-0.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3258d7a78a72793cdd081545da61cabe85b1f37634a1d0b97ffee0ff11d105ef", size = 931502, upload-time = "2025-11-26T02:33:18.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/8a/a3dde6d37cc3da4453f2845cdf16675b5686b73b164f37e2cc579b057c2c/fastar-0.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6eab95dd985cdb6a50666cbeb9e4814676e59cfe52039c880b69d67cfd44767", size = 821454, upload-time = "2025-11-26T02:33:33.427Z" }, + { url = "https://files.pythonhosted.org/packages/da/c1/904fe2468609c8990dce9fe654df3fbc7324a8d8e80d8240ae2c89757064/fastar-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:829b1854166141860887273c116c94e31357213fa8e9fe8baeb18bd6c38aa8d9", size = 821647, upload-time = "2025-11-26T02:34:07Z" }, + { url = "https://files.pythonhosted.org/packages/c8/73/a0642ab7a400bc07528091785e868ace598fde06fcd139b8f865ec1b6f3c/fastar-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1667eae13f9457a3c737f4376d68e8c3e548353538b28f7e4273a30cb3965cd", size = 986342, upload-time = "2025-11-26T02:34:53.371Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/60c1bfa6edab72366461a95f053d0f5f7ab1825fe65ca2ca367432cd8629/fastar-0.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b864a95229a7db0814cd9ef7987cb713fd43dce1b0d809dd17d9cd6f02fdde3e", size = 1040207, upload-time = "2025-11-26T02:35:10.65Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a0/0d624290dec622e7fa084b6881f456809f68777d54a314f5dde932714506/fastar-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c05fbc5618ce17675a42576fa49858d79734627f0a0c74c0875ab45ee8de340c", size = 1045031, upload-time = "2025-11-26T02:35:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/a7/74/cf663af53c4706ba88e6b4af44a6b0c3bd7d7ca09f079dc40647a8f06585/fastar-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7f41c51ee96f338662ee3c3df4840511ba3f9969606840f1b10b7cb633a3c716", size = 994877, upload-time = "2025-11-26T02:35:45.797Z" }, + { url = "https://files.pythonhosted.org/packages/52/17/444c8be6e77206050e350da7c338102b6cab384be937fa0b1d6d1f9ede73/fastar-0.8.0-cp312-cp312-win32.whl", hash = "sha256:d949a1a2ea7968b734632c009df0571c94636a5e1622c87a6e2bf712a7334f47", size = 455996, upload-time = "2025-11-26T02:36:26.938Z" }, + { url = "https://files.pythonhosted.org/packages/dc/34/fc3b5e56d71a17b1904800003d9251716e8fd65f662e1b10a26881698a74/fastar-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc645994d5b927d769121094e8a649b09923b3c13a8b0b98696d8f853f23c532", size = 490429, upload-time = "2025-11-26T02:36:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/35/a8/5608cc837417107c594e2e7be850b9365bcb05e99645966a5d6a156285fe/fastar-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:d81ee82e8dc78a0adb81728383bd39611177d642a8fa2d601d4ad5ad59e5f3bd", size = 461297, upload-time = "2025-11-26T02:36:03.546Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a5/79ecba3646e22d03eef1a66fb7fc156567213e2e4ab9faab3bbd4489e483/fastar-0.8.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a3253a06845462ca2196024c7a18f5c0ba4de1532ab1c4bad23a40b332a06a6a", size = 706112, upload-time = "2025-11-26T02:34:39.237Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/4f883bce878218a8676c2d7ca09b50c856a5470bb3b7f63baf9521ea6995/fastar-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5cbeb3ebfa0980c68ff8b126295cc6b208ccd81b638aebc5a723d810a7a0e5d2", size = 628954, upload-time = "2025-11-26T02:34:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f1/892e471f156b03d10ba48ace9384f5a896702a54506137462545f38e40b8/fastar-0.8.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1c0d5956b917daac77d333d48b3f0f3ff927b8039d5b32d8125462782369f761", size = 868685, upload-time = "2025-11-26T02:33:53.077Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/e24915045852e30014ec6840446975c03f4234d1c9270394b51d3ad18394/fastar-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27b404db2b786b65912927ce7f3790964a4bcbde42cdd13091b82a89cd655e1c", size = 765044, upload-time = "2025-11-26T02:32:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/1aa11ac21a99984864c2fca4994e094319ff3a2046e7a0343c39317bd5b9/fastar-0.8.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0902fc89dcf1e7f07b8563032a4159fe2b835e4c16942c76fd63451d0e5f76a3", size = 764322, upload-time = "2025-11-26T02:33:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f0/4b91902af39fe2d3bae7c85c6d789586b9fbcf618d7fdb3d37323915906d/fastar-0.8.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:069347e2f0f7a8b99bbac8cd1bc0e06c7b4a31dc964fc60d84b95eab3d869dc1", size = 931016, upload-time = "2025-11-26T02:33:19.902Z" }, + { url = "https://files.pythonhosted.org/packages/c9/97/8fc43a5a9c0a2dc195730f6f7a0f367d171282cd8be2511d0e87c6d2dad0/fastar-0.8.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd135306f6bfe9a835918280e0eb440b70ab303e0187d90ab51ca86e143f70d", size = 821308, upload-time = "2025-11-26T02:33:34.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e9/058615b63a7fd27965e8c5966f393ed0c169f7ff5012e1674f21684de3ba/fastar-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d06d6897f43c27154b5f2d0eb930a43a81b7eec73f6f0b0114814d4a10ab38", size = 821171, upload-time = "2025-11-26T02:34:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cf/69e16a17961570a755c37ffb5b5aa7610d2e77807625f537989da66f2a9d/fastar-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a922f8439231fa0c32b15e8d70ff6d415619b9d40492029dabbc14a0c53b5f18", size = 986227, upload-time = "2025-11-26T02:34:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/fb/83/2100192372e59b56f4ace37d7d9cabda511afd71b5febad1643d1c334271/fastar-0.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a739abd51eb766384b4caff83050888e80cd75bbcfec61e6d1e64875f94e4a40", size = 1039395, upload-time = "2025-11-26T02:35:12.166Z" }, + { url = "https://files.pythonhosted.org/packages/75/15/cdd03aca972f55872efbb7cf7540c3fa7b97a75d626303a3ea46932163dc/fastar-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5a65f419d808b23ac89d5cd1b13a2f340f15bc5d1d9af79f39fdb77bba48ff1b", size = 1044766, upload-time = "2025-11-26T02:35:29.62Z" }, + { url = "https://files.pythonhosted.org/packages/3d/29/945e69e4e2652329ace545999334ec31f1431fbae3abb0105587e11af2ae/fastar-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7bb2ae6c0cce58f0db1c9f20495e7557cca2c1ee9c69bbd90eafd54f139171c5", size = 994740, upload-time = "2025-11-26T02:35:47.887Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5d/dbfe28f8cd1eb484bba0c62e5259b2cf6fea229d6ef43e05c06b5a78c034/fastar-0.8.0-cp313-cp313-win32.whl", hash = "sha256:b28753e0d18a643272597cb16d39f1053842aa43131ad3e260c03a2417d38401", size = 455990, upload-time = "2025-11-26T02:36:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/e1/01/e965740bd36e60ef4c5aa2cbe42b6c4eb1dc3551009238a97c2e5e96bd23/fastar-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:620e5d737dce8321d49a5ebb7997f1fd0047cde3512082c27dc66d6ac8c1927a", size = 490227, upload-time = "2025-11-26T02:36:14.363Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/c99202719b83e5249f26902ae53a05aea67d840eeb242019322f20fc171c/fastar-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:c4c4bd08df563120cd33e854fe0a93b81579e8571b11f9b7da9e84c37da2d6b6", size = 461078, upload-time = "2025-11-26T02:36:04.94Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9573b87a0ef07580ed111e7230259aec31bb33ca3667963ebee77022ec61/fastar-0.8.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:50b36ce654ba44b0e13fae607ae17ee6e1597b69f71df1bee64bb8328d881dfc", size = 706041, upload-time = "2025-11-26T02:34:40.638Z" }, + { url = "https://files.pythonhosted.org/packages/4a/19/f95444a1d4f375333af49300aa75ee93afa3335c0e40fda528e460ed859c/fastar-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63a892762683d7ab00df0227d5ea9677c62ff2cde9b875e666c0be569ed940f3", size = 628617, upload-time = "2025-11-26T02:34:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c9/b51481b38b7e3f16ef2b9e233b1a3623386c939d745d6e41bbd389eaae30/fastar-0.8.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ae6a145c1bff592644bde13f2115e0239f4b7babaf506d14e7d208483cf01a5", size = 869299, upload-time = "2025-11-26T02:33:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/bf/02/3ba1267ee5ba7314e29c431cf82eaa68586f2c40cdfa08be3632b7d07619/fastar-0.8.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae0ff7c0a1c7e1428404b81faee8aebef466bfd0be25bfe4dabf5d535c68741", size = 764667, upload-time = "2025-11-26T02:32:49.606Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/bf33530fd015b5d7c2cc69e0bce4a38d736754a6955487005aab1af6adcd/fastar-0.8.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbfd87dbd217b45c898b2dbcd0169aae534b2c1c5cbe3119510881f6a5ac8ef5", size = 763993, upload-time = "2025-11-26T02:33:05.782Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/9564d24e7cea6321a8d921c6d2a457044a476ef197aa4708e179d3d97f0d/fastar-0.8.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5abd99fcba83ef28c8fe6ae2927edc79053db43a0457a962ed85c9bf150d37", size = 930153, upload-time = "2025-11-26T02:33:21.53Z" }, + { url = "https://files.pythonhosted.org/packages/35/b1/6f57fcd8d6e192cfebf97e58eb27751640ad93784c857b79039e84387b51/fastar-0.8.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91d4c685620c3a9d6b5ae091dbabab4f98b20049b7ecc7976e19cc9016c0d5d6", size = 821177, upload-time = "2025-11-26T02:33:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b3/78/9e004ea9f3aa7466f5ddb6f9518780e1d2f0ed3ca55f093632982598bace/fastar-0.8.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f77c2f2cad76e9dc7b6701297adb1eba87d0485944b416fc2ccf5516c01219a3", size = 820652, upload-time = "2025-11-26T02:34:09.776Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/b604ed536544005c9f1aee7c4c74b00150db3d8d535cd8232dc20f947063/fastar-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e7f07c4a3dada7757a8fc430a5b4a29e6ef696d2212747213f57086ffd970316", size = 985961, upload-time = "2025-11-26T02:34:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7b/fa9d4d96a5d494bdb8699363bb9de8178c0c21a02e1d89cd6f913d127018/fastar-0.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:90c0c3fe55105c0aed8a83135dbdeb31e683455dbd326a1c48fa44c378b85616", size = 1039316, upload-time = "2025-11-26T02:35:13.807Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f9/8462789243bc3f33e8401378ec6d54de4e20cfa60c96a0e15e3e9d1389bb/fastar-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fb9ee51e5bffe0dab3d3126d3a4fac8d8f7235cedcb4b8e74936087ce1c157f3", size = 1045028, upload-time = "2025-11-26T02:35:31.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/71/9abb128777e616127194b509e98fcda3db797d76288c1a8c23dd22afc14f/fastar-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e380b1e8d30317f52406c43b11e98d11e1d68723bbd031e18049ea3497b59a6d", size = 994677, upload-time = "2025-11-26T02:35:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/b81b3f194853d7ad232a67a1d768f5f51a016f165cfb56cb31b31bbc6177/fastar-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1c4ffc06e9c4a8ca498c07e094670d8d8c0d25b17ca6465b9774da44ea997ab1", size = 456687, upload-time = "2025-11-26T02:36:30.205Z" }, + { url = "https://files.pythonhosted.org/packages/cb/87/9e0cd4768a98181d56f0cdbab2363404cc15deb93f4aad3b99cd2761bbaa/fastar-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:5517a8ad4726267c57a3e0e2a44430b782e00b230bf51c55b5728e758bb3a692", size = 490578, upload-time = "2025-11-26T02:36:16.218Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/580a76cf91847654f2ad6520e956e93218f778540975bc4190d363f709e2/fastar-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:58030551046ff4a8616931e52a36c83545ff05996db5beb6e0cd2b7e748aa309", size = 461473, upload-time = "2025-11-26T02:36:06.373Z" }, + { url = "https://files.pythonhosted.org/packages/58/4c/bdb5c6efe934f68708529c8c9d4055ebef5c4be370621966438f658b29bd/fastar-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:1e7d29b6bfecb29db126a08baf3c04a5ab667f6cea2b7067d3e623a67729c4a6", size = 705570, upload-time = "2025-11-26T02:34:42.01Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/f01ac7e71d5a37621bd13598a26e948a12b85ca8042f7ee1a0a8c9f59cda/fastar-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05eb7b96940f9526b485f1d0b02393839f0f61cac4b1f60024984f8b326d2640", size = 627761, upload-time = "2025-11-26T02:34:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/06/45/6df0ecda86ea9d2e95053c1a655d153dee55fc121b6e13ea6d1e246a50b6/fastar-0.8.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:619352d8ac011794e2345c462189dc02ba634750d23cd9d86a9267dd71b1f278", size = 869414, upload-time = "2025-11-26T02:33:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b2/72/486421f5a8c0c377cc82e7a50c8a8ea899a6ec2aa72bde8f09fb667a2dc8/fastar-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74ebfecef3fe6d7a90355fac1402fd30636988332a1d33f3e80019a10782bb24", size = 763863, upload-time = "2025-11-26T02:32:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/39f654dbb41a3867fb1f2c8081c014d8f1d32ea10585d84cacbef0b32995/fastar-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2975aca5a639e26a3ab0d23b4b0628d6dd6d521146c3c11486d782be621a35aa", size = 763065, upload-time = "2025-11-26T02:33:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bd/c011a34fb3534c4c3301f7c87c4ffd7e47f6113c904c092ddc8a59a303ea/fastar-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afc438eaed8ff0dcdd9308268be5cb38c1db7e94c3ccca7c498ca13a4a4535a3", size = 930530, upload-time = "2025-11-26T02:33:23.117Z" }, + { url = "https://files.pythonhosted.org/packages/55/9d/aa6e887a7033c571b1064429222bbe09adc9a3c1e04f3d1788ba5838ebd5/fastar-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ced0a5399cc0a84a858ef0a31ca2d0c24d3bbec4bcda506a9192d8119f3590a", size = 820572, upload-time = "2025-11-26T02:33:37.542Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9c/7a3a2278a1052e1a5d98646de7c095a00cffd2492b3b84ce730e2f1cd93a/fastar-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec9b23da8c4c039da3fe2e358973c66976a0c8508aa06d6626b4403cb5666c19", size = 820649, upload-time = "2025-11-26T02:34:11.108Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d38edc1f4438cd047e56137c26d94783ffade42e1b3bde620ccf17b771ef/fastar-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dfba078fcd53478032fd0ceed56960ec6b7ff0511cfc013a8a3a4307e3a7bac4", size = 985653, upload-time = "2025-11-26T02:34:57.884Z" }, + { url = "https://files.pythonhosted.org/packages/69/d9/2147d0c19757e165cd62d41cec3f7b38fad2ad68ab784978b5f81716c7ea/fastar-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ade56c94c14be356d295fecb47a3fcd473dd43a8803ead2e2b5b9e58feb6dcfa", size = 1038140, upload-time = "2025-11-26T02:35:15.778Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/ec4c717ffb8a308871e9602ec3197d957e238dc0227127ac573ec9bca952/fastar-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e48d938f9366db5e59441728f70b7f6c1ccfab7eff84f96f9b7e689b07786c52", size = 1045195, upload-time = "2025-11-26T02:35:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/637334dc8c8f3bb391388b064ae13f0ad9402bc5a6c3e77b8887d0c31921/fastar-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:79c441dc1482ff51a54fb3f57ae6f7bb3d2cff88fa2cc5d196c519f8aab64a56", size = 994686, upload-time = "2025-11-26T02:35:51.392Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e2/dfa19a4b260b8ab3581b7484dcb80c09b25324f4daa6b6ae1c7640d1607a/fastar-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:187f61dc739afe45ac8e47ed7fd1adc45d52eac110cf27d579155720507d6fbe", size = 455767, upload-time = "2025-11-26T02:36:34.758Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/df65c72afc1297797b255f90c4778b5d6f1f0f80282a134d5ab610310ed9/fastar-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:40e9d763cf8bf85ce2fa256e010aa795c0fe3d3bd1326d5c3084e6ce7857127e", size = 489971, upload-time = "2025-11-26T02:36:22.081Z" }, + { url = "https://files.pythonhosted.org/packages/85/11/0aa8455af26f0ae89e42be67f3a874255ee5d7f0f026fc86e8d56f76b428/fastar-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e59673307b6a08210987059a2bdea2614fe26e3335d0e5d1a3d95f49a05b1418", size = 460467, upload-time = "2025-11-26T02:36:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0d/462ce073b1f87cc6bf33b656c340c103eeb67487d118b761a697f1ee454f/fastar-0.8.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:923afc2db5192e56e71952a88e3fe5965c7c9c910d385d2db7573136f064f2fa", size = 709519, upload-time = "2025-11-26T02:34:44.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f1/ceca1e04fd3266f866efdbb5d343fee1d8ff8fe94c64c8d1aab68b483ad0/fastar-0.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4fbe775356930f3aab0ce709fdf8ecf90c10882f5bbdcea215c89a3b14090c50", size = 632509, upload-time = "2025-11-26T02:34:29.428Z" }, + { url = "https://files.pythonhosted.org/packages/4f/db/fdb07e9dce80ebb38138f166a30dc482c82cc8dfcfda1024e8b100a53d1c/fastar-0.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2ff516154e77f4bf78c31a0c11aa78a8a80e11b6964ec6f28982e42ffcbb543c", size = 871400, upload-time = "2025-11-26T02:33:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/54/6d/d0a342528002cf7527339fd275bdee5e5cc90c2f193e8418d3510ffedcd7/fastar-0.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d2fdd1c987ff2300bdf39baed556f8e155f8577018775e794a268ecf1707610", size = 765781, upload-time = "2025-11-26T02:32:53.929Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/dd2721e7160eed7bc9c3b695e942868c7ba2fab41947278fa380c18569be/fastar-0.8.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d80e4dad8ee2362a71870b1e735800bb5e97f12ebbee4bd0cf15a81ad2428b5a", size = 766126, upload-time = "2025-11-26T02:33:10.075Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ac/6533d2608254def722cd8c852f4ac8fb413fc43ad3251b7fb99863ffaa83/fastar-0.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a17abee1febf5363ed2633f5e13de4be481ba1ab5f77860d39470eccdc4b65af", size = 932549, upload-time = "2025-11-26T02:33:25.866Z" }, + { url = "https://files.pythonhosted.org/packages/97/dc/aac042d0d0017b8d75ff34381cc28b482ace0d78a26d4eef9a8674f850c2/fastar-0.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64cbde8e0ece3d799090a4727f936f66c5990d3ac59416f3de76a2c676e8e568", size = 821860, upload-time = "2025-11-26T02:33:41.423Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ef/c31057572e3a5a2c90da4986d8594d0ff33097b4a44058f9f52103c33677/fastar-0.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63d98b26590d293a9d9a379bae88367a8f3a6137c28819ed6dd6e11aca4a5c6e", size = 821660, upload-time = "2025-11-26T02:34:13.84Z" }, + { url = "https://files.pythonhosted.org/packages/39/00/d42fc84e8ce8587eb746406aa74ec231f97ce659a48bc064d5ff5cd78889/fastar-0.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf440983d4d64582bddf2f0bd3c43ea1db93a8c31cf7c20e473bffaf6d9c0b6d", size = 987026, upload-time = "2025-11-26T02:35:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/5e4d37faca6f2df6a0bf1efc340192a871356162a7cc53626dc117645ad6/fastar-0.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1d90cbf984a39afda27afe08e40c2d8eddc49c5e80590af641610c7b6dc20161", size = 1042165, upload-time = "2025-11-26T02:35:18.729Z" }, + { url = "https://files.pythonhosted.org/packages/77/fd/bb4731243e42f385e8db9da824971f5d0380e6b31466c36eb089d631c589/fastar-0.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ca0db5e563d84b639fe15385eeca940777b6d2f0a1f3bb7cd5b55ab7124f0554", size = 1046990, upload-time = "2025-11-26T02:35:36.158Z" }, + { url = "https://files.pythonhosted.org/packages/c4/50/2ac066f771858ea45d885cfde7262b286a223af9538d3f85d29d5159b4fb/fastar-0.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:42ff3052d74684a636423d4f040db88eebd4caf20842fa5f06020e0130c01f69", size = 995637, upload-time = "2025-11-26T02:35:54.714Z" }, + { url = "https://files.pythonhosted.org/packages/d3/04/c884ea3fa7c154ac0e89219a337c9d6e0d52d0eb5547b3f99b896eca417e/fastar-0.8.0-cp39-cp39-win32.whl", hash = "sha256:15e3dfaa769d2117ef707e5f47c62126d1b63f8e9c85133112f33f1fbdf8942f", size = 456817, upload-time = "2025-11-26T02:36:33.198Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0f/3a5758cca852bb221e78950fbfece0b932c6de6981ee36fb0ac57cf66c13/fastar-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:5153aa1c194316d0f67b6884a62d122d51fce4196263e92e4bca2a6c47cd44c0", size = 490633, upload-time = "2025-11-26T02:36:19.976Z" }, + { url = "https://files.pythonhosted.org/packages/25/9f/6eaa810c240236eff2edf736cd50a17c97dbab1693cda4f7bcea09d13418/fastar-0.8.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2127cf2e80ffd49744a160201e0e2f55198af6c028a7b3f750026e0b1f1caa4e", size = 710544, upload-time = "2025-11-26T02:34:46.195Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a5/58ff9e49a1cd5fbfc8f1238226cbf83b905376a391a6622cdd396b2cfa29/fastar-0.8.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ff85094f10003801339ac4fa9b20a3410c2d8f284d4cba2dc99de6e98c877812", size = 634020, upload-time = "2025-11-26T02:34:31.085Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/f839257c6600a83fbdb5a7fcc06319599086137b25ba38ca3d2c0fe14562/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3dbca235f0bd804cca6602fe055d3892bebf95fb802e6c6c7d872fb10f7abc6c", size = 871735, upload-time = "2025-11-26T02:34:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/eb/79/4124c54260f7ee5cb7034bfe499eff2f8512b052d54be4671e59d4f25a4f/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e54bfdee6c81a0005e147319e93d8797f442308032c92fa28d03ef8fda076", size = 766779, upload-time = "2025-11-26T02:32:55.109Z" }, + { url = "https://files.pythonhosted.org/packages/36/b6/043b263c4126bf6557c942d099503989af9c5c7ee5cca9a04e00f754816f/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a78e5221b94a80800930b7fd0d0e797ae73aadf7044c05ed46cb9bdf870f022", size = 766755, upload-time = "2025-11-26T02:33:11.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/ff/29a5dc06f2940439ebf98661ecc98d48d3f22fed8d6a2d5dc985d1e8da24/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997092d31ff451de8d0568f6773f3517cb87dcd0bc76184edb65d7154390a6f8", size = 932732, upload-time = "2025-11-26T02:33:27.122Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e8/2218830f422b37aad52c24b53cb84b5d88bd6fd6ad411bd6689b1a32500d/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:558e8fcf8fe574541df5db14a46cd98bfbed14a811b7014a54f2b714c0cfac42", size = 822571, upload-time = "2025-11-26T02:33:42.986Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fd/ba6dfeff77cddfe58d85c490b1735c002b81c0d6f826916a8b6c4f8818bc/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1d2a54f87e2908cc19e1a6ee249620174fbefc54a219aba1eaa6f31657683c3", size = 822440, upload-time = "2025-11-26T02:34:15.439Z" }, + { url = "https://files.pythonhosted.org/packages/a7/57/54d5740c84b35de0eb12975397ecc16785b5ad8bed2dbac38b8c8a7c1edd/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ef94901537be277f9ec59db939eb817960496c6351afede5b102699b5098604d", size = 987424, upload-time = "2025-11-26T02:35:02.742Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c7/18115927f16deb1ddffdbd4ae992e7e33064bc6defa2b92a147948f8bc0c/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:0afbb92f78bf29d5e9db76fb46cbabc429e49015cddf72ab9e761afbe88ac100", size = 1042675, upload-time = "2025-11-26T02:35:20.252Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1a/ca884fc7973ec6d765e87af23a4dd25784fb0a36ac2df825f18c3630bbab/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fb59c7925e7710ad178d9e1a3e65edf295d9a042a0cdcb673b4040949eb8ad0a", size = 1047098, upload-time = "2025-11-26T02:35:37.643Z" }, + { url = "https://files.pythonhosted.org/packages/44/ee/25cd645db749b206bb95e1512e57e75d56ccbbb8ec3536f52a7979deab6b/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e6c4d6329da568ec36b1347b0c09c4d27f9dfdeddf9f438ddb16799ecf170098", size = 997397, upload-time = "2025-11-26T02:35:56.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/6c46aa7f8c8734e7f96ee5141acd3877667ce66f34eea10703aa7571d191/fastar-0.8.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:998e3fa4b555b63eb134e6758437ed739ad1652fdd2a61dfe1dacbfddc35fe66", size = 710662, upload-time = "2025-11-26T02:34:47.593Z" }, + { url = "https://files.pythonhosted.org/packages/70/27/fd622442f2fbd4ff5459677987481ef1c60e077cb4e63a2ed4d8dce6f869/fastar-0.8.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5f83e60d845091f3a12bc37f412774264d161576eaf810ed8b43567eb934b7e5", size = 634049, upload-time = "2025-11-26T02:34:32.365Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ee/aa4d08aea25b5419a7277132e738ab1cd775f26aebddce11413b07e2fdff/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:299672e1c74d8b73c61684fac9159cfc063d35f4b165996a88facb0e26862cb5", size = 872055, upload-time = "2025-11-26T02:34:01.377Z" }, + { url = "https://files.pythonhosted.org/packages/92/9a/2bf2f77aade575e67997e0c759fd55cb1c66b7a5b437b1cd0e97d8b241bc/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3d3a27066b84d015deab5faee78565509bb33b137896443e4144cb1be1a5f90", size = 766787, upload-time = "2025-11-26T02:32:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/0b/90/23a3f6c252f11b10c70f854bce09abc61f71b5a0e6a4b0eac2bcb9a2c583/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef0bcf4385bbdd3c1acecce2d9ea7dab7cc9b8ee0581bbccb7ab11908a7ce288", size = 766861, upload-time = "2025-11-26T02:33:12.824Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/beeb9078380acd4484db5c957d066171695d9340e3526398eb230127b0c2/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f10ef62b6eda6cb6fd9ba8e1fe08a07d7b2bdcc8eaa00eb91566143b92ed7eee", size = 932667, upload-time = "2025-11-26T02:33:28.405Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6d/b034cc637bd0ee638d5a85d08e941b0b8ffd44cf391fb751ba98233734f7/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4f6c82a8ee98c17aa48585ee73b51c89c1b010e5c951af83e07c3436180e3fc", size = 822712, upload-time = "2025-11-26T02:33:44.27Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/7d183c63f59227c4689792042d6647f2586a5e7273b55e81745063088d81/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6129067fcb86276635b5857010f4e9b9c7d5d15dd571bb03c6c1ed73c40fd92", size = 822659, upload-time = "2025-11-26T02:34:16.815Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f9/716e0cd9de2427fdf766bc68176f76226cd01fffef3a56c5046fa863f5f0/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4cc9e77019e489f1ddac446b6a5b9dfb5c3d9abd142652c22a1d9415dbcc0e47", size = 987412, upload-time = "2025-11-26T02:35:04.259Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b9/9a8c3fd59958c1c8027bc075af11722cdc62c4968bb277e841d131232289/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:382bfe82c026086487cb17fee12f4c1e2b4e67ce230f2e04487d3e7ddfd69031", size = 1042911, upload-time = "2025-11-26T02:35:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2f/c3f30963b47022134b8a231c12845f4d7cfba520f59bbc1a82468aea77c7/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:908d2b9a1ff3d549cc304b32f95706a536da8f0bcb0bc0f9e4c1cce39b80e218", size = 1047464, upload-time = "2025-11-26T02:35:39.376Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/218ab6d9a2bab3b07718e6cd8405529600edc1e9c266320e8524c8f63251/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1aa7dbde2d2d73eb5b6203d0f74875cb66350f0f1b4325b4839fc8fbbf5d074e", size = 997309, upload-time = "2025-11-26T02:35:57.722Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/da0cbac6da78b22ddb6ec0bbf3ad1813f8dd991a911342fc20e5feabaa15/fastar-0.8.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:284036bae786a520456ad3f58e72aaf1bd5d74e309132e568343564daa4ae383", size = 710367, upload-time = "2025-11-26T02:34:48.853Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f0/951d368b4538477fe44651aaa7436318c22bf89e8e18086a68bd89e14a82/fastar-0.8.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5aba0942b4f56acdb8fa8aa7cb506f70c1a17bf13dcab318a17ffb467cb2e7ec", size = 633744, upload-time = "2025-11-26T02:34:33.723Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d2/5f37b634aff1e27191874b7f0fb7306e60566d984df06ff5c2f901ef123b/fastar-0.8.0-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:52eda6230799db7bbd44461c622161e9bcd43603399da19b0daab2782e0030b0", size = 871412, upload-time = "2025-11-26T02:34:02.738Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cd/41148c74cc164aacb8b5e7cb938a7ecc5a4e595f95e312aafcc8dbe36e48/fastar-0.8.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f860566b9f3cb1900980f46a4c3f003990c0009c11730f988f758542c17a2364", size = 766470, upload-time = "2025-11-26T02:32:58.503Z" }, + { url = "https://files.pythonhosted.org/packages/5f/89/b5715d4f969f15fe78b184ae42295e71f0ad2e0e083f0625212271cbda55/fastar-0.8.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:78f3fe5f45437c66d1dbece5f31aa487e48ef46d76b2082b873d5fa18013ebe1", size = 765285, upload-time = "2025-11-26T02:33:14.148Z" }, + { url = "https://files.pythonhosted.org/packages/39/4a/88223721278a663684c4fa6cb95ca296e0ba385362de4809a5e85cacfb36/fastar-0.8.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82bc445202bbc53f067bb15e3b8639f01fd54d3096a0f9601240690cfd7c9684", size = 933881, upload-time = "2025-11-26T02:33:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/ac/73/a13d6d57029378477c4d9232eddfb81bb2f74e0e91bdbca864c74223d877/fastar-0.8.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b1208b5453cfe7192e54765f73844b80d684bd8dc6d6acbbb60ead42590b13e", size = 822511, upload-time = "2025-11-26T02:33:45.882Z" }, + { url = "https://files.pythonhosted.org/packages/a4/96/79000571a10d93e2110e72b5e7155e6bf138c8cb896117cfb768c74d01ff/fastar-0.8.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8922754c66699e27d4f1ce07c9c256228054cdc9bb36363e8bb5b503453a6da", size = 822204, upload-time = "2025-11-26T02:34:18.164Z" }, + { url = "https://files.pythonhosted.org/packages/35/e4/c2066024f336349ce36f85b0df9d082981d453cfdb5c139af2c5eebcc49a/fastar-0.8.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:92cad46dfbb9969359823c9f61165ec32d5d675d86e863889416e9b64efea95c", size = 987454, upload-time = "2025-11-26T02:35:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b7/e3a062534af02610dea226d2c14e70beb90b91a23562fed8f76bfa35e6cd/fastar-0.8.0-pp39-pypy39_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:f4eb9560a447ff6a4b377f08b6e5d3a31909a612b028f2c57810ffaf570eceb8", size = 1041310, upload-time = "2025-11-26T02:35:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/91/73/1640476553705ee29ff9350a3b6c707ac150be53094d326e0d81d62a65ec/fastar-0.8.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:52455794e6cc2b6a6dbf141a1c4312a1a1215d75e8849a35fcff694454da880f", size = 1047020, upload-time = "2025-11-26T02:35:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/b7/79/9b492c4da885a6699bc76032dbc285c565429ca1b6dc6b5aa5c908111d22/fastar-0.8.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8de5decfa18a03807ae26ba5af095c2c04ac31ae915e9a849363a4495463171f", size = 996379, upload-time = "2025-11-26T02:35:59.297Z" }, +] + +[[package]] +name = "fastavro" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/8b/fa2d3287fd2267be6261d0177c6809a7fa12c5600ddb33490c8dc29e77b2/fastavro-1.12.1.tar.gz", hash = "sha256:2f285be49e45bc047ab2f6bed040bb349da85db3f3c87880e4b92595ea093b2b", size = 1025661, upload-time = "2025-10-10T15:40:55.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/a0/077fd7cbfc143152cb96780cb592ed6cb6696667d8bc1b977745eb2255a8/fastavro-1.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:00650ca533907361edda22e6ffe8cf87ab2091c5d8aee5c8000b0f2dcdda7ed3", size = 1000335, upload-time = "2025-10-10T15:40:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ae/a115e027f3a75df237609701b03ecba0b7f0aa3d77fe0161df533fde1eb7/fastavro-1.12.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac76d6d95f909c72ee70d314b460b7e711d928845771531d823eb96a10952d26", size = 3221067, upload-time = "2025-10-10T15:41:04.399Z" }, + { url = "https://files.pythonhosted.org/packages/94/4e/c4991c3eec0175af9a8a0c161b88089cb7bf7fe353b3e3be1bc4cf9036b2/fastavro-1.12.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55eef18c41d4476bd32a82ed5dd86aabc3f614e1b66bdb09ffa291612e1670", size = 3228979, upload-time = "2025-10-10T15:41:06.738Z" }, + { url = "https://files.pythonhosted.org/packages/21/0c/f2afb8eaea38799ccb1ed07d68bf2659f2e313f1902bbd36774cf6a1bef9/fastavro-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81563e1f93570e6565487cdb01ba241a36a00e58cff9c5a0614af819d1155d8f", size = 3160740, upload-time = "2025-10-10T15:41:08.731Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/f4d367924b40b86857862c1fa65f2afba94ddadf298b611e610a676a29e5/fastavro-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec207360f76f0b3de540758a297193c5390e8e081c43c3317f610b1414d8c8f", size = 3235787, upload-time = "2025-10-10T15:41:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/8db9331896e3dfe4f71b2b3c23f2e97fbbfd90129777467ca9f8bafccb74/fastavro-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:c0390bfe4a9f8056a75ac6785fbbff8f5e317f5356481d2e29ec980877d2314b", size = 449350, upload-time = "2025-10-10T15:41:12.104Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e9/31c64b47cefc0951099e7c0c8c8ea1c931edd1350f34d55c27cbfbb08df1/fastavro-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b632b713bc5d03928a87d811fa4a11d5f25cd43e79c161e291c7d3f7aa740fd", size = 1016585, upload-time = "2025-10-10T15:41:13.717Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/111560775b548f5d8d828c1b5285ff90e2d2745643fb80ecbf115344eea4/fastavro-1.12.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa7ab3769beadcebb60f0539054c7755f63bd9cf7666e2c15e615ab605f89a8", size = 3404629, upload-time = "2025-10-10T15:41:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/b0/07/6bb93cb963932146c2b6c5c765903a0a547ad9f0f8b769a4a9aad8c06369/fastavro-1.12.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123fb221df3164abd93f2d042c82f538a1d5a43ce41375f12c91ce1355a9141e", size = 3428594, upload-time = "2025-10-10T15:41:17.779Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8115ec36b584197ea737ec79e3499e1f1b640b288d6c6ee295edd13b80f6/fastavro-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:632a4e3ff223f834ddb746baae0cc7cee1068eb12c32e4d982c2fee8a5b483d0", size = 3344145, upload-time = "2025-10-10T15:41:19.89Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9e/a7cebb3af967e62539539897c10138fa0821668ec92525d1be88a9cd3ee6/fastavro-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e6caf4e7a8717d932a3b1ff31595ad169289bbe1128a216be070d3a8391671", size = 3431942, upload-time = "2025-10-10T15:41:22.076Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/7774ddfb8781c5224294c01a593ebce2ad3289b948061c9701bd1903264d/fastavro-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:b91a0fe5a173679a6c02d53ca22dcaad0a2c726b74507e0c1c2e71a7c3f79ef9", size = 450542, upload-time = "2025-10-10T15:41:23.333Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f0/10bd1a3d08667fa0739e2b451fe90e06df575ec8b8ba5d3135c70555c9bd/fastavro-1.12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:509818cb24b98a804fc80be9c5fed90f660310ae3d59382fc811bfa187122167", size = 1009057, upload-time = "2025-10-10T15:41:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/0d985bc99e1fa9e74c636658000ba38a5cd7f5ab2708e9c62eaf736ecf1a/fastavro-1.12.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089e155c0c76e0d418d7e79144ce000524dd345eab3bc1e9c5ae69d500f71b14", size = 3391866, upload-time = "2025-10-10T15:41:26.882Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/b4951dc84ebc34aac69afcbfbb22ea4a91080422ec2bfd2c06076ff1d419/fastavro-1.12.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44cbff7518901c91a82aab476fcab13d102e4999499df219d481b9e15f61af34", size = 3458005, upload-time = "2025-10-10T15:41:29.017Z" }, + { url = "https://files.pythonhosted.org/packages/af/f8/5a8df450a9f55ca8441f22ea0351d8c77809fc121498b6970daaaf667a21/fastavro-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a275e48df0b1701bb764b18a8a21900b24cf882263cb03d35ecdba636bbc830b", size = 3295258, upload-time = "2025-10-10T15:41:31.564Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/40f25299111d737e58b85696e91138a66c25b7334f5357e7ac2b0e8966f8/fastavro-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2de72d786eb38be6b16d556b27232b1bf1b2797ea09599507938cdb7a9fe3e7c", size = 3430328, upload-time = "2025-10-10T15:41:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/e0/07/85157a7c57c5f8b95507d7829b5946561e5ee656ff80e9dd9a757f53ddaf/fastavro-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:9090f0dee63fe022ee9cc5147483366cc4171c821644c22da020d6b48f576b4f", size = 444140, upload-time = "2025-10-10T15:41:34.902Z" }, + { url = "https://files.pythonhosted.org/packages/bb/57/26d5efef9182392d5ac9f253953c856ccb66e4c549fd3176a1e94efb05c9/fastavro-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78df838351e4dff9edd10a1c41d1324131ffecbadefb9c297d612ef5363c049a", size = 1000599, upload-time = "2025-10-10T15:41:36.554Z" }, + { url = "https://files.pythonhosted.org/packages/33/cb/8ab55b21d018178eb126007a56bde14fd01c0afc11d20b5f2624fe01e698/fastavro-1.12.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780476c23175d2ae457c52f45b9ffa9d504593499a36cd3c1929662bf5b7b14b", size = 3335933, upload-time = "2025-10-10T15:41:39.07Z" }, + { url = "https://files.pythonhosted.org/packages/fe/03/9c94ec9bf873eb1ffb0aa694f4e71940154e6e9728ddfdc46046d7e8ced4/fastavro-1.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0714b285160fcd515eb0455540f40dd6dac93bdeacdb03f24e8eac3d8aa51f8d", size = 3402066, upload-time = "2025-10-10T15:41:41.608Z" }, + { url = "https://files.pythonhosted.org/packages/75/c8/cb472347c5a584ccb8777a649ebb28278fccea39d005fc7df19996f41df8/fastavro-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc2dcec5843d499f2489bfe0747999108f78c5b29295d877379f1972a3d41a", size = 3240038, upload-time = "2025-10-10T15:41:43.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/77/569ce9474c40304b3a09e109494e020462b83e405545b78069ddba5f614e/fastavro-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b1921ac35f3d89090a5816b626cf46e67dbecf3f054131f84d56b4e70496f45", size = 3369398, upload-time = "2025-10-10T15:41:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1f/9589e35e9ea68035385db7bdbf500d36b8891db474063fb1ccc8215ee37c/fastavro-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:5aa777b8ee595b50aa084104cd70670bf25a7bbb9fd8bb5d07524b0785ee1699", size = 444220, upload-time = "2025-10-10T15:41:47.39Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d2/78435fe737df94bd8db2234b2100f5453737cffd29adee2504a2b013de84/fastavro-1.12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c3d67c47f177e486640404a56f2f50b165fe892cc343ac3a34673b80cc7f1dd6", size = 1086611, upload-time = "2025-10-10T15:41:48.818Z" }, + { url = "https://files.pythonhosted.org/packages/b6/be/428f99b10157230ddac77ec8cc167005b29e2bd5cbe228345192bb645f30/fastavro-1.12.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5217f773492bac43dae15ff2931432bce2d7a80be7039685a78d3fab7df910bd", size = 3541001, upload-time = "2025-10-10T15:41:50.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/08/a2eea4f20b85897740efe44887e1ac08f30dfa4bfc3de8962bdcbb21a5a1/fastavro-1.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:469fecb25cba07f2e1bfa4c8d008477cd6b5b34a59d48715e1b1a73f6160097d", size = 3432217, upload-time = "2025-10-10T15:41:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/87/bb/b4c620b9eb6e9838c7f7e4b7be0762834443adf9daeb252a214e9ad3178c/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d71c8aa841ef65cfab709a22bb887955f42934bced3ddb571e98fdbdade4c609", size = 3366742, upload-time = "2025-10-10T15:41:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d1/e69534ccdd5368350646fea7d93be39e5f77c614cca825c990bd9ca58f67/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b81fc04e85dfccf7c028e0580c606e33aa8472370b767ef058aae2c674a90746", size = 3383743, upload-time = "2025-10-10T15:41:57.68Z" }, + { url = "https://files.pythonhosted.org/packages/58/54/b7b4a0c3fb5fcba38128542da1b26c4e6d69933c923f493548bdfd63ab6a/fastavro-1.12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9445da127751ba65975d8e4bdabf36bfcfdad70fc35b2d988e3950cce0ec0e7c", size = 1001377, upload-time = "2025-10-10T15:41:59.241Z" }, + { url = "https://files.pythonhosted.org/packages/1e/4f/0e589089c7df0d8f57d7e5293fdc34efec9a3b758a0d4d0c99a7937e2492/fastavro-1.12.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed924233272719b5d5a6a0b4d80ef3345fc7e84fc7a382b6232192a9112d38a6", size = 3320401, upload-time = "2025-10-10T15:42:01.682Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/260110d56194ae29d7e423a336fccea8bcd103196d00f0b364b732bdb84e/fastavro-1.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3616e2f0e1c9265e92954fa099db79c6e7817356d3ff34f4bcc92699ae99697c", size = 3350894, upload-time = "2025-10-10T15:42:04.073Z" }, + { url = "https://files.pythonhosted.org/packages/d0/96/58b0411e8be9694d5972bee3167d6c1fd1fdfdf7ce253c1a19a327208f4f/fastavro-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb0337b42fd3c047fcf0e9b7597bd6ad25868de719f29da81eabb6343f08d399", size = 3229644, upload-time = "2025-10-10T15:42:06.221Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/38660660eac82c30471d9101f45b3acfdcbadfe42d8f7cdb129459a45050/fastavro-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:64961ab15b74b7c168717bbece5660e0f3d457837c3cc9d9145181d011199fa7", size = 3329704, upload-time = "2025-10-10T15:42:08.384Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a9/1672910f458ecb30b596c9e59e41b7c00309b602a0494341451e92e62747/fastavro-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:792356d320f6e757e89f7ac9c22f481e546c886454a6709247f43c0dd7058004", size = 452911, upload-time = "2025-10-10T15:42:09.795Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/2e15d0938ded1891b33eff252e8500605508b799c2e57188a933f0bd744c/fastavro-1.12.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120aaf82ac19d60a1016afe410935fe94728752d9c2d684e267e5b7f0e70f6d9", size = 3541999, upload-time = "2025-10-10T15:42:11.794Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1c/6dfd082a205be4510543221b734b1191299e6a1810c452b6bc76dfa6968e/fastavro-1.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6a3462934b20a74f9ece1daa49c2e4e749bd9a35fa2657b53bf62898fba80f5", size = 3433972, upload-time = "2025-10-10T15:42:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/9de694625a1a4b727b1ad0958d220cab25a9b6cf7f16a5c7faa9ea7b2261/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1f81011d54dd47b12437b51dd93a70a9aa17b61307abf26542fc3c13efbc6c51", size = 3368752, upload-time = "2025-10-10T15:42:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/fa/93/b44f67589e4d439913dab6720f7e3507b0fa8b8e56d06f6fc875ced26afb/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43ded16b3f4a9f1a42f5970c2aa618acb23ea59c4fcaa06680bdf470b255e5a8", size = 3386636, upload-time = "2025-10-10T15:42:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/67f5682cd59cb59ec6eecb5479b132caaf69709d1e1ceda4f92d8c69d7f1/fastavro-1.12.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:02281432dcb11c78b3280da996eff61ee0eff39c5de06c6e0fbf19275093e6d4", size = 1002311, upload-time = "2025-10-10T15:42:20.415Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/2f15a7ad24e4b6e0239016c068f142358732bf8ead0315ee926b88634bec/fastavro-1.12.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4128978b930aaf930332db4b3acc290783183f3be06a241ae4a482f3ed8ce892", size = 3195871, upload-time = "2025-10-10T15:42:22.675Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e5/6fc0250b3006b1b42c1ab9f0511ccd44e1aeb15a63a77fc780ee97f58797/fastavro-1.12.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:546ffffda6610fca672f0ed41149808e106d8272bb246aa7539fa8bb6f117f17", size = 3204127, upload-time = "2025-10-10T15:42:24.855Z" }, + { url = "https://files.pythonhosted.org/packages/c6/43/1f3909eb096eb1066e416f0875abe783b73fabe823ad616f6956b3e80e84/fastavro-1.12.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a7d840ccd9aacada3ddc80fbcc4ea079b658107fe62e9d289a0de9d54e95d366", size = 3135604, upload-time = "2025-10-10T15:42:28.899Z" }, + { url = "https://files.pythonhosted.org/packages/e6/61/ec083a3a5d7c2b97d0e2c9e137a6e667583afe884af0e49318f7ee7eaa5a/fastavro-1.12.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3100ad643e7fa658469a2a2db229981c1a000ff16b8037c0b58ce3ec4d2107e8", size = 3210932, upload-time = "2025-10-10T15:42:30.891Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ba/b814fb09b32c8f3059451c33bb11d55eb23188e6a499f5dde628d13bc076/fastavro-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:a38607444281619eda3a9c1be9f5397634012d1b237142eee1540e810b30ac8b", size = 510328, upload-time = "2025-10-10T15:42:32.415Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/e0/a75dbe4bca1e7d41307323dad5ea2efdd95408f74ab2de8bd7dba9b51a1a/filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64", size = 19510, upload-time = "2026-01-02T15:33:32.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8", size = 16697, upload-time = "2026-01-02T15:33:31.133Z" }, +] + +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/27/954057b0d1f53f086f681755207dda6de6c660ce133c829158e8e8fe7895/fsspec-2025.12.0.tar.gz", hash = "sha256:c505de011584597b1060ff778bb664c1bc022e87921b0e4f10cc9c44f9635973", size = 309748, upload-time = "2025-12-03T15:23:42.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, +] + +[[package]] +name = "google-auth" +version = "2.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/3c/ec64b9a275ca22fa1cd3b6e77fefcf837b0732c890aa32d2bd21313d9b33/google_auth-2.47.0.tar.gz", hash = "sha256:833229070a9dfee1a353ae9877dcd2dec069a8281a4e72e72f77d4a70ff945da", size = 323719, upload-time = "2026-01-06T21:55:31.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl", hash = "sha256:c516d68336bfde7cf0da26aab674a36fedcf04b37ac4edd59c597178760c3498", size = 234867, upload-time = "2026-01-06T21:55:28.6Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests", marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "google-genai" +version = "1.47.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.10'" }, + { name = "google-auth", marker = "python_full_version < '3.10'" }, + { name = "httpx", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "tenacity", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "websockets", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/97/784fba9bc6c41263ff90cb9063eadfdd755dde79cfa5a8d0e397b067dcf9/google_genai-1.47.0.tar.gz", hash = "sha256:ecece00d0a04e6739ea76cc8dad82ec9593d9380aaabef078990e60574e5bf59", size = 241471, upload-time = "2025-10-29T22:01:02.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/ef/e080e8d67c270ea320956bb911a9359664fc46d3b87d1f029decd33e5c4c/google_genai-1.47.0-py3-none-any.whl", hash = "sha256:e3851237556cbdec96007d8028b4b1f2425cdc5c099a8dc36b72a57e42821b60", size = 241506, upload-time = "2025-10-29T22:01:00.982Z" }, +] + +[[package]] +name = "google-genai" +version = "1.57.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "distro", marker = "python_full_version >= '3.10'" }, + { name = "google-auth", extra = ["requests"], marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, + { name = "sniffio", marker = "python_full_version >= '3.10'" }, + { name = "tenacity", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "websockets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/b4/8251c2d2576224a4b51a8ab6159820f9200b8da28ff555c78ee15607096e/google_genai-1.57.0.tar.gz", hash = "sha256:0ff9c36b8d68abfbdbd13b703ece926de5f3e67955666b36315ecf669b94a826", size = 485648, upload-time = "2026-01-07T20:38:20.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/02/858bdae08e2184b6afe0b18bc3113318522c9cf326a5a1698055edd31f88/google_genai-1.57.0-py3-none-any.whl", hash = "sha256:d63c7a89a1f549c4d14032f41a0cdb4b6fe3f565e2eee6b5e0907a0aeceabefd", size = 713323, upload-time = "2026-01-07T20:38:18.051Z" }, +] + +[[package]] +name = "graphql-core" +version = "3.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/9b/037a640a2983b09aed4a823f9cf1729e6d780b0671f854efa4727a7affbe/graphql_core-3.2.7.tar.gz", hash = "sha256:27b6904bdd3b43f2a0556dad5d579bdfdeab1f38e8e8788e555bdcb586a6f62c", size = 513484, upload-time = "2025-11-01T22:30:40.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/14/933037032608787fb92e365883ad6a741c235e0ff992865ec5d904a38f1e/graphql_core-3.2.7-py3-none-any.whl", hash = "sha256:17fc8f3ca4a42913d8e24d9ac9f08deddf0a0b2483076575757f6c412ead2ec0", size = 207262, upload-time = "2025-11-01T22:30:38.912Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, + { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, + { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, + { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, + { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, + { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, + { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, + { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, + { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, + { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, + { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, + { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, + { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c0/93885c4106d2626bf51fdec377d6aef740dfa5c4877461889a7cf8e565cc/greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", size = 269859, upload-time = "2025-08-07T13:16:16.003Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f5/33f05dc3ba10a02dedb1485870cf81c109227d3d3aa280f0e48486cac248/greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", size = 627610, upload-time = "2025-08-07T13:43:01.345Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/9476decef51a0844195f99ed5dc611d212e9b3515512ecdf7321543a7225/greenlet-3.2.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58", size = 639417, upload-time = "2025-08-07T13:45:32.094Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e0/849b9159cbb176f8c0af5caaff1faffdece7a8417fcc6fe1869770e33e21/greenlet-3.2.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4", size = 634751, upload-time = "2025-08-07T13:53:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d3/844e714a9bbd39034144dca8b658dcd01839b72bb0ec7d8014e33e3705f0/greenlet-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433", size = 634020, upload-time = "2025-08-07T13:18:36.841Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4c/f3de2a8de0e840ecb0253ad0dc7e2bb3747348e798ec7e397d783a3cb380/greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", size = 582817, upload-time = "2025-08-07T13:18:35.48Z" }, + { url = "https://files.pythonhosted.org/packages/89/80/7332915adc766035c8980b161c2e5d50b2f941f453af232c164cff5e0aeb/greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", size = 1111985, upload-time = "2025-08-07T13:42:42.425Z" }, + { url = "https://files.pythonhosted.org/packages/66/71/1928e2c80197353bcb9b50aa19c4d8e26ee6d7a900c564907665cf4b9a41/greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", size = 1136137, upload-time = "2025-08-07T13:18:26.168Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/7bd33643e48ed45dcc0e22572f650767832bd4e1287f97434943cc402148/greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10", size = 1542941, upload-time = "2025-11-04T12:42:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/9b/74/4bc433f91d0d09a1c22954a371f9df928cb85e72640870158853a83415e5/greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be", size = 1609685, upload-time = "2025-11-04T12:42:29.242Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/a5dc74dde38aeb2b15d418cec76ed50e1dd3d620ccda84d8199703248968/greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", size = 281400, upload-time = "2025-08-07T14:02:20.263Z" }, + { url = "https://files.pythonhosted.org/packages/e5/44/342c4591db50db1076b8bda86ed0ad59240e3e1da17806a4cf10a6d0e447/greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", size = 298533, upload-time = "2025-08-07T13:56:34.168Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/6a/33d1702184d94106d3cdd7bfb788e19723206fce152e303473ca3b946c7b/greenlet-3.3.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d", size = 273658, upload-time = "2025-12-04T14:23:37.494Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b7/2b5805bbf1907c26e434f4e448cd8b696a0b71725204fa21a211ff0c04a7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb", size = 574810, upload-time = "2025-12-04T14:50:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/94/38/343242ec12eddf3d8458c73f555c084359883d4ddc674240d9e61ec51fd6/greenlet-3.3.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd", size = 586248, upload-time = "2025-12-04T14:57:39.35Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/0ae86792fb212e4384041e0ef8e7bc66f59a54912ce407d26a966ed2914d/greenlet-3.3.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b299a0cb979f5d7197442dccc3aee67fce53500cd88951b7e6c35575701c980b", size = 597403, upload-time = "2025-12-04T15:07:10.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/d4e73f5dfa888364bbf02efa85616c6714ae7c631c201349782e5b428925/greenlet-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b49e7ed51876b459bd645d83db257f0180e345d3f768a35a85437a24d5a49082", size = 300740, upload-time = "2025-12-04T14:47:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, + { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, + { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, + { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, + { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, + { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, +] + +[[package]] +name = "griffe" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + +[[package]] +name = "griffe-typingdoc" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/77/d5e5fa0a8391bc2890ae45255847197299739833108dd76ee3c9b2ff0bba/griffe_typingdoc-0.3.0.tar.gz", hash = "sha256:59d9ef98d02caa7aed88d8df1119c9e48c02ed049ea50ce4018ace9331d20f8b", size = 33169, upload-time = "2025-10-23T12:01:39.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/af/aa32c13f753e2625ec895b1f56eee3c9380a2088a88a2c028955e223856e/griffe_typingdoc-0.3.0-py3-none-any.whl", hash = "sha256:4f6483fff7733a679d1dce142fb029f314125f3caaf0d620eb82e7390c8564bb", size = 9923, upload-time = "2025-10-23T12:01:37.601Z" }, +] + +[[package]] +name = "griffe-warnings-deprecated" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/0e/f034e1714eb2c694d6196c75f77a02f9c69d19f9961c4804a016397bf3e5/griffe_warnings_deprecated-1.1.0.tar.gz", hash = "sha256:7bf21de327d59c66c7ce08d0166aa4292ce0577ff113de5878f428d102b6f7c5", size = 33260, upload-time = "2024-12-10T21:02:18.395Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/4c/b7241f03ad1f22ec2eed33b0f90c4f8c949e3395c4b7488670b07225a20b/griffe_warnings_deprecated-1.1.0-py3-none-any.whl", hash = "sha256:e7b0e8bfd6e5add3945d4d9805b2a41c72409e456733965be276d55f01e8a7a2", size = 5854, upload-time = "2024-12-10T21:02:16.96Z" }, +] + +[[package]] +name = "groq" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/12/f4099a141677fcd2ed79dcc1fcec431e60c52e0e90c9c5d935f0ffaf8c0e/groq-1.0.0.tar.gz", hash = "sha256:66cb7bb729e6eb644daac7ce8efe945e99e4eb33657f733ee6f13059ef0c25a9", size = 146068, upload-time = "2025-12-17T23:34:23.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/88/3175759d2ef30406ea721f4d837bfa1ba4339fde3b81ba8c5640a96ed231/groq-1.0.0-py3-none-any.whl", hash = "sha256:6e22bf92ffad988f01d2d4df7729add66b8fd5dbfb2154b5bbf3af245b72c731", size = 138292, upload-time = "2025-12-17T23:34:21.957Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[package]] +name = "hjson" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/e5/0b56d723a76ca67abadbf7fb71609fb0ea7e6926e94fcca6c65a85b36a0e/hjson-3.1.0.tar.gz", hash = "sha256:55af475a27cf83a7969c808399d7bccdec8fb836a07ddbd574587593b9cdcf75", size = 40541, upload-time = "2022-08-13T02:53:01.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/7f/13cd798d180af4bf4c0ceddeefba2b864a63c71645abc0308b768d67bb81/hjson-3.1.0-py3-none-any.whl", hash = "sha256:65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89", size = 54018, upload-time = "2022-08-13T02:52:59.899Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/b1fe0e8890f0292c266117d4cd268186758a9c34e576fbd573fdf3beacff/httptools-0.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4", size = 206454, upload-time = "2025-10-10T03:55:01.528Z" }, + { url = "https://files.pythonhosted.org/packages/57/a7/a675c90b49e550c7635ce209c01bc61daa5b08aef17da27ef4e0e78fcf3f/httptools-0.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a", size = 110260, upload-time = "2025-10-10T03:55:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/03/44/fb5ef8136e6e97f7b020e97e40c03a999f97e68574d4998fa52b0a62b01b/httptools-0.7.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf", size = 441524, upload-time = "2025-10-10T03:55:03.292Z" }, + { url = "https://files.pythonhosted.org/packages/b4/62/8496a5425341867796d7e2419695f74a74607054e227bbaeabec8323e87f/httptools-0.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28", size = 440877, upload-time = "2025-10-10T03:55:04.282Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f1/26c2e5214106bf6ed04d03e518ff28ca0c6b5390c5da7b12bbf94b40ae43/httptools-0.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517", size = 425775, upload-time = "2025-10-10T03:55:05.341Z" }, + { url = "https://files.pythonhosted.org/packages/3a/34/7500a19257139725281f7939a7d1aa3701cf1ac4601a1690f9ab6f510e15/httptools-0.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad", size = 425001, upload-time = "2025-10-10T03:55:06.389Z" }, + { url = "https://files.pythonhosted.org/packages/71/04/31a7949d645ebf33a67f56a0024109444a52a271735e0647a210264f3e61/httptools-0.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023", size = 86818, upload-time = "2025-10-10T03:55:07.316Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "fsspec", version = "2025.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "shellingham" }, + { name = "tqdm" }, + { name = "typer-slim" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/c9/d42b5cfa0a50b77cf9165e13edfaf2e3bd4e0def9cb67b6b8a07224a52ab/huggingface_hub-1.3.0.tar.gz", hash = "sha256:289e2a3586fdf01e35882944eaa06fbd57436de24b6e653d1fab248584acd66b", size = 622092, upload-time = "2026-01-09T09:54:44.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/5b/c5fde1f56b1f072b3028ec5413f3f5bf472c5891ebb34589cddb1689609f/huggingface_hub-1.3.0-py3-none-any.whl", hash = "sha256:763f450169bb05ea3867990e9d3ba9464eb617b874791301dc81be2c6ffb0bf5", size = 533092, upload-time = "2026-01-09T09:54:43.228Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "inline-snapshot" +version = "0.31.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pytest" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/b1/52b5ee59f73ed31d5fe21b10881bf2d121d07d54b23c0b6b74186792e620/inline_snapshot-0.31.1.tar.gz", hash = "sha256:4ea5ed70aa1d652713bbfd750606b94bd8a42483f7d3680433b3e92994495f64", size = 2606338, upload-time = "2025-11-07T07:36:18.932Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/52/945db420380efbda8c69a7a4a16c53df9d7ac50d8217286b9d41e5d825ff/inline_snapshot-0.31.1-py3-none-any.whl", hash = "sha256:7875a73c986a03388c7e758fb5cb8a43d2c3a20328aa1d851bfb4ed536c4496f", size = 71965, upload-time = "2025-11-07T07:36:16.836Z" }, +] + +[[package]] +name = "invoke" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jieba" +version = "0.42.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2", size = 19214172, upload-time = "2020-01-20T14:27:23.5Z" } + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652, upload-time = "2025-11-09T20:46:41.021Z" }, + { url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829, upload-time = "2025-11-09T20:46:43.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568, upload-time = "2025-11-09T20:46:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052, upload-time = "2025-11-09T20:46:46.818Z" }, + { url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585, upload-time = "2025-11-09T20:46:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541, upload-time = "2025-11-09T20:46:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423, upload-time = "2025-11-09T20:46:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958, upload-time = "2025-11-09T20:46:53.432Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084, upload-time = "2025-11-09T20:46:54.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054, upload-time = "2025-11-09T20:46:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368, upload-time = "2025-11-09T20:46:58.638Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847, upload-time = "2025-11-09T20:47:00.295Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, + { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, + { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, + { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, + { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/7d/da/3e1fbd1f03f89ff0b4469d481be0b5cf2880c8e7b56fd80303b3ab5ae52d/jiter-0.12.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c9d28b218d5f9e5f69a0787a196322a5056540cb378cac8ff542b4fa7219966c", size = 319378, upload-time = "2025-11-09T20:48:51.761Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/e07d69285e9e19a153050a6d281d2f0968600753a8fed8a3a141d6ffc140/jiter-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0ee12028daf8cfcf880dd492349a122a64f42c059b6c62a2b0c96a83a8da820", size = 312195, upload-time = "2025-11-09T20:48:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/1f1cb5231b36af9f3d6d5b6030e70110faf14fd143419fc5fe7d852e691a/jiter-0.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b135ebe757a82d67ed2821526e72d0acf87dd61f6013e20d3c45b8048af927b", size = 352777, upload-time = "2025-11-09T20:48:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5e/728393bbbc99b31e8f7a4fdd8fa55e455a0a9648f79097d9088baf1f676f/jiter-0.12.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15d7fafb81af8a9e3039fc305529a61cd933eecee33b4251878a1c89859552a3", size = 370738, upload-time = "2025-11-09T20:48:56.632Z" }, + { url = "https://files.pythonhosted.org/packages/30/08/ac92f0df7b14ac82f2fe0a382a8000e600ab90af95798d4a7db0c1bd0736/jiter-0.12.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92d1f41211d8a8fe412faad962d424d334764c01dac6691c44691c2e4d3eedaf", size = 483744, upload-time = "2025-11-09T20:48:58.006Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f4/dbfa4e759a2b82e969a14c3d0a91b176f1ed94717183a2f495cf94a651b9/jiter-0.12.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a64a48d7c917b8f32f25c176df8749ecf08cec17c466114727efe7441e17f6d", size = 382888, upload-time = "2025-11-09T20:48:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d9/b86fff7f748b0bb54222a8f132ffaf4d1be56b4591fa76d3cfdd701a33e5/jiter-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:122046f3b3710b85de99d9aa2f3f0492a8233a2f54a64902b096efc27ea747b5", size = 366465, upload-time = "2025-11-09T20:49:01.408Z" }, + { url = "https://files.pythonhosted.org/packages/93/3c/1152d8b433317a568927e13c1b125c680e6c058ff5d304833be8469bd4f2/jiter-0.12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:27ec39225e03c32c6b863ba879deb427882f243ae46f0d82d68b695fa5b48b40", size = 392603, upload-time = "2025-11-09T20:49:02.784Z" }, + { url = "https://files.pythonhosted.org/packages/6e/92/ff19d8fb87f3f9438eb7464862c8d0126455bc046b345d59b21443640c62/jiter-0.12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26b9e155ddc132225a39b1995b3b9f0fe0f79a6d5cbbeacf103271e7d309b404", size = 523780, upload-time = "2025-11-09T20:49:04.42Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/4260e2d84e4a293c36d2a8e8b8dcd69609c671f3bd310e4625359217c517/jiter-0.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ab05b7c58e29bb9e60b70c2e0094c98df79a1e42e397b9bb6eaa989b7a66dd0", size = 514874, upload-time = "2025-11-09T20:49:05.844Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f7/574d2cb79e86feb035ade18c2254da71d04417555907c9df51dd6b183426/jiter-0.12.0-cp39-cp39-win32.whl", hash = "sha256:59f9f9df87ed499136db1c2b6c9efb902f964bed42a582ab7af413b6a293e7b0", size = 208329, upload-time = "2025-11-09T20:49:07.444Z" }, + { url = "https://files.pythonhosted.org/packages/05/ce/50725ec39782d8c973f19ae2d7dd3d192d01332c7cbde48c75e16a3e85a9/jiter-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3719596a1ebe7a48a498e8d5d0c4bf7553321d4c3eee1d620628d51351a3928", size = 206557, upload-time = "2025-11-09T20:49:08.888Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, + { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" }, + { name = "referencing", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "lia-web" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cross-web", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/3d/7d574a7a5cf5fbc5fc09c07ea3696dd400353b7702bc009cf596b8c12035/lia_web-0.3.1.tar.gz", hash = "sha256:7f551269eddd729f1437e9341ad21622a849eb0c0975d9232ccbbaadbdc74c06", size = 2021, upload-time = "2025-12-25T20:41:51.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/8b/b628fc18658f94b3d094708a18b71083cf47628e85cbc6b9edba54d5b2d7/lia_web-0.3.1-py3-none-any.whl", hash = "sha256:e4e6e7a9381e228aca60a6f3d67dbae9a5f4638eced242d931f95797ddba3f8b", size = 5933, upload-time = "2025-12-25T20:41:52.289Z" }, +] + +[[package]] +name = "logfire-api" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/2a/76d8fbafa881cb03d5ad6e1d67d537e8c308ae7145812b8891f7b8751224/logfire_api-4.17.0.tar.gz", hash = "sha256:4647dad05146a68af441d59a7746a966df4c2581b316616f1210f8cf74931353", size = 58305, upload-time = "2026-01-07T10:52:17.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bc/3844e103dca998dcc195d6ef09e0f29d9000bac870117db1dd59a29bfeef/logfire_api-4.17.0-py3-none-any.whl", hash = "sha256:80a4b79cd9918934cdf2043d944cfb04182708178d846273484d47f3619a5a39", size = 96146, upload-time = "2026-01-07T10:52:15.088Z" }, +] + +[[package]] +name = "markdown" +version = "3.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markdown-include-variants" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/47/ec9eae4a6d2f336d95681df43720e2b25b045dc3ed44ae6d30a5ce2f5dff/markdown_include_variants-0.0.8.tar.gz", hash = "sha256:46d812340c64dcd3646b1eaa356bafb31626dd7b4955d15c44ff8c48c6357227", size = 46882, upload-time = "2025-12-12T16:11:04.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/0e/130958e7ec50d13f2ee7b6c142df5c352520a9251baf1652e41262703857/markdown_include_variants-0.0.8-py3-none-any.whl", hash = "sha256:425a300ae25fbcd598506cba67859a9dfa047333e869e0ff2e11a5e354b326dc", size = 8120, upload-time = "2025-12-12T16:11:02.881Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "mcp" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "httpx-sse", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pywin32", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/2b/916852a5668f45d8787378461eaa1244876d77575ffef024483c94c0649c/mcp-1.19.0.tar.gz", hash = "sha256:213de0d3cd63f71bc08ffe9cc8d4409cc87acffd383f6195d2ce0457c021b5c1", size = 444163, upload-time = "2025-10-24T01:11:15.839Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/a3/3e71a875a08b6a830b88c40bc413bff01f1650f1efe8a054b5e90a9d4f56/mcp-1.19.0-py3-none-any.whl", hash = "sha256:f5907fe1c0167255f916718f376d05f09a830a215327a3ccdd5ec8a519f2e572", size = 170105, upload-time = "2025-10-24T01:11:14.151Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mdx-include" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cyclic" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rcslice" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/f0/f395a9cf164471d3c7bbe58cbd64d74289575a8b85a962b49a804ab7ed34/mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297", size = 15051, upload-time = "2022-07-26T05:46:14.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/40/6844997dee251103c5a4c4eb0d1d2f2162b7c29ffc4e86de3cd68d269be2/mdx_include-1.4.2-py3-none-any.whl", hash = "sha256:cfbeadd59985f27a9b70cb7ab0a3d209892fe1bb1aa342df055e0b135b3c9f34", size = 11591, upload-time = "2022-07-26T05:46:11.518Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mistralai" +version = "1.9.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport" }, + { name = "httpx" }, + { name = "invoke" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/8d/d8b7af67a966b6f227024e1cb7287fc19901a434f87a5a391dcfe635d338/mistralai-1.9.11.tar.gz", hash = "sha256:3df9e403c31a756ec79e78df25ee73cea3eb15f86693773e16b16adaf59c9b8a", size = 208051, upload-time = "2025-10-02T15:53:40.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/76/4ce12563aea5a76016f8643eff30ab731e6656c845e9e4d090ef10c7b925/mistralai-1.9.11-py3-none-any.whl", hash = "sha256:7a3dc2b8ef3fceaa3582220234261b5c4e3e03a972563b07afa150e44a25a6d3", size = 442796, upload-time = "2025-10-02T15:53:39.134Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "mergedeep" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-macros-plugin" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hjson" }, + { name = "jinja2" }, + { name = "mkdocs" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "super-collections" }, + { name = "termcolor", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "termcolor", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/42/bb2ceed148c77f82b57c6d3b7f584f0f34ababf7a9a8ff85809380d1f400/mkdocs_macros_plugin-1.4.1.tar.gz", hash = "sha256:55a9c93871e3744cdeb0736316783d60830a6d5d97b1132364e6b491607f2332", size = 35094, upload-time = "2025-10-25T12:37:20.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/ec/e6e96a7ae8df414f03f43681821234b0d3b86666f7b91f70ab26775a8809/mkdocs_macros_plugin-1.4.1-py3-none-any.whl", hash = "sha256:5a9e483f6056fe7ad0923802affe699233ca468672e20a9640dba237165b3240", size = 40155, upload-time = "2025-10-25T12:37:19.417Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocs-redirects" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/a8/6d44a6cf07e969c7420cb36ab287b0669da636a2044de38a7d2208d5a758/mkdocs_redirects-1.2.2.tar.gz", hash = "sha256:3094981b42ffab29313c2c1b8ac3969861109f58b2dd58c45fc81cd44bfa0095", size = 7162, upload-time = "2024-11-07T14:57:21.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ec/38443b1f2a3821bbcb24e46cd8ba979154417794d54baf949fefde1c2146/mkdocs_redirects-1.2.2-py3-none-any.whl", hash = "sha256:7dbfa5647b79a3589da4401403d69494bd1f4ad03b9c15136720367e1f340ed5", size = 6142, upload-time = "2024-11-07T14:57:19.143Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350, upload-time = "2025-09-19T10:49:26.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/2c/f0dc4e1ee7f618f5bff7e05898d20bf8b6e7fa612038f768bfa295f136a4/mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82", size = 36704, upload-time = "2025-09-19T10:49:24.805Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mkdocstrings-python", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mkdocs-autorefs", marker = "python_full_version < '3.10'" }, + { name = "mkdocstrings", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/ae/58ab2bfbee2792e92a98b97e872f7c003deb903071f75d8d83aa55db28fa/mkdocstrings_python-1.18.2.tar.gz", hash = "sha256:4ad536920a07b6336f50d4c6d5603316fafb1172c5c882370cbbc954770ad323", size = 207972, upload-time = "2025-08-28T16:11:19.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/8f/ce008599d9adebf33ed144e7736914385e8537f5fc686fdb7cceb8c22431/mkdocstrings_python-1.18.2-py3-none-any.whl", hash = "sha256:944fe6deb8f08f33fa936d538233c4036e9f53e840994f6146e8e94eb71b600d", size = 138215, upload-time = "2025-08-28T16:11:18.176Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs-autorefs", marker = "python_full_version >= '3.10'" }, + { name = "mkdocstrings", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "openai" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/b1/12fe1c196bea326261718eb037307c1c1fe1dedc2d2d4de777df822e6238/openai-2.14.0.tar.gz", hash = "sha256:419357bedde9402d23bf8f2ee372fca1985a73348debba94bddff06f19459952", size = 626938, upload-time = "2025-12-19T03:28:45.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/4b/7c1a00c2c3fbd004253937f7520f692a9650767aa73894d7a34f0d65d3f4/openai-2.14.0-py3-none-any.whl", hash = "sha256:7ea40aca4ffc4c4a776e77679021b47eec1160e341f42ae086ba949c9dcc9183", size = 1067558, upload-time = "2025-12-19T03:28:43.727Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" }, + { url = "https://files.pythonhosted.org/packages/03/2e/b136dd6bf30ef5143fbe76a4c142828b55ccc618be490201e9073ad954a1/orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870", size = 132467, upload-time = "2025-12-06T15:53:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fc/ae99bfc1e1887d20a0268f0e2686eb5b13d0ea7bbe01de2b566febcd2130/orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09", size = 130702, upload-time = "2025-12-06T15:53:46.659Z" }, + { url = "https://files.pythonhosted.org/packages/6e/43/ef7912144097765997170aca59249725c3ab8ef6079f93f9d708dd058df5/orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd", size = 135907, upload-time = "2025-12-06T15:53:48.487Z" }, + { url = "https://files.pythonhosted.org/packages/3f/da/24d50e2d7f4092ddd4d784e37a3fa41f22ce8ed97abc9edd222901a96e74/orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac", size = 139935, upload-time = "2025-12-06T15:53:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/02/4a/b4cb6fcbfff5b95a3a019a8648255a0fac9b221fbf6b6e72be8df2361feb/orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e", size = 137541, upload-time = "2025-12-06T15:53:51.226Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/a11bd129f18c2377c27b2846a9d9be04acec981f770d711ba0aaea563984/orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f", size = 139031, upload-time = "2025-12-06T15:53:52.309Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/d7b77d7911574733a036bb3e8ad7053ceb2b7d6ea42208b9dbc55b23b9ed/orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18", size = 141622, upload-time = "2025-12-06T15:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/93/41/332db96c1de76b2feda4f453e91c27202cd092835936ce2b70828212f726/orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a", size = 413800, upload-time = "2025-12-06T15:53:54.866Z" }, + { url = "https://files.pythonhosted.org/packages/76/e1/5a0d148dd1f89ad2f9651df67835b209ab7fcb1118658cf353425d7563e9/orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7", size = 151198, upload-time = "2025-12-06T15:53:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/0d/96/8db67430d317a01ae5cf7971914f6775affdcfe99f5bff9ef3da32492ecc/orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401", size = 141984, upload-time = "2025-12-06T15:53:57.746Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/40d21e1aa1ac569e521069228bb29c9b5a350344ccf922a0227d93c2ed44/orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8", size = 135272, upload-time = "2025-12-06T15:53:59.769Z" }, + { url = "https://files.pythonhosted.org/packages/c4/7e/d0e31e78be0c100e08be64f48d2850b23bcb4d4c70d114f4e43b39f6895a/orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167", size = 133360, upload-time = "2025-12-06T15:54:01.25Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" }, + { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" }, + { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" }, + { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" }, + { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" }, + { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" }, + { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" }, + { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" }, + { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" }, + { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" }, + { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" }, + { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" }, + { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" }, + { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" }, + { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" }, + { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" }, + { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" }, + { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" }, + { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" }, + { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" }, + { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" }, + { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" }, + { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" }, + { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" }, + { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" }, + { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" }, + { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" }, + { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" }, + { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/7b682849dd4c9fb701a981669b964ea700516ecbd8e88f62aae07c6852bd/orjson-3.11.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1b280e2d2d284a6713b0cfec7b08918ebe57df23e3f76b27586197afca3cb1e9", size = 245298, upload-time = "2025-12-06T15:55:20.984Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3f/194355a9335707a15fdc79ddc670148987b43d04712dd26898a694539ce6/orjson-3.11.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d8a112b274fae8c5f0f01954cb0480137072c271f3f4958127b010dfefaec", size = 132150, upload-time = "2025-12-06T15:55:22.364Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/d74b3a986d37e6c2e04b8821c62927620c9a1924bb49ea51519a87751b86/orjson-3.11.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0a2ae6f09ac7bd47d2d5a5305c1d9ed08ac057cda55bb0a49fa506f0d2da00", size = 130490, upload-time = "2025-12-06T15:55:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/b2/16/ebd04c38c1db01e493a68eee442efdffc505a43112eccd481e0146c6acc2/orjson-3.11.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0d87bd1896faac0d10b4f849016db81a63e4ec5df38757ffae84d45ab38aa71", size = 135726, upload-time = "2025-12-06T15:55:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/06/64/2ce4b2c09a099403081c37639c224bdcdfe401138bd66fed5c96d4f8dbd3/orjson-3.11.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:801a821e8e6099b8c459ac7540b3c32dba6013437c57fdcaec205b169754f38c", size = 139640, upload-time = "2025-12-06T15:55:26.535Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e2/425796df8ee1d7cea3a7edf868920121dd09162859dbb76fffc9a5c37fd3/orjson-3.11.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a0f6ac618c98c74b7fbc8c0172ba86f9e01dbf9f62aa0b1776c2231a7bffe5", size = 137289, upload-time = "2025-12-06T15:55:27.78Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/88e482eb8e899a037dcc9eff85ef117a568e6ca1ffa1a2b2be3fcb51b7bb/orjson-3.11.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea7339bdd22e6f1060c55ac31b6a755d86a5b2ad3657f2669ec243f8e3b2bdb", size = 138761, upload-time = "2025-12-06T15:55:29.388Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/131dd6d32eeb74c513bfa487f434a2150811d0fbd9cb06689284f2f21b34/orjson-3.11.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4dad582bc93cef8f26513e12771e76385a7e6187fd713157e971c784112aad56", size = 141357, upload-time = "2025-12-06T15:55:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/7a/90/e4a0abbcca7b53e9098ac854f27f5ed9949c796f3c760bc04af997da0eb2/orjson-3.11.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:0522003e9f7fba91982e83a97fec0708f5a714c96c4209db7104e6b9d132f111", size = 413638, upload-time = "2025-12-06T15:55:32.344Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c2/df91e385514924120001ade9cd52d6295251023d3bfa2c0a01f38cfc485a/orjson-3.11.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7403851e430a478440ecc1258bcbacbfbd8175f9ac1e39031a7121dd0de05ff8", size = 150972, upload-time = "2025-12-06T15:55:33.725Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ff/c76cc5a30a4451191ff1b868a331ad1354433335277fc40931f5fc3cab9d/orjson-3.11.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5f691263425d3177977c8d1dd896cde7b98d93cbf390b2544a090675e83a6a0a", size = 141729, upload-time = "2025-12-06T15:55:35.317Z" }, + { url = "https://files.pythonhosted.org/packages/27/c3/7830bf74389ea1eaab2b017d8b15d1cab2bb0737d9412dfa7fb8644f7d78/orjson-3.11.5-cp39-cp39-win32.whl", hash = "sha256:61026196a1c4b968e1b1e540563e277843082e9e97d78afa03eb89315af531f1", size = 135100, upload-time = "2025-12-06T15:55:36.57Z" }, + { url = "https://files.pythonhosted.org/packages/69/e6/babf31154e047e465bc194eb72d1326d7c52ad4d7f50bf92b02b3cacda5c/orjson-3.11.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b94b947ac08586af635ef922d69dc9bc63321527a3a04647f4986a73f4bd30", size = 133189, upload-time = "2025-12-06T15:55:38.143Z" }, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/b9/6eb731b52f132181a9144bbe77ff82117f6b2d2fbfba49aaab2c014c4760/pathspec-1.0.2.tar.gz", hash = "sha256:fa32b1eb775ed9ba8d599b22c5f906dc098113989da2c00bf8b210078ca7fb92", size = 130502, upload-time = "2026-01-08T04:33:27.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl", hash = "sha256:62f8558917908d237d399b9b338ef455a814801a4688bc41074b25feefd93472", size = 54844, upload-time = "2026-01-08T04:33:26.4Z" }, +] + +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478, upload-time = "2025-07-01T09:15:52.209Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522, upload-time = "2025-07-01T09:15:54.162Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376, upload-time = "2025-07-03T13:11:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020, upload-time = "2025-07-03T13:11:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732, upload-time = "2025-07-01T09:15:56.111Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404, upload-time = "2025-07-01T09:15:58.245Z" }, + { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760, upload-time = "2025-07-01T09:16:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534, upload-time = "2025-07-01T09:16:02.29Z" }, + { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091, upload-time = "2025-07-01T09:16:04.4Z" }, + { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091, upload-time = "2025-07-01T09:16:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632, upload-time = "2025-07-01T09:16:08.142Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "playwright" +version = "1.57.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", version = "3.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "greenlet", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyee" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/b6/e17543cea8290ae4dced10be21d5a43c360096aa2cce0aa7039e60c50df3/playwright-1.57.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:9351c1ac3dfd9b3820fe7fc4340d96c0d3736bb68097b9b7a69bd45d25e9370c", size = 41985039, upload-time = "2025-12-09T08:06:18.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/04/ef95b67e1ff59c080b2effd1a9a96984d6953f667c91dfe9d77c838fc956/playwright-1.57.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4a9d65027bce48eeba842408bcc1421502dfd7e41e28d207e94260fa93ca67e", size = 40775575, upload-time = "2025-12-09T08:06:22.105Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/5563850322a663956c927eefcf1457d12917e8f118c214410e815f2147d1/playwright-1.57.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:99104771abc4eafee48f47dac2369e0015516dc1ce8c409807d2dd440828b9a4", size = 41985042, upload-time = "2025-12-09T08:06:25.357Z" }, + { url = "https://files.pythonhosted.org/packages/56/61/3a803cb5ae0321715bfd5247ea871d25b32c8f372aeb70550a90c5f586df/playwright-1.57.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:284ed5a706b7c389a06caa431b2f0ba9ac4130113c3a779767dda758c2497bb1", size = 45975252, upload-time = "2025-12-09T08:06:29.186Z" }, + { url = "https://files.pythonhosted.org/packages/83/d7/b72eb59dfbea0013a7f9731878df8c670f5f35318cedb010c8a30292c118/playwright-1.57.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a1bae6c0a07839cdeaddbc0756b3b2b85e476c07945f64ece08f1f956a86f1", size = 45706917, upload-time = "2025-12-09T08:06:32.549Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/3fc9ebd7c95ee54ba6a68d5c0bc23e449f7235f4603fc60534a364934c16/playwright-1.57.0-py3-none-win32.whl", hash = "sha256:1dd93b265688da46e91ecb0606d36f777f8eadcf7fbef12f6426b20bf0c9137c", size = 36553860, upload-time = "2025-12-09T08:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/58/d4/dcdfd2a33096aeda6ca0d15584800443dd2be64becca8f315634044b135b/playwright-1.57.0-py3-none-win_amd64.whl", hash = "sha256:6caefb08ed2c6f29d33b8088d05d09376946e49a73be19271c8cd5384b82b14c", size = 36553864, upload-time = "2025-12-09T08:06:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/6a/60/fe31d7e6b8907789dcb0584f88be741ba388413e4fbce35f1eba4e3073de/playwright-1.57.0-py3-none-win_arm64.whl", hash = "sha256:5f065f5a133dbc15e6e7c71e7bc04f258195755b1c32a432b792e28338c8335e", size = 32837940, upload-time = "2025-12-09T08:06:42.268Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prek" +version = "0.2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/a7/1e07536315f77d7b233cbf3dd916dc3424239c435ee0a0110c9b2cbcf6b0/prek-0.2.22.tar.gz", hash = "sha256:5abbda8bae0a63a18d3fe573162e8504a7b100e3603169cc2d06053891a02d7c", size = 267212, upload-time = "2025-12-13T12:57:51.797Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/fe/ba9a940adc55d78b96b58376a8752e95261402c1e5812acce6ea1a000fb8/prek-0.2.22-py3-none-linux_armv6l.whl", hash = "sha256:d026b2d75529a743466000e8dd058d3d5e7c597c34905b333f2ede3d24cb23f1", size = 4798026, upload-time = "2025-12-13T12:57:45.286Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/459cf510491271b08d19b4ef34f8293440eb472e633f4ffaf34179f39a12/prek-0.2.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:60b5bec94fa9f59fe5a9e90554c7346ceef81ea33d01bb18172d2576b07ac449", size = 4894023, upload-time = "2025-12-13T12:57:40.102Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0b/59e0438b1e7d1b6fa3f14174a916d369e27c421f8876f7ec7c7a52fbfae7/prek-0.2.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a0c7c6ceee536122916d32d26b6fa4fac9e95ba28631901164ffc0b0fed28a9e", size = 4615858, upload-time = "2025-12-13T12:57:57.471Z" }, + { url = "https://files.pythonhosted.org/packages/e9/27/ea40cf715717298fdf802da2b15a2c4445b8c114aae28cab6bf794d65670/prek-0.2.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c90273bef7b638dfc36dede62c494f958456330375ffce891c68321b2a7b46ba", size = 4810206, upload-time = "2025-12-13T12:57:46.534Z" }, + { url = "https://files.pythonhosted.org/packages/b5/12/d1c3db35839492236afb8642a2818d5b413e5fce4ea909bc7ddfb3d4591a/prek-0.2.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d8ecf202073433b87df2671a98bc44d3b68bb5711f7119b50b7bd65c2a67f13", size = 4722439, upload-time = "2025-12-13T12:57:48.106Z" }, + { url = "https://files.pythonhosted.org/packages/64/a0/0f24a9cacd5d78119f47063d860e03fa42b4d7dcf6803a49b0bef51b771b/prek-0.2.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4f26d76247ce7671cf5d9786e7fc86fdb43c065fd5507e8d64b3de7fd5e4447", size = 5037705, upload-time = "2025-12-13T12:57:50.596Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6e/7616f84141755f1d9fe232f0bd06589421ae0dabd99180fdae2840d22ae8/prek-0.2.22-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac1f0ea2c82e35eb0ffc98dfbcd9ee34cfd7350b64f97198da4c311a271cdb8f", size = 5453199, upload-time = "2025-12-13T12:57:38.458Z" }, + { url = "https://files.pythonhosted.org/packages/51/80/542a583db9b27bfd34954243666e451b266513bc742e0491cd61ff1b390e/prek-0.2.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f5d5131b9e57548f64d74665fd4414a8deb603a67d52ee18b3e6540cdb77733", size = 5399635, upload-time = "2025-12-13T12:57:43.359Z" }, + { url = "https://files.pythonhosted.org/packages/49/06/ca4e6fee73e14e1aced90f5c83b9cdf9a8e1c3b1aa1e4f45a2a65de05a28/prek-0.2.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a768484e1c94a33228765f63701261316b64e11c482abe2a35c54045d3f81feb", size = 5498340, upload-time = "2025-12-13T12:57:41.827Z" }, + { url = "https://files.pythonhosted.org/packages/85/a8/9636fc782db9c22d1740a8e5dc4e1ffc3a28099d074f812da46332e7c7a7/prek-0.2.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c941c8503ea537a84ea97074dc97b0f0dfd9861864883eb8b90586ed321847e", size = 5078431, upload-time = "2025-12-13T12:57:31.664Z" }, + { url = "https://files.pythonhosted.org/packages/4f/29/e78d2f444cf1f097aaaefee8910d7b9fe34195f06b086e0d2153b6c66e07/prek-0.2.22-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:946c6cfe18b17a7b53c49a389bf65f1e8e45a1b96bfdaeeacde21f5b5ca2d149", size = 4820871, upload-time = "2025-12-13T12:57:33.074Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ec/779db6c35663e949b3f9989c584297aa115d3cc44822c149dbe40d51cd14/prek-0.2.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2cffd5809cf678b4300378d612b5da12cd2183ddc7aee78178db0b1ea48f0069", size = 4834431, upload-time = "2025-12-13T12:57:34.65Z" }, + { url = "https://files.pythonhosted.org/packages/1d/18/12bb4fece680457f4d4f13d21c5784675ce8b1db5c968261348c52087232/prek-0.2.22-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:f91df793cbc28647863eb54d578f37782736726671838ca92c9d0601329cb928", size = 4709742, upload-time = "2025-12-13T12:57:52.706Z" }, + { url = "https://files.pythonhosted.org/packages/3f/27/de1d9d037f59393568713121f4bfcea11cd546dcf96f214827983b8beccf/prek-0.2.22-py3-none-musllinux_1_1_i686.whl", hash = "sha256:3c40ba36b3e89817b20efe6163fd15387b81caf1f489060265d84103ae6e5184", size = 4925048, upload-time = "2025-12-13T12:57:49.348Z" }, + { url = "https://files.pythonhosted.org/packages/49/bf/d40eef2e5ccbc520da94c2463450d0ecab598c092684002b463fd5491ff6/prek-0.2.22-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a4154a419581723d12eccaa5b1d27686283c5c78b753c1984270d7e144a15fa7", size = 5192083, upload-time = "2025-12-13T12:57:35.957Z" }, + { url = "https://files.pythonhosted.org/packages/41/ba/11ea837a876dcc7f5df85962bc560c8627a962261f046a1615b0a6016b01/prek-0.2.22-py3-none-win32.whl", hash = "sha256:9fd3d629a256ce3171bebc3183f9c608022fff0db19a511307ab0f4c7682d5e3", size = 4586129, upload-time = "2025-12-13T12:57:54.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8c/05ab6d11ac670664c99944e4819a77a63360aab253d8daf4ae411c705bcd/prek-0.2.22-py3-none-win_amd64.whl", hash = "sha256:ad7997ae4bef4fccc0a6761c00479bdd44f2a5bb7eb97aebda3b42fe785e10a1", size = 5273787, upload-time = "2025-12-13T12:57:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/38/7a/53e8a550df705b5bf78a589c4e11d21485ac38c1a65e9c98fc3169a5eb25/prek-0.2.22-py3-none-win_arm64.whl", hash = "sha256:2442c0f12bd57675124542a92f5c799e7ffe52dc7cd98301c43c361849a3aef6", size = 4941186, upload-time = "2025-12-13T12:57:56.214Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "pwdlib" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/82/a0/9daed437a6226f632a25d98d65d60ba02bdafa920c90dcb6454c611ead6c/pwdlib-0.2.1.tar.gz", hash = "sha256:9a1d8a8fa09a2f7ebf208265e55d7d008103cbdc82b9e4902ffdd1ade91add5e", size = 11699, upload-time = "2024-08-19T06:48:59.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/f3/0dae5078a486f0fdf4d4a1121e103bc42694a9da9bea7b0f2c63f29cfbd3/pwdlib-0.2.1-py3-none-any.whl", hash = "sha256:1823dc6f22eae472b540e889ecf57fd424051d6a4023ec0bcf7f0de2d9d7ef8c", size = 8082, upload-time = "2024-08-19T06:49:00.997Z" }, +] + +[package.optional-dependencies] +argon2 = [ + { name = "argon2-cffi", version = "23.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "pwdlib" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/41/a7c0d8a003c36ce3828ae3ed0391fe6a15aad65f082dbd6bec817ea95c0b/pwdlib-0.3.0.tar.gz", hash = "sha256:6ca30f9642a1467d4f5d0a4d18619de1c77f17dfccb42dd200b144127d3c83fc", size = 215810, upload-time = "2025-10-25T12:44:24.395Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/0c/9086a357d02a050fbb3270bf5043ac284dbfb845670e16c9389a41defc9e/pwdlib-0.3.0-py3-none-any.whl", hash = "sha256:f86c15c138858c09f3bba0a10984d4f9178158c55deaa72eac0210849b1a140d", size = 8633, upload-time = "2025-10-25T12:44:23.406Z" }, +] + +[package.optional-dependencies] +argon2 = [ + { name = "argon2-cffi", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-ai" +version = "0.4.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "huggingface", "mcp", "mistral", "openai", "retries", "vertexai"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/67/eef2d1d64579c20ff732244c8f46f5b10401a47122b2404fd1a786860e0e/pydantic_ai-0.4.10.tar.gz", hash = "sha256:21cda3b62706dfdd5b9d662bb9a2ead50d51951f6ee0bbee45320b8752a95055", size = 43555371, upload-time = "2025-07-30T23:03:50.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/9c/3ea9ce322448e963fd84abb61985fa2d2ea0cb3f09bf0634cf43b2f49de6/pydantic_ai-0.4.10-py3-none-any.whl", hash = "sha256:4822325c92a57c9b45bdfd993106558e2397362b98ec211670d106c4b073ddd8", size = 10194, upload-time = "2025-07-30T23:03:42.435Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "0.4.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/fc/4123faf9372807e487c83acc858482f6d5a2f39ee6c1e25a895f23f700b6/pydantic_ai_slim-0.4.10.tar.gz", hash = "sha256:c9f6904aaa91c0309f91cc3a5617c570e153afbbb9888ee52190f58f029640f0", size = 189595, upload-time = "2025-07-30T23:03:55.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/d7/b1f6cf2eb9d24086de8a7f9e08956421fde514bcb7a4a3594b108e12636e/pydantic_ai_slim-0.4.10-py3-none-any.whl", hash = "sha256:49a386d6a900b4c1a81bca3469522a4c0eaab5a46a3953d1ffda8f0f2865ed97", size = 254994, upload-time = "2025-07-30T23:03:45.03Z" }, +] + +[package.optional-dependencies] +ag-ui = [ + { name = "ag-ui-protocol" }, + { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +anthropic = [ + { name = "anthropic" }, +] +bedrock = [ + { name = "boto3" }, +] +cli = [ + { name = "argcomplete" }, + { name = "prompt-toolkit" }, + { name = "rich" }, +] +cohere = [ + { name = "cohere", marker = "sys_platform != 'emscripten'" }, +] +evals = [ + { name = "pydantic-evals" }, +] +google = [ + { name = "google-genai", version = "1.47.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "google-genai", version = "1.57.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +groq = [ + { name = "groq" }, +] +huggingface = [ + { name = "huggingface-hub" }, +] +mcp = [ + { name = "mcp", marker = "python_full_version >= '3.10'" }, +] +mistral = [ + { name = "mistralai" }, +] +openai = [ + { name = "openai" }, +] +retries = [ + { name = "tenacity" }, +] +vertexai = [ + { name = "google-auth" }, + { name = "requests" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, + { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-evals" +version = "0.4.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "eval-type-backport", marker = "python_full_version < '3.11'" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "pydantic-ai-slim" }, + { name = "pyyaml" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/b7/3f925d6ec9a2627e3310e87c2131514c3b44427c8975fafe83453825cfca/pydantic_evals-0.4.10.tar.gz", hash = "sha256:310acc7559d601743a101c606e50c0c5a9592bfc53cb733c7e54143b39e0fc97", size = 43729, upload-time = "2025-07-30T23:03:56.943Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/9f/7972c450d74c2b0f6d9f9de644ac33c4c63f0f2156f8acd37a274b1f43a3/pydantic_evals-0.4.10-py3-none-any.whl", hash = "sha256:5fda10c5ced2c99f03b407bd56645574598e6daab0e5be2ed7056e815e6037f6", size = 52511, upload-time = "2025-07-30T23:03:46.717Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/35/2fee58b1316a73e025728583d3b1447218a97e621933fc776fb8c0f2ebdd/pydantic_extra_types-2.11.0.tar.gz", hash = "sha256:4e9991959d045b75feb775683437a97991d02c138e00b59176571db9ce634f0e", size = 157226, upload-time = "2025-12-31T16:18:27.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/17/fabd56da47096d240dd45ba627bead0333b0cf0ee8ada9bec579287dadf3/pydantic_extra_types-2.11.0-py3-none-any.whl", hash = "sha256:84b864d250a0fc62535b7ec591e36f2c5b4d1325fa0017eb8cda9aeb63b374a6", size = 74296, upload-time = "2025-12-31T16:18:26.38Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "0.4.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/46/62b83bd471a8743c41aba6693f55b746b8ee1294f64b7e9f42db7c3fd4b5/pydantic_graph-0.4.10.tar.gz", hash = "sha256:034063ac0ce2143a877a4fac563520492e70dde42d262b22c928f081d7759c5b", size = 21978, upload-time = "2025-07-30T23:03:57.986Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/55/0026897b1cd6ab6b179b25c06b1dff496a3ebb83e30c6acb1a5a40e80cd1/pydantic_graph-0.4.10-py3-none-any.whl", hash = "sha256:e5128d5e370a6391aa6441eaefff6b3a139e583e0ac6755d83b88c9df9c6c6a7", size = 27578, upload-time = "2025-07-30T23:03:47.925Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "python-dotenv", marker = "python_full_version < '3.10'" }, + { name = "typing-inspection", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pyee" +version = "13.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" }, +] + +[[package]] +name = "pygithub" +version = "2.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyjwt", extra = ["crypto"] }, + { name = "pynacl" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/74/e560bdeffea72ecb26cff27f0fad548bbff5ecc51d6a155311ea7f9e4c4c/pygithub-2.8.1.tar.gz", hash = "sha256:341b7c78521cb07324ff670afd1baa2bf5c286f8d9fd302c1798ba594a5400c9", size = 2246994, upload-time = "2025-09-02T17:41:54.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/ba/7049ce39f653f6140aac4beb53a5aaf08b4407b6a3019aae394c1c5244ff/pygithub-2.8.1-py3-none-any.whl", hash = "sha256:23a0a5bca93baef082e03411bf0ce27204c32be8bfa7abc92fe4a3e132936df0", size = 432709, upload-time = "2025-09-02T17:41:52.947Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c", size = 78825, upload-time = "2024-08-01T15:01:08.445Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", size = 22344, upload-time = "2024-08-01T15:01:06.481Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/35/e3814a5b7df295df69d035cfb8aab78b2967cdf11fcfae7faed726b66664/pymdown_extensions-10.20.tar.gz", hash = "sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52", size = 852774, upload-time = "2025-12-31T19:59:42.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" }, +] + +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-codspeed" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "pytest" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/e8/27fcbe6516a1c956614a4b61a7fccbf3791ea0b992e07416e8948184327d/pytest_codspeed-4.2.0.tar.gz", hash = "sha256:04b5d0bc5a1851ba1504d46bf9d7dbb355222a69f2cd440d54295db721b331f7", size = 113263, upload-time = "2025-10-24T09:02:55.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/b8/d599a466c50af3f04001877ae8b17c12b803f3b358235736b91a0769de0d/pytest_codspeed-4.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609828b03972966b75b9b7416fa2570c4a0f6124f67e02d35cd3658e64312a7b", size = 261943, upload-time = "2025-10-24T09:02:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/74/19/ccc1a2fcd28357a8db08ba6b60f381832088a3850abc262c8e0b3406491a/pytest_codspeed-4.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23a0c0fbf8bb4de93a3454fd9e5efcdca164c778aaef0a9da4f233d85cb7f5b8", size = 250782, upload-time = "2025-10-24T09:02:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2d/f0083a2f14ecf008d961d40439a71da0ae0d568e5f8dc2fccd3e8a2ab3e4/pytest_codspeed-4.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2de87bde9fbc6fd53f0fd21dcf2599c89e0b8948d49f9bad224edce51c47e26b", size = 261960, upload-time = "2025-10-24T09:02:40.665Z" }, + { url = "https://files.pythonhosted.org/packages/5f/0c/1f514c553db4ea5a69dfbe2706734129acd0eca8d5101ec16f1dd00dbc0f/pytest_codspeed-4.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95aeb2479ca383f6b18e2cc9ebcd3b03ab184980a59a232aea6f370bbf59a1e3", size = 250808, upload-time = "2025-10-24T09:02:42.07Z" }, + { url = "https://files.pythonhosted.org/packages/81/04/479905bd6653bc981c0554fcce6df52d7ae1594e1eefd53e6cf31810ec7f/pytest_codspeed-4.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d4fefbd4ae401e2c60f6be920a0be50eef0c3e4a1f0a1c83962efd45be38b39", size = 262084, upload-time = "2025-10-24T09:02:43.155Z" }, + { url = "https://files.pythonhosted.org/packages/d2/46/d6f345d7907bac6cbb6224bd697ecbc11cf7427acc9e843c3618f19e3476/pytest_codspeed-4.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:309b4227f57fcbb9df21e889ea1ae191d0d1cd8b903b698fdb9ea0461dbf1dfe", size = 251100, upload-time = "2025-10-24T09:02:44.168Z" }, + { url = "https://files.pythonhosted.org/packages/de/dc/e864f45e994a50390ff49792256f1bdcbf42f170e3bc0470ee1a7d2403f3/pytest_codspeed-4.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72aab8278452a6d020798b9e4f82780966adb00f80d27a25d1274272c54630d5", size = 262057, upload-time = "2025-10-24T09:02:45.791Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1c/f1d2599784486879cf6579d8d94a3e22108f0e1f130033dab8feefd29249/pytest_codspeed-4.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:684fcd9491d810ded653a8d38de4835daa2d001645f4a23942862950664273f8", size = 251013, upload-time = "2025-10-24T09:02:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fd/eafd24db5652a94b4d00fe9b309b607de81add0f55f073afb68a378a24b6/pytest_codspeed-4.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50794dabea6ec90d4288904452051e2febace93e7edf4ca9f2bce8019dd8cd37", size = 262065, upload-time = "2025-10-24T09:02:48.018Z" }, + { url = "https://files.pythonhosted.org/packages/f9/14/8d9340d7dc0ae647991b28a396e16b3403e10def883cde90d6b663d3f7ec/pytest_codspeed-4.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0ebd87f2a99467a1cfd8e83492c4712976e43d353ee0b5f71cbb057f1393aca", size = 251057, upload-time = "2025-10-24T09:02:49.102Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/48cf6afbca55bc7c8c93c3d4ae926a1068bcce3f0241709db19b078d5418/pytest_codspeed-4.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbbb2d61b85bef8fc7e2193f723f9ac2db388a48259d981bbce96319043e9830", size = 267983, upload-time = "2025-10-24T09:02:50.558Z" }, + { url = "https://files.pythonhosted.org/packages/33/86/4407341efb5dceb3e389635749ce1d670542d6ca148bd34f9d5334295faf/pytest_codspeed-4.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:748411c832147bfc85f805af78a1ab1684f52d08e14aabe22932bbe46c079a5f", size = 256732, upload-time = "2025-10-24T09:02:51.603Z" }, + { url = "https://files.pythonhosted.org/packages/fe/60/c395c19c14a1345d41ac3f7f0a9b372b666e88f9ba1f71988215174882bb/pytest_codspeed-4.2.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:238e17abe8f08d8747fa6c7acff34fefd3c40f17a56a7847ca13dc8d6e8c6009", size = 261935, upload-time = "2025-10-24T09:02:52.702Z" }, + { url = "https://files.pythonhosted.org/packages/15/ed/442fb6a1832c2c9002653f24770873839b24c091bd2ed658090c7862c563/pytest_codspeed-4.2.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0881a736285f33b9a8894da8fe8e1775aa1a4310226abe5d1f0329228efb680c", size = 250770, upload-time = "2025-10-24T09:02:53.73Z" }, + { url = "https://files.pythonhosted.org/packages/25/0e/8cb71fd3ed4ed08c07aec1245aea7bc1b661ba55fd9c392db76f1978d453/pytest_codspeed-4.2.0-py3-none-any.whl", hash = "sha256:e81bbb45c130874ef99aca97929d72682733527a49f84239ba575b5cb843bab0", size = 113726, upload-time = "2025-10-24T09:02:54.785Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.21" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "rcslice" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/3e/abe47d91d5340b77b003baf96fdf8966c946eb4c5a704a844b5d03e6e578/rcslice-1.1.0.tar.gz", hash = "sha256:a2ce70a60690eb63e52b722e046b334c3aaec5e900b28578f529878782ee5c6e", size = 4414, upload-time = "2018-09-27T12:44:06.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/96/7935186fba032312eb8a75e6503440b0e6de76c901421f791408e4debd93/rcslice-1.1.0-py3-none-any.whl", hash = "sha256:1b12fc0c0ca452e8a9fd2b56ac008162f19e250906a4290a7e7a98be3200c2a6", size = 5180, upload-time = "2018-09-27T12:44:05.197Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/09/3f9b8d9daaf235195c626f21e03604c05b987404ee3bcacee0c1f67f2a8e/rich_toolkit-0.17.1.tar.gz", hash = "sha256:5af54df8d1dd9c8530e462e1bdcaed625c9b49f5a55b035aa0ba1c17bdb87c9a", size = 187925, upload-time = "2025-12-17T10:49:22.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/7b/15e55fa8a76d0d41bf34d965af78acdaf80a315907adb30de8b63c272694/rich_toolkit-0.17.1-py3-none-any.whl", hash = "sha256:96d24bb921ecd225ffce7c526a9149e74006410c05e6d405bd74ffd54d5631ed", size = 31412, upload-time = "2025-12-17T10:49:21.793Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/7a/b970cd0138b0ece72eb28f086e933f9ed75b795716ad3de5ab22994b3b54/rignore-0.7.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f3c74a7e5ee77aea669c95fdb3933f2a6c7549893700082e759128a29cf67e45", size = 884999, upload-time = "2025-11-05T20:42:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/23faca29616d8966ada63fb0e13c214107811fa9a0aba2275e4c7ca63bd5/rignore-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7202404958f5fe3474bac91f65350f0b1dde1a5e05089f2946549b7e91e79ec", size = 824824, upload-time = "2025-11-05T20:42:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/05a1e61f04cf2548524224f0b5f21ca19ea58f7273a863bac10846b8ff69/rignore-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bde7c5835fa3905bfb7e329a4f1d7eccb676de63da7a3f934ddd5c06df20597", size = 899121, upload-time = "2025-11-05T20:40:48.94Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/71518847e10bdbf359badad8800e4681757a01f4777b3c5e03dbde8a42d8/rignore-0.7.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:626c3d4ba03af266694d25101bc1d8d16eda49c5feb86cedfec31c614fceca7d", size = 873813, upload-time = "2025-11-05T20:41:04.71Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c8/32ae405d3e7fd4d9f9b7838f2fcca0a5005bb87fa514b83f83fd81c0df22/rignore-0.7.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a43841e651e7a05a4274b9026cc408d1912e64016ede8cd4c145dae5d0635be", size = 1168019, upload-time = "2025-11-05T20:41:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/013c955982bc5b4719bf9a5bea58be317eea28aa12bfd004025e3cd7c000/rignore-0.7.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7978c498dbf7f74d30cdb8859fe612167d8247f0acd377ae85180e34490725da", size = 942822, upload-time = "2025-11-05T20:41:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/90/fb/9a3f3156c6ed30bcd597e63690353edac1fcffe9d382ad517722b56ac195/rignore-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d22f72ab695c07d2d96d2a645208daff17084441b5d58c07378c9dd6f9c4c87", size = 959820, upload-time = "2025-11-05T20:42:06.364Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b2/93bf609633021e9658acaff24cfb055d8cdaf7f5855d10ebb35307900dda/rignore-0.7.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5bd8e1a91ed1a789b2cbe39eeea9204a6719d4f2cf443a9544b521a285a295f", size = 985050, upload-time = "2025-11-05T20:41:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/ec2d040469bdfd7b743df10f2201c5d285009a4263d506edbf7a06a090bb/rignore-0.7.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fc03efad5789365018e94ac4079f851a999bc154d1551c45179f7fcf45322", size = 1079164, upload-time = "2025-11-05T21:40:10.368Z" }, + { url = "https://files.pythonhosted.org/packages/df/26/4b635f4ea5baf4baa8ba8eee06163f6af6e76dfbe72deb57da34bb24b19d/rignore-0.7.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ce2617fe28c51367fd8abfd4eeea9e61664af63c17d4ea00353d8ef56dfb95fa", size = 1139028, upload-time = "2025-11-05T21:40:27.977Z" }, + { url = "https://files.pythonhosted.org/packages/6a/54/a3147ebd1e477b06eb24e2c2c56d951ae5faa9045b7b36d7892fec5080d9/rignore-0.7.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c4ad2cee85068408e7819a38243043214e2c3047e9bd4c506f8de01c302709e", size = 1119024, upload-time = "2025-11-05T21:40:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f4/27475db769a57cff18fe7e7267b36e6cdb5b1281caa185ba544171106cba/rignore-0.7.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:02cd240bfd59ecc3907766f4839cbba20530a2e470abca09eaa82225e4d946fb", size = 1128531, upload-time = "2025-11-05T21:41:02.734Z" }, + { url = "https://files.pythonhosted.org/packages/97/32/6e782d3b352e4349fa0e90bf75b13cb7f11d8908b36d9e2b262224b65d9a/rignore-0.7.6-cp310-cp310-win32.whl", hash = "sha256:fe2bd8fa1ff555259df54c376abc73855cb02628a474a40d51b358c3a1ddc55b", size = 646817, upload-time = "2025-11-05T21:41:47.51Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8a/53185c69abb3bb362e8a46b8089999f820bf15655629ff8395107633c8ab/rignore-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:d80afd6071c78baf3765ec698841071b19e41c326f994cfa69b5a1df676f5d39", size = 727001, upload-time = "2025-11-05T21:41:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/25/41/b6e2be3069ef3b7f24e35d2911bd6deb83d20ed5642ad81d5a6d1c015473/rignore-0.7.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:40be8226e12d6653abbebaffaea2885f80374c1c8f76fe5ca9e0cadd120a272c", size = 885285, upload-time = "2025-11-05T20:42:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/ba7f561b6062402022887706a7f2b2c2e2e2a28f1e3839202b0a2f77e36d/rignore-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182f4e5e4064d947c756819446a7d4cdede8e756b8c81cf9e509683fe38778d7", size = 823882, upload-time = "2025-11-05T20:42:23.488Z" }, + { url = "https://files.pythonhosted.org/packages/f5/81/4087453df35a90b07370647b19017029324950c1b9137d54bf1f33843f17/rignore-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", size = 899362, upload-time = "2025-11-05T20:40:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c9/390a8fdfabb76d71416be773bd9f162977bd483084f68daf19da1dec88a6/rignore-0.7.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", size = 873633, upload-time = "2025-11-05T20:41:06.193Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/79404fcb0faa76edfbc9df0901f8ef18568d1104919ebbbad6d608c888d1/rignore-0.7.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", size = 1167633, upload-time = "2025-11-05T20:41:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/b3466d32d445d158a0aceb80919085baaae495b1f540fb942f91d93b5e5b/rignore-0.7.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", size = 941434, upload-time = "2025-11-05T20:41:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/e8/40/9cd949761a7af5bc27022a939c91ff622d29c7a0b66d0c13a863097dde2d/rignore-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", size = 959461, upload-time = "2025-11-05T20:42:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/b5/87/1e1a145731f73bdb7835e11f80da06f79a00d68b370d9a847de979575e6d/rignore-0.7.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25b3536d13a5d6409ce85f23936f044576eeebf7b6db1d078051b288410fc049", size = 985323, upload-time = "2025-11-05T20:41:52.735Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/1ecff992fc3f59c4fcdcb6c07d5f6c1e6dfb55ccda19c083aca9d86fa1c6/rignore-0.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", size = 1079173, upload-time = "2025-11-05T21:40:12.007Z" }, + { url = "https://files.pythonhosted.org/packages/17/18/162eedadb4c2282fa4c521700dbf93c9b14b8842e8354f7d72b445b8d593/rignore-0.7.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", size = 1139012, upload-time = "2025-11-05T21:40:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/a9ca398a8af74bb143ad66c2a31303c894111977e28b0d0eab03867f1b43/rignore-0.7.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c8ae562e5d1246cba5eaeb92a47b2a279e7637102828dde41dcbe291f529a3e", size = 1118827, upload-time = "2025-11-05T21:40:46.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/22/1c1a65047df864def9a047dbb40bc0b580b8289a4280e62779cd61ae21f2/rignore-0.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", size = 1128182, upload-time = "2025-11-05T21:41:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f4/1526eb01fdc2235aca1fd9d0189bee4021d009a8dcb0161540238c24166e/rignore-0.7.6-cp311-cp311-win32.whl", hash = "sha256:166ebce373105dd485ec213a6a2695986346e60c94ff3d84eb532a237b24a4d5", size = 646547, upload-time = "2025-11-05T21:41:49.439Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/dda0983e1845706beb5826459781549a840fe5a7eb934abc523e8cd17814/rignore-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:44f35ee844b1a8cea50d056e6a595190ce9d42d3cccf9f19d280ae5f3058973a", size = 727139, upload-time = "2025-11-05T21:41:34.367Z" }, + { url = "https://files.pythonhosted.org/packages/e3/47/eb1206b7bf65970d41190b879e1723fc6bbdb2d45e53565f28991a8d9d96/rignore-0.7.6-cp311-cp311-win_arm64.whl", hash = "sha256:14b58f3da4fa3d5c3fa865cab49821675371f5e979281c683e131ae29159a581", size = 657598, upload-time = "2025-11-05T21:41:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, + { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b4/e7577504d926ced2d6a3fa5ec5f27756639a1ed58a6a3fbefcf3a5659721/rignore-0.7.6-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b3746bda73f2fe6a9c3ab2f20b792e7d810b30acbdba044313fbd2d0174802e7", size = 886535, upload-time = "2025-11-05T20:42:49.317Z" }, + { url = "https://files.pythonhosted.org/packages/2b/74/098bc71a33e2997bc3291d500760123d23e3a6d354380d26c8a7ddc036de/rignore-0.7.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:67a99cf19a5137cc12f14b78dc1bb3f48500f1d5580702c623297d5297bf2752", size = 826621, upload-time = "2025-11-05T20:42:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/7b/73/5f8c276d71009a7e73fb3af6ec3bb930269efeae5830de5c796fa1fb020f/rignore-0.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9e851cfa87033c0c3fd9d35dd8b102aff2981db8bc6e0cab27b460bfe38bf3f", size = 900335, upload-time = "2025-11-05T20:40:59.178Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/dde3758084a087e6a5cd981c5277c6171d12127deed64fc4fbf12fb8ceaa/rignore-0.7.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e9b0def154665036516114437a5d603274e5451c0dc9694f622cc3b7e94603e7", size = 874274, upload-time = "2025-11-05T20:41:14.512Z" }, + { url = "https://files.pythonhosted.org/packages/58/b9/da85646824ab728036378ce62c330316108a52f30f36e6c69cac6ceda376/rignore-0.7.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b81274a47e8121224f7f637392b5dfcd9558e32a53e67ba7d04007d8b5281da9", size = 1171639, upload-time = "2025-11-05T20:41:31.206Z" }, + { url = "https://files.pythonhosted.org/packages/35/d1/8c12b779b7f0302c03c1d41511f2ab47012afecdfcd684fbec80af06b331/rignore-0.7.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d75d0b0696fb476664bea1169c8e67b13197750b91eceb4f10b3c7f379c7a204", size = 943985, upload-time = "2025-11-05T20:41:45.598Z" }, + { url = "https://files.pythonhosted.org/packages/79/bf/c233a85d31e4f94b911e92ee7e2dd2b962a5c2528f5ebd79a702596f0626/rignore-0.7.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ad3aa4dca77cef9168d0c142f72376f5bd27d1d4b8a81561bd01276d3ad9fe1", size = 961707, upload-time = "2025-11-05T20:42:16.461Z" }, + { url = "https://files.pythonhosted.org/packages/9d/eb/cadee9316a5f2a52b4aa7051967ecb94ec17938d6b425bd842d9317559eb/rignore-0.7.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00f8a59e19d219f44a93af7173de197e0d0e61c386364da20ebe98a303cbe38c", size = 986638, upload-time = "2025-11-05T20:42:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f0/2c3042c8c9639056593def5e99c3bfe850fbb9a38d061ba67b6314315bad/rignore-0.7.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6c682f3cdd741e7a30af2581f6a382ac910080977cd1f97c651467b6268352", size = 1080136, upload-time = "2025-11-05T21:40:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/7237b9eb1257b593ee51cd7ef8eed7cc32f50ccff18cb4d7cfe1e6dc54d7/rignore-0.7.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ae4e93193f75ebf6b820241594a78f347785cfd5a5fbbac94634052589418352", size = 1139413, upload-time = "2025-11-05T21:40:39.025Z" }, + { url = "https://files.pythonhosted.org/packages/a5/df/c3f382a31ad7ed68510b411c28fec42354d2c43fecb7c053d998ee9410ed/rignore-0.7.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1163d8b5d3a320d4d7cc8635213328850dc41f60e438c7869d540061adf66c98", size = 1120204, upload-time = "2025-11-05T21:40:56.062Z" }, + { url = "https://files.pythonhosted.org/packages/9c/3d/e8585c4e9c0077255ba599684aee78326176ab13ff13805ea62aa7e3235f/rignore-0.7.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3e685f47b4c58b2df7dee81ebc1ec9dbb7f798b9455c3f22be6d75ac6bddee30", size = 1129757, upload-time = "2025-11-05T21:41:14.148Z" }, + { url = "https://files.pythonhosted.org/packages/fd/56/852226c13f89ddbbf12d639900941dc55dcbcf79f5d15294796fd3279d73/rignore-0.7.6-cp39-cp39-win32.whl", hash = "sha256:2af6a0a76575220863cd838693c808a94e750640e0c8a3e9f707e93c2f131fdf", size = 648265, upload-time = "2025-11-05T21:41:58.589Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c6/14e7585dc453a870fe99b1270ee95e2adff02ea0d297cd6e2c4aa46cd43a/rignore-0.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:a326eab6db9ab85b4afb5e6eb28736a9f2b885a9246d9e8c1989bc693dd059a0", size = 728715, upload-time = "2025-11-05T21:41:42.823Z" }, + { url = "https://files.pythonhosted.org/packages/85/12/62d690b4644c330d7ac0f739b7f078190ab4308faa909a60842d0e4af5b2/rignore-0.7.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3d3a523af1cd4ed2c0cba8d277a32d329b0c96ef9901fb7ca45c8cfaccf31a5", size = 887462, upload-time = "2025-11-05T20:42:50.804Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/6528a0e97ed2bd7a7c329183367d1ffbc5b9762ae8348d88dae72cc9d1f5/rignore-0.7.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:990853566e65184a506e1e2af2d15045afad3ebaebb8859cb85b882081915110", size = 826918, upload-time = "2025-11-05T20:42:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2c/7d7bad116e09a04e9e1688c6f891fa2d4fd33f11b69ac0bd92419ddebeae/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cab9ff2e436ce7240d7ee301c8ef806ed77c1fd6b8a8239ff65f9bbbcb5b8a3", size = 900922, upload-time = "2025-11-05T20:41:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/e5ea89fbde8e37a90ce456e31c5e9d85512cef5ae38e0f4d2426eb776a19/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1a6671b2082c13bfd9a5cf4ce64670f832a6d41470556112c4ab0b6519b2fc4", size = 876987, upload-time = "2025-11-05T20:41:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fb/93d14193f0ec0c3d35b763f0a000e9780f63b2031f3d3756442c2152622d/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2468729b4c5295c199d084ab88a40afcb7c8b974276805105239c07855bbacee", size = 1171110, upload-time = "2025-11-05T20:41:32.631Z" }, + { url = "https://files.pythonhosted.org/packages/9e/46/08436312ff96ffa29cfa4e1a987efc37e094531db46ba5e9fda9bb792afd/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:775710777fd71e5fdf54df69cdc249996a1d6f447a2b5bfb86dbf033fddd9cf9", size = 943339, upload-time = "2025-11-05T20:41:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/34/28/3b3c51328f505cfaf7e53f408f78a1e955d561135d02f9cb0341ea99f69a/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4565407f4a77f72cf9d91469e75d15d375f755f0a01236bb8aaa176278cc7085", size = 961680, upload-time = "2025-11-05T20:42:18.061Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9e/cbff75c8676d4f4a90bd58a1581249d255c7305141b0868f0abc0324836b/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc44c33f8fb2d5c9da748de7a6e6653a78aa740655e7409895e94a247ffa97c8", size = 987045, upload-time = "2025-11-05T20:42:02.315Z" }, + { url = "https://files.pythonhosted.org/packages/8c/25/d802d1d369502a7ddb8816059e7c79d2d913e17df975b863418e0aca4d8a/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8f32478f05540513c11923e8838afab9efef0131d66dca7f67f0e1bbd118af6a", size = 1080310, upload-time = "2025-11-05T21:40:23.184Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/250b785c2e473b1ab763eaf2be820934c2a5409a722e94b279dddac21c7d/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:1b63a3dd76225ea35b01dd6596aa90b275b5d0f71d6dc28fce6dd295d98614aa", size = 1140998, upload-time = "2025-11-05T21:40:40.603Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d6/bb42fd2a8bba6aea327962656e20621fd495523259db40cfb4c5f760f05c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fe6c41175c36554a4ef0994cd1b4dbd6d73156fca779066456b781707402048e", size = 1121178, upload-time = "2025-11-05T21:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/97/f4/aeb548374129dce3dc191a4bb598c944d9ed663f467b9af830315d86059c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a0c6792406ae36f4e7664dc772da909451d46432ff8485774526232d4885063", size = 1130190, upload-time = "2025-11-05T21:41:16.403Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/a6250ff0c49a3cdb943910ada4116e708118e9b901c878cfae616c80a904/rignore-0.7.6-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a20b6fb61bcced9a83dfcca6599ad45182b06ba720cff7c8d891e5b78db5b65f", size = 886470, upload-time = "2025-11-05T20:42:52.314Z" }, + { url = "https://files.pythonhosted.org/packages/35/af/c69c0c51b8f9f7914d95c4ea91c29a2ac067572048cae95dd6d2efdbe05d/rignore-0.7.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25", size = 825976, upload-time = "2025-11-05T20:42:35.118Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d2/1b264f56132264ea609d3213ab603d6a27016b19559a1a1ede1a66a03dcd/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", size = 899739, upload-time = "2025-11-05T20:41:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/55/e4/b3c5dfdd8d8a10741dfe7199ef45d19a0e42d0c13aa377c83bd6caf65d90/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", size = 874843, upload-time = "2025-11-05T20:41:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/d6f3750233881a2a154cefc9a6a0a9b19da526b19f7f08221b552c6f827d/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", size = 1170348, upload-time = "2025-11-05T20:41:34.21Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/ad98ca05c9771c15af734cee18114a3c280914b6e34fde9ffea2e61e88aa/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", size = 942315, upload-time = "2025-11-05T20:41:48.508Z" }, + { url = "https://files.pythonhosted.org/packages/de/00/ab5c0f872acb60d534e687e629c17e0896c62da9b389c66d3aa16b817aa8/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", size = 961047, upload-time = "2025-11-05T20:42:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/3030fdc363a8f0d1cd155b4c453d6db9bab47a24fcc64d03f61d9d78fe6a/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbd8a48abbd3747a6c830393cd578782fab5d43f4deea48c5f5e344b8fed2b0", size = 986090, upload-time = "2025-11-05T20:42:03.581Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/133aa4002cee0ebbb39362f94e4898eec7fbd09cec9fcbce1cd65b355b7f/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", size = 1079656, upload-time = "2025-11-05T21:40:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/67/56/36d5d34210e5e7dfcd134eed8335b19e80ae940ee758f493e4f2b344dd70/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", size = 1139789, upload-time = "2025-11-05T21:40:42.119Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5b/bb4f9420802bf73678033a4a55ab1bede36ce2e9b41fec5f966d83d932b3/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:57e8327aacc27f921968cb2a174f9e47b084ce9a7dd0122c8132d22358f6bd79", size = 1120308, upload-time = "2025-11-05T21:40:59.402Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/47/98/80ef6fda78161e88ef9336fcbe851afccf78c48e69e8266a23fb7922b5aa/rignore-0.7.6-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e6ba1511c0ab8cd1ed8d6055bb0a6e629f48bfe04854293e0cd2dd88bd7153f8", size = 887180, upload-time = "2025-11-05T21:40:07.665Z" }, + { url = "https://files.pythonhosted.org/packages/21/d7/8666e7081f8476b003d8d2c8f39ecc17c93b7efd261740d15b6830acde82/rignore-0.7.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:50586d90be15f9aa8a2e2ee5a042ee6c51e28848812a35f0c95d4bfc0533d469", size = 827029, upload-time = "2025-11-05T20:42:36.628Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/3aba657d17b1737f4180b143866fedd269de15f361a8cb26ba363c0c3c13/rignore-0.7.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b129873dd0ade248e67f25a09b5b72288cbef76ba1a9aae6bac193ee1d8be72", size = 901338, upload-time = "2025-11-05T20:41:03.059Z" }, + { url = "https://files.pythonhosted.org/packages/90/cc/d8c2c9770f5f61b28999c582804f282f2227c155ba13dfb0e9ea03daeaaf/rignore-0.7.6-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9d6dd947556ddebfd62753005104986ee14a4e0663818aed19cdf2c33a6b5d5", size = 877563, upload-time = "2025-11-05T20:41:19.209Z" }, + { url = "https://files.pythonhosted.org/packages/55/63/42dd625bf96989be4a928b5444ddec9101ee63a98a15646e611b3ce58b82/rignore-0.7.6-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91b95faa532efba888b196331e9af69e693635d469185ac52c796e435e2484e5", size = 1171087, upload-time = "2025-11-05T20:41:35.558Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/4130fb622c2081c5322caf7a8888d1d265b99cd5d62cb714b512b8911233/rignore-0.7.6-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1016f430fb56f7e400838bbc56fdf43adddb6fcb7bf2a14731dfd725c2fae6c", size = 944335, upload-time = "2025-11-05T20:41:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b9/3d3ef7773da85e002fab53b1fdd9e9bb111cc86792b761cb38bd00c1532e/rignore-0.7.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f00c519861926dc703ecbb7bbeb884be67099f96f98b175671fa0a54718f55d1", size = 961500, upload-time = "2025-11-05T20:42:20.798Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bc/346c874a31a721064935c60666a19016b6b01cd716cf73d52dc64e467b30/rignore-0.7.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e34d172bf50e881b7c02e530ae8b1ea96093f0b16634c344f637227b39707b41", size = 987741, upload-time = "2025-11-05T20:42:05.071Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b8/d12dc548da8fdb63292a38727b035153495220cd93730019ee8ed3bdcffb/rignore-0.7.6-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:101d3143619898db1e7bede2e3e647daf19bb867c4fb25978016d67978d14868", size = 1081057, upload-time = "2025-11-05T21:40:26.53Z" }, + { url = "https://files.pythonhosted.org/packages/8e/51/7eea5d949212709740ad07e01c524336e44608ef0614a2a1cb31c9a0ea30/rignore-0.7.6-pp39-pypy39_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c9f3b420f54199a2b2b3b532d8c7e0860be3fa51f67501113cca6c7bfc392840", size = 1141653, upload-time = "2025-11-05T21:40:43.676Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2b/76ec843cc392fcb4e37d6a8340e823a0bf644872e191d2f5652a4c2c18ee/rignore-0.7.6-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:1c6795e3694d750ae5ef172eab7d68a52aefbd9168d2e06647df691db2b03a50", size = 1121465, upload-time = "2025-11-05T21:41:00.904Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9d/e69ad5cf03211a1076f9fe04ca2698c9cb8208b63419c928c26646bdf1d9/rignore-0.7.6-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:750a83a254b020e1193bfa7219dc7edca26bd8888a94cdc59720cbe386ab0c72", size = 1130110, upload-time = "2025-11-05T21:41:20.263Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/62/50b7727004dfe361104dfbf898c45a9a2fdfad8c72c04ae62900224d6ecf/ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153", size = 5558687, upload-time = "2025-10-31T00:26:26.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8e/0c10ff1ea5d4360ab8bfca4cb2c9d979101a391f3e79d2616c9bf348cd26/ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371", size = 12535613, upload-time = "2025-10-31T00:25:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/6724f4634c1daf52409fbf13fefda64aa9c8f81e44727a378b7b73dc590b/ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654", size = 12855812, upload-time = "2025-10-31T00:25:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/db1bce591d55fd5f8a08bb02517fa0b5097b2ccabd4ea1ee29aa72b67d96/ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14", size = 11944026, upload-time = "2025-10-31T00:25:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/0b/75/4f8dbd48e03272715d12c87dc4fcaaf21b913f0affa5f12a4e9c6f8a0582/ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed", size = 12356818, upload-time = "2025-10-31T00:25:51.949Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9b/506ec5b140c11d44a9a4f284ea7c14ebf6f8b01e6e8917734a3325bff787/ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc", size = 12336745, upload-time = "2025-10-31T00:25:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e1/c560d254048c147f35e7f8131d30bc1f63a008ac61595cf3078a3e93533d/ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd", size = 13101684, upload-time = "2025-10-31T00:25:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/a5/32/e310133f8af5cd11f8cc30f52522a3ebccc5ea5bff4b492f94faceaca7a8/ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb", size = 14535000, upload-time = "2025-10-31T00:25:58.397Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/7b0470a22158c6d8501eabc5e9b6043c99bede40fa1994cadf6b5c2a61c7/ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20", size = 14156450, upload-time = "2025-10-31T00:26:00.889Z" }, + { url = "https://files.pythonhosted.org/packages/0a/96/24bfd9d1a7f532b560dcee1a87096332e461354d3882124219bcaff65c09/ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0", size = 13568414, upload-time = "2025-10-31T00:26:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/138b883f0dfe4ad5b76b58bf4ae675f4d2176ac2b24bdd81b4d966b28c61/ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e", size = 13315293, upload-time = "2025-10-31T00:26:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/33/f4/c09bb898be97b2eb18476b7c950df8815ef14cf956074177e9fbd40b7719/ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5", size = 13539444, upload-time = "2025-10-31T00:26:08.09Z" }, + { url = "https://files.pythonhosted.org/packages/9c/aa/b30a1db25fc6128b1dd6ff0741fa4abf969ded161599d07ca7edd0739cc0/ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e", size = 12252581, upload-time = "2025-10-31T00:26:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/da/13/21096308f384d796ffe3f2960b17054110a9c3828d223ca540c2b7cc670b/ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e", size = 12307503, upload-time = "2025-10-31T00:26:12.646Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cc/a350bac23f03b7dbcde3c81b154706e80c6f16b06ff1ce28ed07dc7b07b0/ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa", size = 12675457, upload-time = "2025-10-31T00:26:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/cb/76/46346029fa2f2078826bc88ef7167e8c198e58fe3126636e52f77488cbba/ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f", size = 13403980, upload-time = "2025-10-31T00:26:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a4/35f1ef68c4e7b236d4a5204e3669efdeefaef21f0ff6a456792b3d8be438/ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7", size = 12500045, upload-time = "2025-10-31T00:26:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/51960ae340823c9859fb60c63301d977308735403e2134e17d1d2858c7fb/ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f", size = 13594005, upload-time = "2025-10-31T00:26:22.533Z" }, + { url = "https://files.pythonhosted.org/packages/b7/73/4de6579bac8e979fca0a77e54dec1f1e011a0d268165eb8a9bc0982a6564/ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1", size = 12590017, upload-time = "2025-10-31T00:26:24.52Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.49.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/94/23ac26616a883f492428d9ee9ad6eee391612125326b784dbfc30e1e7bab/sentry_sdk-2.49.0.tar.gz", hash = "sha256:c1878599cde410d481c04ef50ee3aedd4f600e4d0d253f4763041e468b332c30", size = 387228, upload-time = "2026-01-08T09:56:25.642Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/43/1c586f9f413765201234541857cb82fda076f4b0f7bad4a0ec248da39cf3/sentry_sdk-2.49.0-py2.py3-none-any.whl", hash = "sha256:6ea78499133874445a20fe9c826c9e960070abeb7ae0cdf930314ab16bb97aa0", size = 415693, upload-time = "2026-01-08T09:56:21.872Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[package]] +name = "smokeshow" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/94/c99b76517c268ef8d5c2ff88faba5a019664bd69e4754944afa294b4f24c/smokeshow-0.5.0.tar.gz", hash = "sha256:91dcabc29ac3116bff59b4d8a7bda4ae3ccc4c70742a38cec7127b8162e4a0f6", size = 101349, upload-time = "2025-01-07T19:41:51.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/10/0d23e4953eb7c1e1ad848084b3115f19234f34f907658ed11bed0d826aee/smokeshow-0.5.0-py3-none-any.whl", hash = "sha256:da12a960fc7cb525efc4035a0c3c9363b6217ea7e66bc39b9ed3cd8bed6eeedc", size = 8389, upload-time = "2025-01-07T19:41:49.194Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", version = "3.2.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.10' and platform_machine == 'AMD64') or (python_full_version < '3.10' and platform_machine == 'WIN32') or (python_full_version < '3.10' and platform_machine == 'aarch64') or (python_full_version < '3.10' and platform_machine == 'amd64') or (python_full_version < '3.10' and platform_machine == 'ppc64le') or (python_full_version < '3.10' and platform_machine == 'win32') or (python_full_version < '3.10' and platform_machine == 'x86_64')" }, + { name = "greenlet", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and platform_machine == 'AMD64') or (python_full_version >= '3.10' and platform_machine == 'WIN32') or (python_full_version >= '3.10' and platform_machine == 'aarch64') or (python_full_version >= '3.10' and platform_machine == 'amd64') or (python_full_version >= '3.10' and platform_machine == 'ppc64le') or (python_full_version >= '3.10' and platform_machine == 'win32') or (python_full_version >= '3.10' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/70/75b1387d72e2847220441166c5eb4e9846dd753895208c13e6d66523b2d9/sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85", size = 2154148, upload-time = "2025-12-10T20:03:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/7805e02323c49cb9d1ae5cd4913b28c97103079765f520043f914fca4cb3/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4", size = 3233051, upload-time = "2025-12-09T22:06:04.768Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ec/32ae09139f61bef3de3142e85c47abdee8db9a55af2bb438da54a4549263/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0", size = 3232781, upload-time = "2025-12-09T22:09:54.435Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/bf7b869b6f5585eac34222e1cf4405f4ba8c3b85dd6b1af5d4ce8bca695f/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0", size = 3182096, upload-time = "2025-12-09T22:06:06.169Z" }, + { url = "https://files.pythonhosted.org/packages/21/6a/c219720a241bb8f35c88815ccc27761f5af7fdef04b987b0e8a2c1a6dcaa/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826", size = 3205109, upload-time = "2025-12-09T22:09:55.969Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c4/6ccf31b2bc925d5d95fab403ffd50d20d7c82b858cf1a4855664ca054dce/sqlalchemy-2.0.45-cp310-cp310-win32.whl", hash = "sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a", size = 2114240, upload-time = "2025-12-09T21:29:54.007Z" }, + { url = "https://files.pythonhosted.org/packages/de/29/a27a31fca07316def418db6f7c70ab14010506616a2decef1906050a0587/sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl", hash = "sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7", size = 2137615, upload-time = "2025-12-09T21:29:55.85Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, + { url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, + { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, + { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, + { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, + { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, + { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, + { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, + { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, + { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, + { url = "https://files.pythonhosted.org/packages/53/01/a01b9829d146ba59972e6dfc88138142f5ffa4110e492c83326e7d765a17/sqlalchemy-2.0.45-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d29b2b99d527dbc66dd87c3c3248a5dd789d974a507f4653c969999fc7c1191b", size = 2157179, upload-time = "2025-12-10T20:05:13.998Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/ed43ed8ac27844f129adfc45a8735bab5dcad3e5211f4dc1bd7e676bc3ed/sqlalchemy-2.0.45-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59a8b8bd9c6bedf81ad07c8bd5543eedca55fe9b8780b2b628d495ba55f8db1e", size = 3233038, upload-time = "2025-12-09T22:06:55.42Z" }, + { url = "https://files.pythonhosted.org/packages/24/1c/721ec797f21431c905ad98cbce66430d72a340935e3b7e3232cf05e015cc/sqlalchemy-2.0.45-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd93c6f5d65f254ceabe97548c709e073d6da9883343adaa51bf1a913ce93f8e", size = 3233117, upload-time = "2025-12-09T22:10:03.143Z" }, + { url = "https://files.pythonhosted.org/packages/52/33/dcfb8dffb2ccd7c6803d63454dc1917ef5ec5b5e281fecbbc0ed1de1f125/sqlalchemy-2.0.45-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d0beadc2535157070c9c17ecf25ecec31e13c229a8f69196d7590bde8082bf1", size = 3182306, upload-time = "2025-12-09T22:06:56.894Z" }, + { url = "https://files.pythonhosted.org/packages/53/76/7cf8ce9e6dcac1d37125425aadec406d8a839dffc1b8763f6e7a56b0bf33/sqlalchemy-2.0.45-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e057f928ffe9c9b246a55b469c133b98a426297e1772ad24ce9f0c47d123bd5b", size = 3205587, upload-time = "2025-12-09T22:10:04.812Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/5cd0d14f7830981c06f468507237b0a8205691d626492b5551a67535eb30/sqlalchemy-2.0.45-cp39-cp39-win32.whl", hash = "sha256:c1c2091b1489435ff85728fafeb990f073e64f6f5e81d5cd53059773e8521eb6", size = 2115932, upload-time = "2025-12-09T22:09:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/76f6db8828c6e0cfac89820a07a40a2bab25e82e69827177b942a9bff42a/sqlalchemy-2.0.45-cp39-cp39-win_amd64.whl", hash = "sha256:56ead1f8dfb91a54a28cd1d072c74b3d635bcffbd25e50786533b822d4f2cde2", size = 2139570, upload-time = "2025-12-09T22:09:18.545Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, +] + +[[package]] +name = "sqlmodel" +version = "0.0.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/5a/693d90866233e837d182da76082a6d4c2303f54d3aaaa5c78e1238c5d863/sqlmodel-0.0.27.tar.gz", hash = "sha256:ad1227f2014a03905aef32e21428640848ac09ff793047744a73dfdd077ff620", size = 118053, upload-time = "2025-10-08T16:39:11.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/92/c35e036151fe53822893979f8a13e6f235ae8191f4164a79ae60a95d66aa/sqlmodel-0.0.27-py3-none-any.whl", hash = "sha256:667fe10aa8ff5438134668228dc7d7a08306f4c5c4c7e6ad3ad68defa0e7aa49", size = 29131, upload-time = "2025-10-08T16:39:10.917Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303, upload-time = "2025-12-31T08:02:20.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484, upload-time = "2025-12-31T08:02:18.894Z" }, +] + +[[package]] +name = "starlette" +version = "0.49.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "strawberry-graphql" +version = "0.283.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "graphql-core", marker = "python_full_version < '3.10'" }, + { name = "lia-web", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/74/729c227b1e7fce28678290a5013ddceb543f350b6c14ae83400ab2c727d1/strawberry_graphql-0.283.3.tar.gz", hash = "sha256:375e545856b7587debd4e0f1e2a6fca19d09cc126238a07b9e5164e5eb09342a", size = 212141, upload-time = "2025-10-10T20:03:46.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/1b/aa358ef730d727d2e42810bf943542a8cc4c15aa2401f8629d356643a06f/strawberry_graphql-0.283.3-py3-none-any.whl", hash = "sha256:3751d86a219d81a16a13f335bb7d2fa3f57a85fab83d7d284b8ea88e2261d68b", size = 309885, upload-time = "2025-10-10T20:03:44.051Z" }, +] + +[[package]] +name = "strawberry-graphql" +version = "0.288.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "cross-web", marker = "python_full_version >= '3.10'" }, + { name = "graphql-core", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/13/306dd2edb5f0c4aa51e2d071994a91ed1a0304e9feafa79ea0f04da51298/strawberry_graphql-0.288.2.tar.gz", hash = "sha256:853dbab407e3f5099f3a27dbf37786535894a0fbf150df5dde145fc290db607e", size = 215182, upload-time = "2026-01-01T20:01:19.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/14/15abfa6d289048eeb1b1cca4a582db08a3c1f42784b485c21ef54617e2c7/strawberry_graphql-0.288.2-py3-none-any.whl", hash = "sha256:ad72d7904582db333158568751bb6186a872380a8cc6671159d011d279382542", size = 313137, upload-time = "2026-01-01T20:01:17.32Z" }, +] + +[[package]] +name = "super-collections" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/de/a0c3d1244912c260638f0f925e190e493ccea37ecaea9bbad7c14413b803/super_collections-0.6.2.tar.gz", hash = "sha256:0c8d8abacd9fad2c7c1c715f036c29f5db213f8cac65f24d45ecba12b4da187a", size = 31315, upload-time = "2025-09-30T00:37:08.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/43/47c7cf84b3bd74a8631b02d47db356656bb8dff6f2e61a4c749963814d0d/super_collections-0.6.2-py3-none-any.whl", hash = "sha256:291b74d26299e9051d69ad9d89e61b07b6646f86a57a2f5ab3063d206eee9c56", size = 16173, upload-time = "2025-09-30T00:37:07.104Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + +[[package]] +name = "termcolor" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "webencodings", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "webencodings", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, + { url = "https://files.pythonhosted.org/packages/27/46/8d7db1dff181be50b207ab0a7483a22d5c3a4f903a9afc7cf7e465ad8109/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319f659ee992222f04e58f84cbf407cfa66a65fe3a8de44e8ad2bc53e7d99012", size = 3287784, upload-time = "2026-01-05T10:40:37.108Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6e/3bc33cae8bf114afa5a98e35eb065c72b7c37d01d370906a893f33881767/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e50f8554d504f617d9e9d6e4c2c2884a12b388a97c5c77f0bc6cf4cd032feee", size = 3164301, upload-time = "2026-01-05T10:40:42.367Z" }, + { url = "https://files.pythonhosted.org/packages/91/fc/6aa749d7d443aab4daa6f8bc00338389149fd2534e25b772285c3301993e/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a62ba2c5faa2dd175aaeed7b15abf18d20266189fb3406c5d0550dd34dd5f37", size = 3717771, upload-time = "2026-01-05T10:40:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/fc/60/5b440d251863bd33f9b0a416c695b0309487b83abf6f2dafe9163a3aeac2/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143b999bdc46d10febb15cbffb4207ddd1f410e2c755857b5a0797961bbdc113", size = 3377740, upload-time = "2026-01-05T10:40:54.859Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "trio" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "attrs", marker = "python_full_version < '3.10'" }, + { name = "cffi", marker = "python_full_version < '3.10' and implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "outcome", marker = "python_full_version < '3.10'" }, + { name = "sniffio", marker = "python_full_version < '3.10'" }, + { name = "sortedcontainers", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/8f/c6e36dd11201e2a565977d8b13f0b027ba4593c1a80bed5185489178e257/trio-0.31.0.tar.gz", hash = "sha256:f71d551ccaa79d0cb73017a33ef3264fde8335728eb4c6391451fe5d253a9d5b", size = 605825, upload-time = "2025-09-09T15:17:15.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/5b/94237a3485620dbff9741df02ff6d8acaa5fdec67d81ab3f62e4d8511bf7/trio-0.31.0-py3-none-any.whl", hash = "sha256:b5d14cd6293d79298b49c3485ffd9c07e3ce03a6da8c7dfbe0cb3dd7dc9a4774", size = 512679, upload-time = "2025-09-09T15:17:13.821Z" }, +] + +[[package]] +name = "trio" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "cffi", marker = "python_full_version >= '3.10' and implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "outcome", marker = "python_full_version >= '3.10'" }, + { name = "sniffio", marker = "python_full_version >= '3.10'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/ce/0041ddd9160aac0031bcf5ab786c7640d795c797e67c438e15cfedf815c8/trio-0.32.0.tar.gz", hash = "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b", size = 605323, upload-time = "2025-10-31T07:18:17.466Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030, upload-time = "2025-10-31T07:18:15.885Z" }, +] + +[[package]] +name = "typer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl", hash = "sha256:6e6c31047f171ac93cc5a973c9e617dbc5ab2bddc4d0a3135dc161b4e2020e0d", size = 47444, upload-time = "2026-01-06T11:21:12.441Z" }, +] + +[[package]] +name = "types-orjson" +version = "3.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/97/3f78cfdf663e5668e8b490d8c84d6de089d2d8dbad935f0dc43555d52a90/types-orjson-3.6.2.tar.gz", hash = "sha256:cf9afcc79a86325c7aff251790338109ed6f6b1bab09d2d4262dd18c85a3c638", size = 1999, upload-time = "2022-01-07T11:31:10.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/84/b34abd2d08381c5113e475908a1d79d27dc9a15f669213cee4ca03d1a891/types_orjson-3.6.2-py3-none-any.whl", hash = "sha256:22ee9a79236b6b0bfb35a0684eded62ad930a88a56797fa3c449b026cf7dbfe4", size = 2224, upload-time = "2022-01-07T11:31:09.271Z" }, +] + +[[package]] +name = "types-requests" +version = "2.31.0.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "types-urllib3", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/b8/c1e8d39996b4929b918aba10dba5de07a8b3f4c8487bb61bb79882544e69/types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0", size = 15535, upload-time = "2023-09-27T06:19:38.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/a1/6f8dc74d9069e790d604ddae70cb46dcbac668f1bb08136e7b0f2f5cd3bf/types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9", size = 14516, upload-time = "2023-09-27T06:19:36.373Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "types-ujson" +version = "5.10.0.20240515" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/49/abb4bcb9f2258f785edbf236b517c3e7ba8a503a8cbce6b5895930586cc0/types-ujson-5.10.0.20240515.tar.gz", hash = "sha256:ceae7127f0dafe4af5dd0ecf98ee13e9d75951ef963b5c5a9b7ea92e0d71f0d7", size = 3571, upload-time = "2024-05-15T02:24:43.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/1f/9d018cee3d09ab44a5211f0b5ed9b0422ad9a8c226bf3967f5884498d8f0/types_ujson-5.10.0.20240515-py3-none-any.whl", hash = "sha256:02bafc36b3a93d2511757a64ff88bd505e0a57fba08183a9150fbcfcb2015310", size = 2757, upload-time = "2024-05-15T02:24:42.315Z" }, +] + +[[package]] +name = "types-urllib3" +version = "1.26.25.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/de/b9d7a68ad39092368fb21dd6194b362b98a1daeea5dcfef5e1adb5031c7e/types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", size = 11239, upload-time = "2023-07-20T15:19:31.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/7b/3fc711b2efea5e85a7a0bbfe269ea944aa767bbba5ec52f9ee45d362ccf3/types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e", size = 15377, upload-time = "2023-07-20T15:19:30.379Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "ujson" +version = "5.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/d9/3f17e3c5773fb4941c68d9a37a47b1a79c9649d6c56aefbed87cc409d18a/ujson-5.11.0.tar.gz", hash = "sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0", size = 7156583, upload-time = "2025-08-20T11:57:02.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/8bf7a4fabfd01c7eed92d9b290930ce6d14910dec708e73538baa38885d1/ujson-5.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25", size = 55248, upload-time = "2025-08-20T11:55:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/eeab0b8b641817031ede4f790db4c4942df44a12f44d72b3954f39c6a115/ujson-5.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89", size = 53157, upload-time = "2025-08-20T11:55:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/a4e7a41870797633423ea79618526747353fd7be9191f3acfbdee0bf264b/ujson-5.11.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6", size = 57657, upload-time = "2025-08-20T11:55:05.169Z" }, + { url = "https://files.pythonhosted.org/packages/94/ae/4e0d91b8f6db7c9b76423b3649612189506d5a06ddd3b6334b6d37f77a01/ujson-5.11.0-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb", size = 59780, upload-time = "2025-08-20T11:55:06.325Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cc/46b124c2697ca2da7c65c4931ed3cb670646978157aa57a7a60f741c530f/ujson-5.11.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db", size = 57307, upload-time = "2025-08-20T11:55:07.493Z" }, + { url = "https://files.pythonhosted.org/packages/39/eb/20dd1282bc85dede2f1c62c45b4040bc4c389c80a05983515ab99771bca7/ujson-5.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c", size = 1036369, upload-time = "2025-08-20T11:55:09.192Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/80072439065d493e3a4b1fbeec991724419a1b4c232e2d1147d257cac193/ujson-5.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138", size = 1195738, upload-time = "2025-08-20T11:55:11.402Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7e/d77f9e9c039d58299c350c978e086a804d1fceae4fd4a1cc6e8d0133f838/ujson-5.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915", size = 1088718, upload-time = "2025-08-20T11:55:13.297Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f1/697559d45acc849cada6b3571d53522951b1a64027400507aabc6a710178/ujson-5.11.0-cp310-cp310-win32.whl", hash = "sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723", size = 39653, upload-time = "2025-08-20T11:55:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/70b73a0f55abe0e6b8046d365d74230c20c5691373e6902a599b2dc79ba1/ujson-5.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0", size = 43720, upload-time = "2025-08-20T11:55:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5f/b19104afa455630b43efcad3a24495b9c635d92aa8f2da4f30e375deb1a2/ujson-5.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105", size = 38410, upload-time = "2025-08-20T11:55:17.556Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/80346b826349d60ca4d612a47cdf3533694e49b45e9d1c07071bb867a184/ujson-5.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f", size = 55248, upload-time = "2025-08-20T11:55:19.033Z" }, + { url = "https://files.pythonhosted.org/packages/57/df/b53e747562c89515e18156513cc7c8ced2e5e3fd6c654acaa8752ffd7cd9/ujson-5.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58", size = 53156, upload-time = "2025-08-20T11:55:20.174Z" }, + { url = "https://files.pythonhosted.org/packages/41/b8/ab67ec8c01b8a3721fd13e5cb9d85ab2a6066a3a5e9148d661a6870d6293/ujson-5.11.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26", size = 57657, upload-time = "2025-08-20T11:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/fb84f27cd80a2c7e2d3c6012367aecade0da936790429801803fa8d4bffc/ujson-5.11.0-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a", size = 59779, upload-time = "2025-08-20T11:55:22.772Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/48706f7c1e917ecb97ddcfb7b1d756040b86ed38290e28579d63bd3fcc48/ujson-5.11.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6", size = 57284, upload-time = "2025-08-20T11:55:24.01Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ce/48877c6eb4afddfd6bd1db6be34456538c07ca2d6ed233d3f6c6efc2efe8/ujson-5.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b", size = 1036395, upload-time = "2025-08-20T11:55:25.725Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7a/2c20dc97ad70cd7c31ad0596ba8e2cf8794d77191ba4d1e0bded69865477/ujson-5.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba", size = 1195731, upload-time = "2025-08-20T11:55:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/15/f5/ca454f2f6a2c840394b6f162fff2801450803f4ff56c7af8ce37640b8a2a/ujson-5.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3", size = 1088710, upload-time = "2025-08-20T11:55:29.426Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d3/9ba310e07969bc9906eb7548731e33a0f448b122ad9705fed699c9b29345/ujson-5.11.0-cp311-cp311-win32.whl", hash = "sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34", size = 39648, upload-time = "2025-08-20T11:55:31.194Z" }, + { url = "https://files.pythonhosted.org/packages/57/f7/da05b4a8819f1360be9e71fb20182f0bb3ec611a36c3f213f4d20709e099/ujson-5.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01", size = 43717, upload-time = "2025-08-20T11:55:32.241Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cc/f3f9ac0f24f00a623a48d97dc3814df5c2dc368cfb00031aa4141527a24b/ujson-5.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835", size = 38402, upload-time = "2025-08-20T11:55:33.641Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ef/a9cb1fce38f699123ff012161599fb9f2ff3f8d482b4b18c43a2dc35073f/ujson-5.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702", size = 55434, upload-time = "2025-08-20T11:55:34.987Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/dba51a00eb30bd947791b173766cbed3492269c150a7771d2750000c965f/ujson-5.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d", size = 53190, upload-time = "2025-08-20T11:55:36.384Z" }, + { url = "https://files.pythonhosted.org/packages/03/3c/fd11a224f73fbffa299fb9644e425f38b38b30231f7923a088dd513aabb4/ujson-5.11.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80", size = 57600, upload-time = "2025-08-20T11:55:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/405103cae24899df688a3431c776e00528bd4799e7d68820e7ebcf824f92/ujson-5.11.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc", size = 59791, upload-time = "2025-08-20T11:55:38.877Z" }, + { url = "https://files.pythonhosted.org/packages/17/7b/2dcbc2bbfdbf68f2368fb21ab0f6735e872290bb604c75f6e06b81edcb3f/ujson-5.11.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c", size = 57356, upload-time = "2025-08-20T11:55:40.036Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/fea2ca18986a366c750767b694430d5ded6b20b6985fddca72f74af38a4c/ujson-5.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5", size = 1036313, upload-time = "2025-08-20T11:55:41.408Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bb/d4220bd7532eac6288d8115db51710fa2d7d271250797b0bfba9f1e755af/ujson-5.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b", size = 1195782, upload-time = "2025-08-20T11:55:43.357Z" }, + { url = "https://files.pythonhosted.org/packages/80/47/226e540aa38878ce1194454385701d82df538ccb5ff8db2cf1641dde849a/ujson-5.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc", size = 1088817, upload-time = "2025-08-20T11:55:45.262Z" }, + { url = "https://files.pythonhosted.org/packages/7e/81/546042f0b23c9040d61d46ea5ca76f0cc5e0d399180ddfb2ae976ebff5b5/ujson-5.11.0-cp312-cp312-win32.whl", hash = "sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88", size = 39757, upload-time = "2025-08-20T11:55:46.522Z" }, + { url = "https://files.pythonhosted.org/packages/44/1b/27c05dc8c9728f44875d74b5bfa948ce91f6c33349232619279f35c6e817/ujson-5.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f", size = 43859, upload-time = "2025-08-20T11:55:47.987Z" }, + { url = "https://files.pythonhosted.org/packages/22/2d/37b6557c97c3409c202c838aa9c960ca3896843b4295c4b7bb2bbd260664/ujson-5.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6", size = 38361, upload-time = "2025-08-20T11:55:49.122Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ec/2de9dd371d52c377abc05d2b725645326c4562fc87296a8907c7bcdf2db7/ujson-5.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53", size = 55435, upload-time = "2025-08-20T11:55:50.243Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a4/f611f816eac3a581d8a4372f6967c3ed41eddbae4008d1d77f223f1a4e0a/ujson-5.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752", size = 53193, upload-time = "2025-08-20T11:55:51.373Z" }, + { url = "https://files.pythonhosted.org/packages/e9/c5/c161940967184de96f5cbbbcce45b562a4bf851d60f4c677704b1770136d/ujson-5.11.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50", size = 57603, upload-time = "2025-08-20T11:55:52.583Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d6/c7b2444238f5b2e2d0e3dab300b9ddc3606e4b1f0e4bed5a48157cebc792/ujson-5.11.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a", size = 59794, upload-time = "2025-08-20T11:55:53.69Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a3/292551f936d3d02d9af148f53e1bc04306b00a7cf1fcbb86fa0d1c887242/ujson-5.11.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03", size = 57363, upload-time = "2025-08-20T11:55:54.843Z" }, + { url = "https://files.pythonhosted.org/packages/90/a6/82cfa70448831b1a9e73f882225980b5c689bf539ec6400b31656a60ea46/ujson-5.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701", size = 1036311, upload-time = "2025-08-20T11:55:56.197Z" }, + { url = "https://files.pythonhosted.org/packages/84/5c/96e2266be50f21e9b27acaee8ca8f23ea0b85cb998c33d4f53147687839b/ujson-5.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60", size = 1195783, upload-time = "2025-08-20T11:55:58.081Z" }, + { url = "https://files.pythonhosted.org/packages/8d/20/78abe3d808cf3bb3e76f71fca46cd208317bf461c905d79f0d26b9df20f1/ujson-5.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328", size = 1088822, upload-time = "2025-08-20T11:55:59.469Z" }, + { url = "https://files.pythonhosted.org/packages/d8/50/8856e24bec5e2fc7f775d867aeb7a3f137359356200ac44658f1f2c834b2/ujson-5.11.0-cp313-cp313-win32.whl", hash = "sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241", size = 39753, upload-time = "2025-08-20T11:56:01.345Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/1baee0f4179a4d0f5ce086832147b6cc9b7731c24ca08e14a3fdb8d39c32/ujson-5.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0", size = 43866, upload-time = "2025-08-20T11:56:02.552Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8c/6d85ef5be82c6d66adced3ec5ef23353ed710a11f70b0b6a836878396334/ujson-5.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9", size = 38363, upload-time = "2025-08-20T11:56:03.688Z" }, + { url = "https://files.pythonhosted.org/packages/28/08/4518146f4984d112764b1dfa6fb7bad691c44a401adadaa5e23ccd930053/ujson-5.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302", size = 55462, upload-time = "2025-08-20T11:56:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/29/37/2107b9a62168867a692654d8766b81bd2fd1e1ba13e2ec90555861e02b0c/ujson-5.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d", size = 53246, upload-time = "2025-08-20T11:56:06.054Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f8/25583c70f83788edbe3ca62ce6c1b79eff465d78dec5eb2b2b56b3e98b33/ujson-5.11.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638", size = 57631, upload-time = "2025-08-20T11:56:07.374Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ca/19b3a632933a09d696f10dc1b0dfa1d692e65ad507d12340116ce4f67967/ujson-5.11.0-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c", size = 59877, upload-time = "2025-08-20T11:56:08.534Z" }, + { url = "https://files.pythonhosted.org/packages/55/7a/4572af5324ad4b2bfdd2321e898a527050290147b4ea337a79a0e4e87ec7/ujson-5.11.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba", size = 57363, upload-time = "2025-08-20T11:56:09.758Z" }, + { url = "https://files.pythonhosted.org/packages/7b/71/a2b8c19cf4e1efe53cf439cdf7198ac60ae15471d2f1040b490c1f0f831f/ujson-5.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018", size = 1036394, upload-time = "2025-08-20T11:56:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3e/7b98668cba3bb3735929c31b999b374ebc02c19dfa98dfebaeeb5c8597ca/ujson-5.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840", size = 1195837, upload-time = "2025-08-20T11:56:12.6Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/8870f208c20b43571a5c409ebb2fe9b9dba5f494e9e60f9314ac01ea8f78/ujson-5.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c", size = 1088837, upload-time = "2025-08-20T11:56:14.15Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/c0e6607e37fa47929920a685a968c6b990a802dec65e9c5181e97845985d/ujson-5.11.0-cp314-cp314-win32.whl", hash = "sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac", size = 41022, upload-time = "2025-08-20T11:56:15.509Z" }, + { url = "https://files.pythonhosted.org/packages/4e/56/f4fe86b4c9000affd63e9219e59b222dc48b01c534533093e798bf617a7e/ujson-5.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629", size = 45111, upload-time = "2025-08-20T11:56:16.597Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f3/669437f0280308db4783b12a6d88c00730b394327d8334cc7a32ef218e64/ujson-5.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764", size = 39682, upload-time = "2025-08-20T11:56:17.763Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cd/e9809b064a89fe5c4184649adeb13c1b98652db3f8518980b04227358574/ujson-5.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433", size = 55759, upload-time = "2025-08-20T11:56:18.882Z" }, + { url = "https://files.pythonhosted.org/packages/1b/be/ae26a6321179ebbb3a2e2685b9007c71bcda41ad7a77bbbe164005e956fc/ujson-5.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3", size = 53634, upload-time = "2025-08-20T11:56:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/fb4a220ee6939db099f4cfeeae796ecb91e7584ad4d445d4ca7f994a9135/ujson-5.11.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823", size = 58547, upload-time = "2025-08-20T11:56:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f8/fc4b952b8f5fea09ea3397a0bd0ad019e474b204cabcb947cead5d4d1ffc/ujson-5.11.0-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9", size = 60489, upload-time = "2025-08-20T11:56:22.342Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e5/af5491dfda4f8b77e24cf3da68ee0d1552f99a13e5c622f4cef1380925c3/ujson-5.11.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076", size = 58035, upload-time = "2025-08-20T11:56:23.92Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/0945349dd41f25cc8c38d78ace49f14c5052c5bbb7257d2f466fa7bdb533/ujson-5.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c", size = 1037212, upload-time = "2025-08-20T11:56:25.274Z" }, + { url = "https://files.pythonhosted.org/packages/49/44/8e04496acb3d5a1cbee3a54828d9652f67a37523efa3d3b18a347339680a/ujson-5.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746", size = 1196500, upload-time = "2025-08-20T11:56:27.517Z" }, + { url = "https://files.pythonhosted.org/packages/64/ae/4bc825860d679a0f208a19af2f39206dfd804ace2403330fdc3170334a2f/ujson-5.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef", size = 1089487, upload-time = "2025-08-20T11:56:29.07Z" }, + { url = "https://files.pythonhosted.org/packages/30/ed/5a057199fb0a5deabe0957073a1c1c1c02a3e99476cd03daee98ea21fa57/ujson-5.11.0-cp314-cp314t-win32.whl", hash = "sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5", size = 41859, upload-time = "2025-08-20T11:56:30.495Z" }, + { url = "https://files.pythonhosted.org/packages/aa/03/b19c6176bdf1dc13ed84b886e99677a52764861b6cc023d5e7b6ebda249d/ujson-5.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec", size = 46183, upload-time = "2025-08-20T11:56:31.574Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ca/a0413a3874b2dc1708b8796ca895bf363292f9c70b2e8ca482b7dbc0259d/ujson-5.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab", size = 40264, upload-time = "2025-08-20T11:56:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/39/bf/c6f59cdf74ce70bd937b97c31c42fd04a5ed1a9222d0197e77e4bd899841/ujson-5.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694", size = 55283, upload-time = "2025-08-20T11:56:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c1/a52d55638c0c644b8a63059f95ad5ffcb4ad8f60d8bc3e8680f78e77cc75/ujson-5.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac", size = 53168, upload-time = "2025-08-20T11:56:35.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/6c/e64e19a01d59c8187d01ffc752ee3792a09f5edaaac2a0402de004459dd7/ujson-5.11.0-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a", size = 57809, upload-time = "2025-08-20T11:56:36.293Z" }, + { url = "https://files.pythonhosted.org/packages/9f/36/910117b7a8a1c188396f6194ca7bc8fd75e376d8f7e3cf5eb6219fc8b09d/ujson-5.11.0-cp39-cp39-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02", size = 59797, upload-time = "2025-08-20T11:56:37.746Z" }, + { url = "https://files.pythonhosted.org/packages/c7/17/bcc85d282ee2f4cdef5f577e0a43533eedcae29cc6405edf8c62a7a50368/ujson-5.11.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c", size = 57378, upload-time = "2025-08-20T11:56:39.123Z" }, + { url = "https://files.pythonhosted.org/packages/ef/39/120bb76441bf835f3c3f42db9c206f31ba875711637a52a8209949ab04b0/ujson-5.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a", size = 1036515, upload-time = "2025-08-20T11:56:40.848Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ae/fe1b4ff6388f681f6710e9494656957725b1e73ae50421ec04567df9fb75/ujson-5.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052", size = 1195753, upload-time = "2025-08-20T11:56:42.341Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/005b93f2cf846ae50b46812fcf24bbdd127521197e5f1e1a82e3b3e730a1/ujson-5.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596", size = 1088844, upload-time = "2025-08-20T11:56:43.777Z" }, + { url = "https://files.pythonhosted.org/packages/41/9e/3142023c30008e2b24d7368a389b26d28d62fcd3f596d3d898a72dd09173/ujson-5.11.0-cp39-cp39-win32.whl", hash = "sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547", size = 39652, upload-time = "2025-08-20T11:56:45.034Z" }, + { url = "https://files.pythonhosted.org/packages/ca/89/f4de0a3c485d0163f85f552886251876645fb62cbbe24fcdc0874b9fae03/ujson-5.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b", size = 43783, upload-time = "2025-08-20T11:56:46.156Z" }, + { url = "https://files.pythonhosted.org/packages/48/b1/2d50987a7b7cccb5c1fbe9ae7b184211106237b32c7039118c41d79632ea/ujson-5.11.0-cp39-cp39-win_arm64.whl", hash = "sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9", size = 38430, upload-time = "2025-08-20T11:56:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/50/17/30275aa2933430d8c0c4ead951cc4fdb922f575a349aa0b48a6f35449e97/ujson-5.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362", size = 51206, upload-time = "2025-08-20T11:56:48.797Z" }, + { url = "https://files.pythonhosted.org/packages/c3/15/42b3924258eac2551f8f33fa4e35da20a06a53857ccf3d4deb5e5d7c0b6c/ujson-5.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39", size = 48907, upload-time = "2025-08-20T11:56:50.136Z" }, + { url = "https://files.pythonhosted.org/packages/94/7e/0519ff7955aba581d1fe1fb1ca0e452471250455d182f686db5ac9e46119/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc", size = 50319, upload-time = "2025-08-20T11:56:51.63Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/209d90506b7d6c5873f82c5a226d7aad1a1da153364e9ebf61eff0740c33/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844", size = 56584, upload-time = "2025-08-20T11:56:52.89Z" }, + { url = "https://files.pythonhosted.org/packages/e9/97/bd939bb76943cb0e1d2b692d7e68629f51c711ef60425fa5bb6968037ecd/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49", size = 51588, upload-time = "2025-08-20T11:56:54.054Z" }, + { url = "https://files.pythonhosted.org/packages/52/5b/8c5e33228f7f83f05719964db59f3f9f276d272dc43752fa3bbf0df53e7b/ujson-5.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04", size = 43835, upload-time = "2025-08-20T11:56:55.237Z" }, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.39.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "h11", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/4f/f9fdac7cf6dd79790eb165639b5c452ceeabc7bbabbba4569155470a287d/uvicorn-0.39.0.tar.gz", hash = "sha256:610512b19baa93423d2892d7823741f6d27717b642c8964000d7194dded19302", size = 82001, upload-time = "2025-12-21T13:05:17.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/25/db2b1c6c35bf22e17fe5412d2ee5d3fd7a20d07ebc9dac8b58f7db2e23a0/uvicorn-0.39.0-py3-none-any.whl", hash = "sha256:7beec21bd2693562b386285b188a7963b06853c0d006302b3e4cfed950c9929a", size = 68491, upload-time = "2025-12-21T13:05:16.291Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "httptools", marker = "python_full_version < '3.10'" }, + { name = "python-dotenv", marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "uvloop", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles", marker = "python_full_version < '3.10'" }, + { name = "websockets", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "h11", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "httptools", marker = "python_full_version >= '3.10'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "uvloop", marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles", marker = "python_full_version >= '3.10'" }, + { name = "websockets", marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1b/6fbd611aeba01ef802c5876c94d7be603a9710db055beacbad39e75a31aa/uvloop-0.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4", size = 1345858, upload-time = "2025-10-16T22:17:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/9e/91/2c84f00bdbe3c51023cc83b027bac1fe959ba4a552e970da5ef0237f7945/uvloop-0.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c", size = 743913, upload-time = "2025-10-16T22:17:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/76aec83886d41a88aca5681db6a2c0601622d0d2cb66cd0d200587f962ad/uvloop-0.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54", size = 3635818, upload-time = "2025-10-16T22:17:13.812Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9a/733fcb815d345979fc54d3cdc3eb50bc75a47da3e4003ea7ada58e6daa65/uvloop-0.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659", size = 3685477, upload-time = "2025-10-16T22:17:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/83/fb/bee1eb11cc92bd91f76d97869bb6a816e80d59fd73721b0a3044dc703d9c/uvloop-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743", size = 3496128, upload-time = "2025-10-16T22:17:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/76/ee/3fdfeaa9776c0fd585d358c92b1dbca669720ffa476f0bbe64ed8f245bd7/uvloop-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7", size = 3602565, upload-time = "2025-10-16T22:17:17.755Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/a4/68/a7303a15cc797ab04d58f1fea7f67c50bd7f80090dfd7e750e7576e07582/watchfiles-1.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70", size = 409220, upload-time = "2025-10-14T15:05:51.917Z" }, + { url = "https://files.pythonhosted.org/packages/99/b8/d1857ce9ac76034c053fa7ef0e0ef92d8bd031e842ea6f5171725d31e88f/watchfiles-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e", size = 396712, upload-time = "2025-10-14T15:05:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/41/7a/da7ada566f48beaa6a30b13335b49d1f6febaf3a5ddbd1d92163a1002cf4/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956", size = 451462, upload-time = "2025-10-14T15:05:54.742Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b2/7cb9e0d5445a8d45c4cccd68a590d9e3a453289366b96ff37d1075aaebef/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c", size = 460811, upload-time = "2025-10-14T15:05:55.743Z" }, + { url = "https://files.pythonhosted.org/packages/04/9d/b07d4491dde6db6ea6c680fdec452f4be363d65c82004faf2d853f59b76f/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c", size = 490576, upload-time = "2025-10-14T15:05:56.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/03/e64dcab0a1806157db272a61b7891b062f441a30580a581ae72114259472/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3", size = 597726, upload-time = "2025-10-14T15:05:57.986Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8e/a827cf4a8d5f2903a19a934dcf512082eb07675253e154d4cd9367978a58/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2", size = 474900, upload-time = "2025-10-14T15:05:59.378Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/94fed0b346b85b22303a12eee5f431006fae6af70d841cac2f4403245533/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02", size = 457521, upload-time = "2025-10-14T15:06:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/c4/64/bc3331150e8f3c778d48a4615d4b72b3d2d87868635e6c54bbd924946189/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be", size = 632191, upload-time = "2025-10-14T15:06:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/e4/84/f39e19549c2f3ec97225dcb2ceb9a7bb3c5004ed227aad1f321bf0ff2051/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f", size = 623923, upload-time = "2025-10-14T15:06:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/0759ae15d9a0c9c5fe946bd4cf45ab9e7bad7cfede2c06dc10f59171b29f/watchfiles-1.1.1-cp39-cp39-win32.whl", hash = "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b", size = 274010, upload-time = "2025-10-14T15:06:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3b/eb26cddd4dfa081e2bf6918be3b2fc05ee3b55c1d21331d5562ee0c6aaad/watchfiles-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957", size = 289090, upload-time = "2025-10-14T15:06:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/db/38a2c52fdbbfe2fc7ffaaaaaebc927d52b9f4d5139bba3186c19a7463001/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f", size = 409210, upload-time = "2025-10-14T15:06:14.492Z" }, + { url = "https://files.pythonhosted.org/packages/d1/43/d7e8b71f6c21ff813ee8da1006f89b6c7fff047fb4c8b16ceb5e840599c5/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34", size = 397286, upload-time = "2025-10-14T15:06:16.177Z" }, + { url = "https://files.pythonhosted.org/packages/1f/5d/884074a5269317e75bd0b915644b702b89de73e61a8a7446e2b225f45b1f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc", size = 451768, upload-time = "2025-10-14T15:06:18.266Z" }, + { url = "https://files.pythonhosted.org/packages/17/71/7ffcaa9b5e8961a25026058058c62ec8f604d2a6e8e1e94bee8a09e1593f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e", size = 458561, upload-time = "2025-10-14T15:06:19.323Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/36/db/3fff0bcbe339a6fa6a3b9e3fbc2bfb321ec2f4cd233692272c5a8d6cf801/websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5", size = 175424, upload-time = "2025-03-05T20:02:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/46/e6/519054c2f477def4165b0ec060ad664ed174e140b0d1cbb9fafa4a54f6db/websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a", size = 173077, upload-time = "2025-03-05T20:02:58.37Z" }, + { url = "https://files.pythonhosted.org/packages/1a/21/c0712e382df64c93a0d16449ecbf87b647163485ca1cc3f6cbadb36d2b03/websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b", size = 173324, upload-time = "2025-03-05T20:02:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cb/51ba82e59b3a664df54beed8ad95517c1b4dc1a913730e7a7db778f21291/websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770", size = 182094, upload-time = "2025-03-05T20:03:01.827Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0f/bf3788c03fec679bcdaef787518dbe60d12fe5615a544a6d4cf82f045193/websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb", size = 181094, upload-time = "2025-03-05T20:03:03.123Z" }, + { url = "https://files.pythonhosted.org/packages/5e/da/9fb8c21edbc719b66763a571afbaf206cb6d3736d28255a46fc2fe20f902/websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054", size = 181397, upload-time = "2025-03-05T20:03:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/2e/65/65f379525a2719e91d9d90c38fe8b8bc62bd3c702ac651b7278609b696c4/websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee", size = 181794, upload-time = "2025-03-05T20:03:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/31ac2d08f8e9304d81a1a7ed2851c0300f636019a57cbaa91342015c72cc/websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed", size = 181194, upload-time = "2025-03-05T20:03:08.844Z" }, + { url = "https://files.pythonhosted.org/packages/98/72/1090de20d6c91994cd4b357c3f75a4f25ee231b63e03adea89671cc12a3f/websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880", size = 181164, upload-time = "2025-03-05T20:03:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/2d/37/098f2e1c103ae8ed79b0e77f08d83b0ec0b241cf4b7f2f10edd0126472e1/websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411", size = 176381, upload-time = "2025-03-05T20:03:12.77Z" }, + { url = "https://files.pythonhosted.org/packages/75/8b/a32978a3ab42cebb2ebdd5b05df0696a09f4d436ce69def11893afa301f0/websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4", size = 176841, upload-time = "2025-03-05T20:03:14.367Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/48/4b67623bac4d79beb3a6bb27b803ba75c1bdedc06bd827e465803690a4b2/websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940", size = 173106, upload-time = "2025-03-05T20:03:29.404Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f0/adb07514a49fe5728192764e04295be78859e4a537ab8fcc518a3dbb3281/websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e", size = 173339, upload-time = "2025-03-05T20:03:30.755Z" }, + { url = "https://files.pythonhosted.org/packages/87/28/bd23c6344b18fb43df40d0700f6d3fffcd7cef14a6995b4f976978b52e62/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9", size = 174597, upload-time = "2025-03-05T20:03:32.247Z" }, + { url = "https://files.pythonhosted.org/packages/6d/79/ca288495863d0f23a60f546f0905ae8f3ed467ad87f8b6aceb65f4c013e4/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b", size = 174205, upload-time = "2025-03-05T20:03:33.731Z" }, + { url = "https://files.pythonhosted.org/packages/04/e4/120ff3180b0872b1fe6637f6f995bcb009fb5c87d597c1fc21456f50c848/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f", size = 174150, upload-time = "2025-03-05T20:03:35.757Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/30e2f9c539b8da8b1d76f64012f3b19253271a63413b2d3adb94b143407f/websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123", size = 176877, upload-time = "2025-03-05T20:03:37.199Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From 961b2e844a23f3ffedc318a0c7c82dc9f5cb6fcd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:03:19 +0000 Subject: [PATCH 050/243] =?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 c1d24561c..f7d8cb58f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,6 +19,7 @@ hide: ### Internal +* ⬆️ Migrate to uv. PR [#14676](https://github.com/fastapi/fastapi/pull/14676) by [@DoctorJohn](https://github.com/DoctorJohn). * 🔨 Add LLM translations tool fixer. PR [#14652](https://github.com/fastapi/fastapi/pull/14652) by [@YuriiMotov](https://github.com/YuriiMotov). * 👥 Update FastAPI People - Sponsors. PR [#14626](https://github.com/fastapi/fastapi/pull/14626) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI GitHub topic repositories. PR [#14630](https://github.com/fastapi/fastapi/pull/14630) by [@tiangolo](https://github.com/tiangolo). From c75ae058e448063c2ace68c5c8ad0af99bee3e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 10 Jan 2026 14:17:46 -0800 Subject: [PATCH 051/243] =?UTF-8?q?=F0=9F=94=A7=20Add=20pre-commit=20local?= =?UTF-8?q?=20script=20to=20fix=20language=20translations=20(#14683)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5da3cccf1..d88b70b7b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -58,3 +58,9 @@ repos: entry: uv run ./scripts/docs.py ensure-non-translated files: ^docs/(?!en/).*|^scripts/docs\.py$ pass_filenames: false + + - id: fix-translations + language: unsupported + name: fix translations + entry: uv run ./scripts/translation_fixer.py fix-pages + files: ^docs/(?!en/).*/docs/.*\.md$ From 21d2c5cea089c9477a1d5dad26702068fa043e49 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:18:13 +0000 Subject: [PATCH 052/243] =?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 f7d8cb58f..818a55362 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,6 +19,7 @@ hide: ### Internal +* 🔧 Add pre-commit local script to fix language translations. PR [#14683](https://github.com/fastapi/fastapi/pull/14683) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Migrate to uv. PR [#14676](https://github.com/fastapi/fastapi/pull/14676) by [@DoctorJohn](https://github.com/DoctorJohn). * 🔨 Add LLM translations tool fixer. PR [#14652](https://github.com/fastapi/fastapi/pull/14652) by [@YuriiMotov](https://github.com/YuriiMotov). * 👥 Update FastAPI People - Sponsors. PR [#14626](https://github.com/fastapi/fastapi/pull/14626) by [@tiangolo](https://github.com/tiangolo). From 7eac6e31695bd02d3607aa4416df50a02bc5445a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 10 Jan 2026 14:43:44 -0800 Subject: [PATCH 053/243] =?UTF-8?q?=E2=9C=85=20Enable=20tests=20in=20CI=20?= =?UTF-8?q?for=20scripts=20(#14684)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/doc_parsing_utils.py | 20 +++++++++++-------- scripts/docs.py | 2 +- scripts/notify_translations.py | 2 +- scripts/test.sh | 2 +- .../tests/test_translation_fixer/conftest.py | 10 ++++++++++ .../test_code_blocks_lines_number_mismatch.py | 16 +++++++-------- .../test_code_blocks_mermaid.py | 8 ++++---- .../test_code_blocks_number_mismatch.py | 12 +++++++---- .../test_code_blocks_wrong_lang_code.py | 12 +++++------ .../test_number_mismatch.py | 12 +++++++---- .../test_complex_doc/test_compex_doc.py | 4 ++-- .../test_header_level_mismatch.py | 8 ++++---- .../test_header_number_mismatch.py | 12 +++++++---- .../test_html_links_number_mismatch.py | 12 +++++++---- .../test_mkd_links_number_mismatch.py | 12 +++++++---- 15 files changed, 89 insertions(+), 55 deletions(-) diff --git a/scripts/doc_parsing_utils.py b/scripts/doc_parsing_utils.py index 857d808aa..79f2e9ec0 100644 --- a/scripts/doc_parsing_utils.py +++ b/scripts/doc_parsing_utils.py @@ -1,5 +1,5 @@ import re -from typing import TypedDict +from typing import TypedDict, Union CODE_INCLUDE_RE = re.compile(r"^\{\*\s*(\S+)\s*(.*)\*\}$") CODE_INCLUDE_PLACEHOLDER = "<CODE_INCLUDE>" @@ -50,8 +50,8 @@ class MarkdownLinkInfo(TypedDict): line_no: int url: str text: str - title: str | None - attributes: str | None + title: Union[str, None] + attributes: Union[str, None] full_match: str @@ -285,7 +285,11 @@ def _add_lang_code_to_url(url: str, lang_code: str) -> str: def _construct_markdown_link( - url: str, text: str, title: str | None, attributes: str | None, lang_code: str + url: str, + text: str, + title: Union[str, None], + attributes: Union[str, None], + lang_code: str, ) -> str: """ Construct a markdown link, adjusting the URL for the given language code if needed. @@ -545,7 +549,7 @@ def extract_multiline_code_blocks(text: list[str]) -> list[MultilineCodeBlockInf return blocks -def _split_hash_comment(line: str) -> tuple[str, str | None]: +def _split_hash_comment(line: str) -> tuple[str, Union[str, None]]: match = HASH_COMMENT_RE.match(line) if match: code = match.group("code").rstrip() @@ -554,7 +558,7 @@ def _split_hash_comment(line: str) -> tuple[str, str | None]: return line.rstrip(), None -def _split_slashes_comment(line: str) -> tuple[str, str | None]: +def _split_slashes_comment(line: str) -> tuple[str, Union[str, None]]: match = SLASHES_COMMENT_RE.match(line) if match: code = match.group("code").rstrip() @@ -600,8 +604,8 @@ def replace_multiline_code_block( code_block: list[str] = [] for line_a, line_b in zip(block_a["content"], block_b["content"]): - line_a_comment: str | None = None - line_b_comment: str | None = None + line_a_comment: Union[str, None] = None + line_b_comment: Union[str, None] = None # Handle comments based on language if block_language in { diff --git a/scripts/docs.py b/scripts/docs.py index fbde1eca4..84cf01c72 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -239,7 +239,7 @@ def generate_readme() -> None: Generate README.md content from main index.md """ readme_path = Path("README.md") - old_content = readme_path.read_text() + old_content = readme_path.read_text("utf-8") new_content = generate_readme_content() if new_content != old_content: print("README.md outdated from the latest index.md") diff --git a/scripts/notify_translations.py b/scripts/notify_translations.py index 2ca740a60..74cdf0dff 100644 --- a/scripts/notify_translations.py +++ b/scripts/notify_translations.py @@ -316,7 +316,7 @@ def main() -> None: raise RuntimeError( f"No github event file available at: {settings.github_event_path}" ) - contents = settings.github_event_path.read_text() + contents = settings.github_event_path.read_text("utf-8") github_event = PartialGitHubEvent.model_validate_json(contents) logging.info(f"Using GitHub event: {github_event}") number = ( diff --git a/scripts/test.sh b/scripts/test.sh index 7d17add8f..0bffcd1cd 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -4,4 +4,4 @@ set -e set -x export PYTHONPATH=./docs_src -coverage run -m pytest tests ${@} +coverage run -m pytest tests scripts/tests/ ${@} diff --git a/scripts/tests/test_translation_fixer/conftest.py b/scripts/tests/test_translation_fixer/conftest.py index b2c745de1..006f519f4 100644 --- a/scripts/tests/test_translation_fixer/conftest.py +++ b/scripts/tests/test_translation_fixer/conftest.py @@ -1,9 +1,19 @@ import shutil +import sys from pathlib import Path import pytest from typer.testing import CliRunner +skip_on_windows = pytest.mark.skipif( + sys.platform == "win32", reason="Skipping on Windows" +) + + +def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: + for item in items: + item.add_marker(skip_on_windows) + @pytest.fixture(name="runner") def get_runner(): diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_lines_number_mismatch.py b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_lines_number_mismatch.py index 906c8a560..9cdbe8323 100644 --- a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_lines_number_mismatch.py +++ b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_lines_number_mismatch.py @@ -22,10 +22,10 @@ def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 1, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path( - f"{data_path}/translated_doc_lines_number_gt.md" - ).read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_lines_number_gt.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output @@ -46,10 +46,10 @@ def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): ) # assert result.exit_code == 1, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path( - f"{data_path}/translated_doc_lines_number_lt.md" - ).read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_lines_number_lt.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_mermaid.py b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_mermaid.py index 75c589fb5..8b80c70f3 100644 --- a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_mermaid.py +++ b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_mermaid.py @@ -22,10 +22,10 @@ def test_translated(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 0, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") expected_content = Path( f"{data_path}/translated_doc_mermaid_translated.md" - ).read_text() + ).read_text("utf-8") assert fixed_content == expected_content # Translated doc remains unchanged assert ( @@ -50,10 +50,10 @@ def test_not_translated(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 0, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") expected_content = Path( f"{data_path}/translated_doc_mermaid_not_translated.md" - ).read_text() + ).read_text("utf-8") assert fixed_content == expected_content # Translated doc remains unchanged assert ("Skipping mermaid code block replacement") not in result.output diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_number_mismatch.py b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_number_mismatch.py index b05dac900..ad5767c1c 100644 --- a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_number_mismatch.py +++ b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_number_mismatch.py @@ -22,8 +22,10 @@ def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 1, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output @@ -45,8 +47,10 @@ def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): ) # assert result.exit_code == 1, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output diff --git a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_wrong_lang_code.py b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_wrong_lang_code.py index 6c2b18c89..85d73e3b4 100644 --- a/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_wrong_lang_code.py +++ b/scripts/tests/test_translation_fixer/test_code_blocks/test_code_blocks_wrong_lang_code.py @@ -22,10 +22,10 @@ def test_wrong_lang_code_1(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 1, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path( - f"{data_path}/translated_doc_wrong_lang_code.md" - ).read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_wrong_lang_code.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output @@ -46,10 +46,10 @@ def test_wrong_lang_code_2(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 1, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") expected_content = Path( f"{data_path}/translated_doc_wrong_lang_code_2.md" - ).read_text() + ).read_text("utf-8") assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output diff --git a/scripts/tests/test_translation_fixer/test_code_includes/test_number_mismatch.py b/scripts/tests/test_translation_fixer/test_code_includes/test_number_mismatch.py index 5e3eee51a..1020b890c 100644 --- a/scripts/tests/test_translation_fixer/test_code_includes/test_number_mismatch.py +++ b/scripts/tests/test_translation_fixer/test_code_includes/test_number_mismatch.py @@ -22,8 +22,10 @@ def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 1 - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output @@ -45,8 +47,10 @@ def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 1 - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output diff --git a/scripts/tests/test_translation_fixer/test_complex_doc/test_compex_doc.py b/scripts/tests/test_translation_fixer/test_complex_doc/test_compex_doc.py index 86cc4d5eb..cc7bcadda 100644 --- a/scripts/tests/test_translation_fixer/test_complex_doc/test_compex_doc.py +++ b/scripts/tests/test_translation_fixer/test_complex_doc/test_compex_doc.py @@ -22,8 +22,8 @@ def test_fix(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 0, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = (data_path / "translated_doc_expected.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = (data_path / "translated_doc_expected.md").read_text("utf-8") assert fixed_content == expected_content assert "Fixing multiline code blocks in" in result.output diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_level_mismatch.py b/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_level_mismatch.py index 9fe2f7ba7..99d27db95 100644 --- a/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_level_mismatch.py +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_level_mismatch.py @@ -22,10 +22,10 @@ def test_level_mismatch_1(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 1 - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") expected_content = Path( f"{data_path}/translated_doc_level_mismatch_1.md" - ).read_text() + ).read_text("utf-8") assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output @@ -47,10 +47,10 @@ def test_level_mismatch_2(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 1 - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") expected_content = Path( f"{data_path}/translated_doc_level_mismatch_2.md" - ).read_text() + ).read_text("utf-8") assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output diff --git a/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_number_mismatch.py b/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_number_mismatch.py index c0e78d030..c4d034617 100644 --- a/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_number_mismatch.py +++ b/scripts/tests/test_translation_fixer/test_header_permalinks/test_header_number_mismatch.py @@ -22,8 +22,10 @@ def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 1 - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output @@ -45,8 +47,10 @@ def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 1 - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output diff --git a/scripts/tests/test_translation_fixer/test_html_links/test_html_links_number_mismatch.py b/scripts/tests/test_translation_fixer/test_html_links/test_html_links_number_mismatch.py index 271e5d205..11dcea154 100644 --- a/scripts/tests/test_translation_fixer/test_html_links/test_html_links_number_mismatch.py +++ b/scripts/tests/test_translation_fixer/test_html_links/test_html_links_number_mismatch.py @@ -20,8 +20,10 @@ def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 1, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output @@ -43,8 +45,10 @@ def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): ) # assert result.exit_code == 1, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output diff --git a/scripts/tests/test_translation_fixer/test_markdown_links/test_mkd_links_number_mismatch.py b/scripts/tests/test_translation_fixer/test_markdown_links/test_mkd_links_number_mismatch.py index 0f4952f35..c72e01254 100644 --- a/scripts/tests/test_translation_fixer/test_markdown_links/test_mkd_links_number_mismatch.py +++ b/scripts/tests/test_translation_fixer/test_markdown_links/test_mkd_links_number_mismatch.py @@ -22,8 +22,10 @@ def test_gt(runner: CliRunner, root_dir: Path, copy_test_files): ) assert result.exit_code == 1, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_gt.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output @@ -45,8 +47,10 @@ def test_lt(runner: CliRunner, root_dir: Path, copy_test_files): ) # assert result.exit_code == 1, result.output - fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text() - expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text() + fixed_content = (root_dir / "docs" / "lang" / "docs" / "doc.md").read_text("utf-8") + expected_content = Path(f"{data_path}/translated_doc_number_lt.md").read_text( + "utf-8" + ) assert fixed_content == expected_content # Translated doc remains unchanged assert "Error processing docs/lang/docs/doc.md" in result.output From 3d1f9268fc53fe4ca4dc55b9e2c1fe98e0650dc4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:44:05 +0000 Subject: [PATCH 054/243] =?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 818a55362..5090a9ea2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,6 +19,7 @@ hide: ### Internal +* ✅ Enable tests in CI for scripts. PR [#14684](https://github.com/fastapi/fastapi/pull/14684) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add pre-commit local script to fix language translations. PR [#14683](https://github.com/fastapi/fastapi/pull/14683) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Migrate to uv. PR [#14676](https://github.com/fastapi/fastapi/pull/14676) by [@DoctorJohn](https://github.com/DoctorJohn). * 🔨 Add LLM translations tool fixer. PR [#14652](https://github.com/fastapi/fastapi/pull/14652) by [@YuriiMotov](https://github.com/YuriiMotov). From cefd50702a5269351fadbf8110628356bd505449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 10 Jan 2026 15:06:37 -0800 Subject: [PATCH 055/243] =?UTF-8?q?=F0=9F=90=9B=20Fix=20translation=20scri?= =?UTF-8?q?pt=20path=20(#14685)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/translate.py b/scripts/translate.py index ffecfde07..c7e6c7ea1 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -25,7 +25,7 @@ non_translated_sections = ( "contributing.md", ) -general_prompt_path = Path(__file__).absolute().parent / "llm-general-prompt.md" +general_prompt_path = Path(__file__).absolute().parent / "general-llm-prompt.md" general_prompt = general_prompt_path.read_text(encoding="utf-8") app = typer.Typer() From 8183e748eef56b0c00c212b81fdd9034b186cd8e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:06:59 +0000 Subject: [PATCH 056/243] =?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 5090a9ea2..08bc2927c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,6 +19,7 @@ hide: ### Internal +* 🐛 Fix translation script path. PR [#14685](https://github.com/fastapi/fastapi/pull/14685) by [@tiangolo](https://github.com/tiangolo). * ✅ Enable tests in CI for scripts. PR [#14684](https://github.com/fastapi/fastapi/pull/14684) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add pre-commit local script to fix language translations. PR [#14683](https://github.com/fastapi/fastapi/pull/14683) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Migrate to uv. PR [#14676](https://github.com/fastapi/fastapi/pull/14676) by [@DoctorJohn](https://github.com/DoctorJohn). From a2912ffa26ea49db08d9a02a4888ebe60b44e702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 10 Jan 2026 15:41:20 -0800 Subject: [PATCH 057/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20es=20(update-outdated)=20(#14686)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- docs/es/docs/_llm-test.md | 46 +++++++-------- .../path-operation-advanced-configuration.md | 40 ++----------- docs/es/docs/advanced/settings.md | 56 ++----------------- ...migrate-from-pydantic-v1-to-pydantic-v2.md | 26 +++++---- .../docs/how-to/separate-openapi-schemas.md | 6 +- docs/es/docs/index.md | 26 +++++---- docs/es/docs/tutorial/bigger-applications.md | 28 +++++----- docs/es/docs/tutorial/body-updates.md | 22 +------- docs/es/docs/tutorial/body.md | 14 ++--- docs/es/docs/tutorial/extra-models.md | 39 +++++-------- .../tutorial/query-params-str-validations.md | 26 ++------- docs/es/docs/tutorial/response-model.md | 20 +------ docs/es/docs/tutorial/schema-extra-example.md | 38 +++---------- 13 files changed, 117 insertions(+), 270 deletions(-) diff --git a/docs/es/docs/_llm-test.md b/docs/es/docs/_llm-test.md index 2ab1d9314..591b1008b 100644 --- a/docs/es/docs/_llm-test.md +++ b/docs/es/docs/_llm-test.md @@ -1,17 +1,17 @@ # Archivo de prueba de LLM { #llm-test-file } -Este documento prueba si el <abbr title="Large Language Model – Modelo de lenguaje grande">LLM</abbr>, que traduce la documentación, entiende el `general_prompt` en `scripts/translate.py` y el prompt específico del idioma en `docs/{language code}/llm-prompt.md`. El prompt específico del idioma se agrega al final de `general_prompt`. +Este documento prueba si el <abbr title="Large Language Model - Modelo de lenguaje grande">LLM</abbr>, que traduce la documentación, entiende el `general_prompt` en `scripts/translate.py` y el prompt específico del idioma en `docs/{language code}/llm-prompt.md`. El prompt específico del idioma se agrega al final de `general_prompt`. Las pruebas añadidas aquí serán vistas por todas las personas que diseñan prompts específicos del idioma. Úsalo de la siguiente manera: -* Ten un prompt específico del idioma – `docs/{language code}/llm-prompt.md`. +* Ten un prompt específico del idioma - `docs/{language code}/llm-prompt.md`. * Haz una traducción fresca de este documento a tu idioma destino (mira, por ejemplo, el comando `translate-page` de `translate.py`). Esto creará la traducción en `docs/{language code}/docs/_llm-test.md`. -* Comprueba si todo está bien en la traducción. +* Revisa si las cosas están bien en la traducción. * Si es necesario, mejora tu prompt específico del idioma, el prompt general, o el documento en inglés. * Luego corrige manualmente los problemas restantes en la traducción para que sea una buena traducción. -* Vuelve a traducir, teniendo la buena traducción en su lugar. El resultado ideal sería que el LLM ya no hiciera cambios a la traducción. Eso significa que el prompt general y tu prompt específico del idioma están tan bien como pueden estar (a veces hará algunos cambios aparentemente aleatorios; la razón es que <a href="https://doublespeak.chat/#/handbook#deterministic-output" class="external-link" target="_blank">los LLMs no son algoritmos deterministas</a>). +* Vuelve a traducir, teniendo la buena traducción en su lugar. El resultado ideal sería que el LLM ya no hiciera cambios a la traducción. Eso significa que el prompt general y tu prompt específico del idioma están tan bien como pueden estar (A veces hará algunos cambios aparentemente aleatorios; la razón es que <a href="https://doublespeak.chat/#/handbook#deterministic-output" class="external-link" target="_blank">los LLMs no son algoritmos deterministas</a>). Las pruebas: @@ -23,7 +23,7 @@ Este es un fragmento de código: `foo`. Y este es otro fragmento de código: `ba //// -//// tab | Información +//// tab | Info El contenido de los fragmentos de código debe dejarse tal cual. @@ -45,7 +45,7 @@ El LLM probablemente traducirá esto mal. Lo interesante es si mantiene la tradu //// -//// tab | Información +//// tab | Info La persona que diseña el prompt puede elegir si quiere convertir comillas neutras a comillas tipográficas. También está bien dejarlas como están. @@ -67,7 +67,7 @@ Hardcore: `Yesterday, my friend wrote: "If you spell incorrectly correctly, you //// -//// tab | Información +//// tab | Info ... Sin embargo, las comillas dentro de fragmentos de código deben quedarse tal cual. @@ -112,7 +112,7 @@ works(foo="bar") # Esto funciona 🎉 //// -//// tab | Información +//// tab | Info El código en bloques de código no debe modificarse, con la excepción de los comentarios. @@ -154,7 +154,7 @@ Algo de texto //// -//// tab | Información +//// tab | Info Las pestañas y los bloques `Info`/`Note`/`Warning`/etc. deben tener la traducción de su título añadida después de una barra vertical (`|`). @@ -181,7 +181,7 @@ El texto del enlace debe traducirse, la dirección del enlace debe apuntar a la //// -//// tab | Información +//// tab | Info Los enlaces deben traducirse, pero su dirección debe permanecer sin cambios. Una excepción son los enlaces absolutos a páginas de la documentación de FastAPI. En ese caso deben enlazar a la traducción. @@ -197,10 +197,10 @@ Aquí algunas cosas envueltas en elementos HTML "abbr" (algunas son inventadas): ### El abbr da una frase completa { #the-abbr-gives-a-full-phrase } -* <abbr title="Getting Things Done – Hacer las cosas">GTD</abbr> -* <abbr title="less than – menor que"><code>lt</code></abbr> -* <abbr title="XML Web Token – Token web XML">XWT</abbr> -* <abbr title="Parallel Server Gateway Interface – Interfaz de pasarela de servidor paralela">PSGI</abbr> +* <abbr title="Getting Things Done - Hacer las cosas">GTD</abbr> +* <abbr title="less than - menor que"><code>lt</code></abbr> +* <abbr title="XML Web Token - Token web XML">XWT</abbr> +* <abbr title="Parallel Server Gateway Interface - Interfaz de pasarela de servidor paralela">PSGI</abbr> ### El abbr da una explicación { #the-abbr-gives-an-explanation } @@ -209,12 +209,12 @@ Aquí algunas cosas envueltas en elementos HTML "abbr" (algunas son inventadas): ### El abbr da una frase completa y una explicación { #the-abbr-gives-a-full-phrase-and-an-explanation } -* <abbr title="Mozilla Developer Network – Red de Desarrolladores de Mozilla: documentación para desarrolladores, escrita por la gente de Firefox">MDN</abbr> -* <abbr title="Input/Output – Entrada/Salida: lectura o escritura de disco, comunicaciones de red.">I/O</abbr>. +* <abbr title="Mozilla Developer Network - Red de Desarrolladores de Mozilla: documentación para desarrolladores, escrita por la gente de Firefox">MDN</abbr> +* <abbr title="Input/Output: lectura o escritura de disco, comunicaciones de red.">I/O</abbr>. //// -//// tab | Información +//// tab | Info Los atributos "title" de los elementos "abbr" se traducen siguiendo instrucciones específicas. @@ -242,7 +242,7 @@ Hola de nuevo. //// -//// tab | Información +//// tab | Info La única regla estricta para los encabezados es que el LLM deje la parte del hash dentro de llaves sin cambios, lo que asegura que los enlaces no se rompan. @@ -355,7 +355,7 @@ Para instrucciones específicas del idioma, mira p. ej. la sección `### Heading * los headers * el header de autorización * el header `Authorization` -* el header Forwarded +* el header forwarded * el sistema de inyección de dependencias * la dependencia @@ -368,7 +368,7 @@ Para instrucciones específicas del idioma, mira p. ej. la sección `### Heading * paralelismo * multiprocesamiento -* la variable de entorno +* la env var * la variable de entorno * el `PATH` * la variable `PATH` @@ -433,7 +433,7 @@ Para instrucciones específicas del idioma, mira p. ej. la sección `### Heading * el motor de plantillas * la anotación de tipos -* la anotación de tipos +* las anotaciones de tipos * el worker del servidor * el worker de Uvicorn @@ -468,7 +468,7 @@ Para instrucciones específicas del idioma, mira p. ej. la sección `### Heading * el ítem * el paquete * el lifespan -* el bloqueo +* el lock * el middleware * la aplicación móvil * el módulo @@ -494,7 +494,7 @@ Para instrucciones específicas del idioma, mira p. ej. la sección `### Heading //// -//// tab | Información +//// tab | Info Esta es una lista no completa y no normativa de términos (mayormente) técnicos vistos en la documentación. Puede ayudar a la persona que diseña el prompt a identificar para qué términos el LLM necesita una mano. Por ejemplo cuando sigue revirtiendo una buena traducción a una traducción subóptima. O cuando tiene problemas conjugando/declinando un término en tu idioma. diff --git a/docs/es/docs/advanced/path-operation-advanced-configuration.md b/docs/es/docs/advanced/path-operation-advanced-configuration.md index 396159868..ea58a300a 100644 --- a/docs/es/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/es/docs/advanced/path-operation-advanced-configuration.md @@ -10,7 +10,7 @@ Si no eres un "experto" en OpenAPI, probablemente no necesites esto. Puedes establecer el `operationId` de OpenAPI para ser usado en tu *path operation* con el parámetro `operation_id`. -Tienes que asegurarte de que sea único para cada operación. +Tendrías que asegurarte de que sea único para cada operación. {* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} @@ -46,7 +46,7 @@ Para excluir una *path operation* del esquema OpenAPI generado (y por lo tanto, Puedes limitar las líneas usadas del docstring de una *path operation function* para OpenAPI. -Añadir un `\f` (un carácter de separación de página escapado) hace que **FastAPI** trunque la salida usada para OpenAPI en este punto. +Añadir un `\f` (un carácter "form feed" escapado) hace que **FastAPI** trunque la salida usada para OpenAPI en este punto. No aparecerá en la documentación, pero otras herramientas (como Sphinx) podrán usar el resto. @@ -141,9 +141,9 @@ Podrías hacer eso con `openapi_extra`: {* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} -En este ejemplo, no declaramos ningún modelo Pydantic. De hecho, el cuerpo del request ni siquiera se <abbr title="convertido de algún formato plano, como bytes, a objetos de Python">parse</abbr> como JSON, se lee directamente como `bytes`, y la función `magic_data_reader()` sería la encargada de parsearlo de alguna manera. +En este ejemplo, no declaramos ningún modelo Pydantic. De hecho, el request body ni siquiera se <abbr title="converted from some plain format, like bytes, into Python objects - convertido de algún formato plano, como bytes, a objetos de Python">parse</abbr> como JSON, se lee directamente como `bytes`, y la función `magic_data_reader()` sería la encargada de parsearlo de alguna manera. -Sin embargo, podemos declarar el esquema esperado para el cuerpo del request. +Sin embargo, podemos declarar el esquema esperado para el request body. ### Tipo de contenido personalizado de OpenAPI { #custom-openapi-content-type } @@ -153,48 +153,16 @@ Y podrías hacer esto incluso si el tipo de datos en el request no es JSON. Por ejemplo, en esta aplicación no usamos la funcionalidad integrada de FastAPI para extraer el JSON Schema de los modelos Pydantic ni la validación automática para JSON. De hecho, estamos declarando el tipo de contenido del request como YAML, no JSON: -//// tab | Pydantic v2 - {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *} - -//// - -/// info | Información - -En la versión 1 de Pydantic el método para obtener el JSON Schema para un modelo se llamaba `Item.schema()`, en la versión 2 de Pydantic, el método se llama `Item.model_json_schema()`. - -/// - Sin embargo, aunque no estamos usando la funcionalidad integrada por defecto, aún estamos usando un modelo Pydantic para generar manualmente el JSON Schema para los datos que queremos recibir en YAML. Luego usamos el request directamente, y extraemos el cuerpo como `bytes`. Esto significa que FastAPI ni siquiera intentará parsear la carga útil del request como JSON. Y luego en nuestro código, parseamos ese contenido YAML directamente, y nuevamente estamos usando el mismo modelo Pydantic para validar el contenido YAML: -//// tab | Pydantic v2 - {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *} - -//// - -/// info | Información - -En la versión 1 de Pydantic el método para parsear y validar un objeto era `Item.parse_obj()`, en la versión 2 de Pydantic, el método se llama `Item.model_validate()`. - -/// - /// tip | Consejo Aquí reutilizamos el mismo modelo Pydantic. diff --git a/docs/es/docs/advanced/settings.md b/docs/es/docs/advanced/settings.md index 610bda5e2..a2d749103 100644 --- a/docs/es/docs/advanced/settings.md +++ b/docs/es/docs/advanced/settings.md @@ -46,12 +46,6 @@ $ pip install "fastapi[all]" </div> -/// info | Información - -En Pydantic v1 venía incluido con el paquete principal. Ahora se distribuye como este paquete independiente para que puedas elegir si instalarlo o no si no necesitas esa funcionalidad. - -/// - ### Crear el objeto `Settings` { #create-the-settings-object } Importa `BaseSettings` de Pydantic y crea una sub-clase, muy similar a un modelo de Pydantic. @@ -60,31 +54,15 @@ De la misma forma que con los modelos de Pydantic, declaras atributos de clase c Puedes usar todas las mismas funcionalidades de validación y herramientas que usas para los modelos de Pydantic, como diferentes tipos de datos y validaciones adicionales con `Field()`. -//// tab | Pydantic v2 - {* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} -//// - -//// tab | Pydantic v1 - -/// info | Información - -En Pydantic v1 importarías `BaseSettings` directamente desde `pydantic` en lugar de desde `pydantic_settings`. - -/// - -{* ../../docs_src/settings/tutorial001_pv1_py39.py hl[2,5:8,11] *} - -//// - /// tip | Consejo Si quieres algo rápido para copiar y pegar, no uses este ejemplo, usa el último más abajo. /// -Luego, cuando creas una instance de esa clase `Settings` (en este caso, en el objeto `settings`), Pydantic leerá las variables de entorno de una manera indiferente a mayúsculas y minúsculas, por lo que una variable en mayúsculas `APP_NAME` aún será leída para el atributo `app_name`. +Luego, cuando creas un instance de esa clase `Settings` (en este caso, en el objeto `settings`), Pydantic leerá las variables de entorno de una manera indiferente a mayúsculas y minúsculas, por lo que una variable en mayúsculas `APP_NAME` aún será leída para el atributo `app_name`. Luego convertirá y validará los datos. Así que, cuando uses ese objeto `settings`, tendrás datos de los tipos que declaraste (por ejemplo, `items_per_user` será un `int`). @@ -110,7 +88,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p /// tip | Consejo -Para establecer múltiples variables de entorno para un solo comando, simplemente sepáralas con un espacio y ponlas todas antes del comando. +Para establecer múltiples env vars para un solo comando, simplemente sepáralas con un espacio y ponlas todas antes del comando. /// @@ -150,7 +128,7 @@ Proveniente del ejemplo anterior, tu archivo `config.py` podría verse como: {* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} -Nota que ahora no creamos una instance por defecto `settings = Settings()`. +Nota que ahora no creamos un instance por defecto `settings = Settings()`. ### El archivo principal de la app { #the-main-app-file } @@ -172,11 +150,11 @@ Y luego podemos requerirlo desde la *path operation function* como una dependenc ### Configuraciones y pruebas { #settings-and-testing } -Luego sería muy fácil proporcionar un objeto de configuraciones diferente durante las pruebas al sobrescribir una dependencia para `get_settings`: +Luego sería muy fácil proporcionar un objeto de configuraciones diferente durante las pruebas al crear una sobrescritura de dependencia para `get_settings`: {* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *} -En la dependencia sobreescrita establecemos un nuevo valor para el `admin_email` al crear el nuevo objeto `Settings`, y luego devolvemos ese nuevo objeto. +En la sobrescritura de dependencia establecemos un nuevo valor para el `admin_email` al crear el nuevo objeto `Settings`, y luego devolvemos ese nuevo objeto. Luego podemos probar que se está usando. @@ -215,8 +193,6 @@ APP_NAME="ChimichangApp" Y luego actualizar tu `config.py` con: -//// tab | Pydantic v2 - {* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip | Consejo @@ -225,26 +201,6 @@ El atributo `model_config` se usa solo para configuración de Pydantic. Puedes l /// -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *} - -/// tip | Consejo - -La clase `Config` se usa solo para configuración de Pydantic. Puedes leer más en <a href="https://docs.pydantic.dev/1.10/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>. - -/// - -//// - -/// info | Información - -En la versión 1 de Pydantic la configuración se hacía en una clase interna `Config`, en la versión 2 de Pydantic se hace en un atributo `model_config`. Este atributo toma un `dict`, y para obtener autocompletado y errores en línea, puedes importar y usar `SettingsConfigDict` para definir ese `dict`. - -/// - Aquí definimos la configuración `env_file` dentro de tu clase Pydantic `Settings`, y establecemos el valor en el nombre del archivo con el archivo dotenv que queremos usar. ### Creando el `Settings` solo una vez con `lru_cache` { #creating-the-settings-only-once-with-lru-cache } @@ -331,7 +287,7 @@ participant execute as Ejecutar función end ``` -En el caso de nuestra dependencia `get_settings()`, la función ni siquiera toma argumentos, por lo que siempre devolverá el mismo valor. +En el caso de nuestra dependencia `get_settings()`, la función ni siquiera toma argumentos, por lo que siempre devuelve el mismo valor. De esa manera, se comporta casi como si fuera solo una variable global. Pero como usa una función de dependencia, entonces podemos sobrescribirla fácilmente para las pruebas. diff --git a/docs/es/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/es/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md index deda9f2e5..c862ace90 100644 --- a/docs/es/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md +++ b/docs/es/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -2,21 +2,23 @@ Si tienes una app de FastAPI antigua, podrías estar usando Pydantic versión 1. -FastAPI ha tenido compatibilidad con Pydantic v1 o v2 desde la versión 0.100.0. +FastAPI versión 0.100.0 tenía compatibilidad con Pydantic v1 o v2. Usaba la que tuvieras instalada. -Si tenías instalado Pydantic v2, lo usaba. Si en cambio tenías Pydantic v1, usaba ese. +FastAPI versión 0.119.0 introdujo compatibilidad parcial con Pydantic v1 desde dentro de Pydantic v2 (como `pydantic.v1`), para facilitar la migración a v2. -Pydantic v1 está deprecado y su soporte se eliminará en las próximas versiones de FastAPI, deberías migrar a Pydantic v2. Así obtendrás las funcionalidades, mejoras y correcciones más recientes. +FastAPI 0.126.0 eliminó la compatibilidad con Pydantic v1, aunque siguió soportando `pydantic.v1` por un poquito más de tiempo. /// warning | Advertencia -Además, el equipo de Pydantic dejó de dar soporte a Pydantic v1 para las versiones más recientes de Python, comenzando con Python 3.14. +El equipo de Pydantic dejó de dar soporte a Pydantic v1 para las versiones más recientes de Python, comenzando con **Python 3.14**. + +Esto incluye `pydantic.v1`, que ya no está soportado en Python 3.14 y superiores. Si quieres usar las funcionalidades más recientes de Python, tendrás que asegurarte de usar Pydantic v2. /// -Si tienes una app de FastAPI antigua con Pydantic v1, aquí te muestro cómo migrarla a Pydantic v2 y las nuevas funcionalidades en FastAPI 0.119.0 para ayudarte con una migración gradual. +Si tienes una app de FastAPI antigua con Pydantic v1, aquí te muestro cómo migrarla a Pydantic v2, y las **funcionalidades en FastAPI 0.119.0** para ayudarte con una migración gradual. ## Guía oficial { #official-guide } @@ -44,9 +46,9 @@ Después de esto, puedes ejecutar los tests y revisa si todo funciona. Si es as ## Pydantic v1 en v2 { #pydantic-v1-in-v2 } -Pydantic v2 incluye todo lo de Pydantic v1 como un submódulo `pydantic.v1`. +Pydantic v2 incluye todo lo de Pydantic v1 como un submódulo `pydantic.v1`. Pero esto ya no está soportado en versiones por encima de Python 3.13. -Esto significa que puedes instalar la versión más reciente de Pydantic v2 e importar y usar los componentes viejos de Pydantic v1 desde ese submódulo, como si tuvieras instalado el Pydantic v1 antiguo. +Esto significa que puedes instalar la versión más reciente de Pydantic v2 e importar y usar los componentes viejos de Pydantic v1 desde este submódulo, como si tuvieras instalado el Pydantic v1 antiguo. {* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} @@ -66,7 +68,7 @@ Ten en cuenta que, como el equipo de Pydantic ya no da soporte a Pydantic v1 en ### Pydantic v1 y v2 en la misma app { #pydantic-v1-and-v2-on-the-same-app } -No está soportado por Pydantic tener un modelo de Pydantic v2 con sus propios campos definidos como modelos de Pydantic v1 o viceversa. +**No está soportado** por Pydantic tener un modelo de Pydantic v2 con sus propios campos definidos como modelos de Pydantic v1 o viceversa. ```mermaid graph TB @@ -106,7 +108,7 @@ graph TB style V2Field fill:#f9fff3 ``` -En algunos casos, incluso es posible tener modelos de Pydantic v1 y v2 en la misma path operation de tu app de FastAPI: +En algunos casos, incluso es posible tener modelos de Pydantic v1 y v2 en la misma **path operation** de tu app de FastAPI: {* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} @@ -122,12 +124,12 @@ Si necesitas usar algunas de las herramientas específicas de FastAPI para pará /// tip | Consejo -Primero prueba con `bump-pydantic`; si tus tests pasan y eso funciona, entonces terminaste con un solo comando. ✨ +Primero prueba con `bump-pydantic`, si tus tests pasan y eso funciona, entonces terminaste con un solo comando. ✨ /// -Si `bump-pydantic` no funciona para tu caso, puedes usar la compatibilidad de modelos Pydantic v1 y v2 en la misma app para hacer la migración a Pydantic v2 de forma gradual. +Si `bump-pydantic` no funciona para tu caso de uso, puedes usar la compatibilidad de modelos Pydantic v1 y v2 en la misma app para hacer la migración a Pydantic v2 de forma gradual. -Podrías primero actualizar Pydantic para usar la última versión 2 y cambiar los imports para usar `pydantic.v1` para todos tus modelos. +Podrías primero actualizar Pydantic para usar la última versión 2, y cambiar los imports para usar `pydantic.v1` para todos tus modelos. Luego puedes empezar a migrar tus modelos de Pydantic v1 a v2 por grupos, en pasos graduales. 🚶 diff --git a/docs/es/docs/how-to/separate-openapi-schemas.md b/docs/es/docs/how-to/separate-openapi-schemas.md index 72396f8c9..903313599 100644 --- a/docs/es/docs/how-to/separate-openapi-schemas.md +++ b/docs/es/docs/how-to/separate-openapi-schemas.md @@ -1,6 +1,6 @@ # Separación de Esquemas OpenAPI para Entrada y Salida o No { #separate-openapi-schemas-for-input-and-output-or-not } -Al usar **Pydantic v2**, el OpenAPI generado es un poco más exacto y **correcto** que antes. 😎 +Desde que se lanzó **Pydantic v2**, el OpenAPI generado es un poco más exacto y **correcto** que antes. 😎 De hecho, en algunos casos, incluso tendrá **dos JSON Schemas** en OpenAPI para el mismo modelo Pydantic, para entrada y salida, dependiendo de si tienen **valores por defecto**. @@ -85,7 +85,7 @@ Probablemente el caso principal para esto es si ya tienes algún código cliente En ese caso, puedes desactivar esta funcionalidad en **FastAPI**, con el parámetro `separate_input_output_schemas=False`. -/// info | Información +/// info El soporte para `separate_input_output_schemas` fue agregado en FastAPI `0.102.0`. 🤓 @@ -100,5 +100,3 @@ Y ahora habrá un único esquema para entrada y salida para el modelo, solo `Ite <div class="screenshot"> <img src="/img/tutorial/separate-openapi-schemas/image05.png"> </div> - -Este es el mismo comportamiento que en Pydantic v1. 🤓 diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index c14369a11..14fa07e41 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -93,13 +93,13 @@ Las funcionalidades clave son: "_Estoy súper emocionado con **FastAPI**. ¡Es tan divertido!_" -<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">host del podcast Python Bytes</a></strong> <a href="https://x.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div> +<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> host del podcast</strong> <a href="https://x.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div> --- "_Honestamente, lo que has construido parece súper sólido y pulido. En muchos aspectos, es lo que quería que **Hug** fuera; es realmente inspirador ver a alguien construir eso._" -<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://github.com/hugapi/hug" target="_blank">creador de Hug</a></strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> +<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://github.com/hugapi/hug" target="_blank">Hug</a> creador</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> --- @@ -117,6 +117,12 @@ Las funcionalidades clave son: --- +## Mini documental de FastAPI { #fastapi-mini-documentary } + +Hay un <a href="https://www.youtube.com/watch?v=mpR8ngthqiE" class="external-link" target="_blank">mini documental de FastAPI</a> lanzado a finales de 2025, puedes verlo online: + +<a href="https://www.youtube.com/watch?v=mpR8ngthqiE" target="_blank"><img src="https://fastapi.tiangolo.com/img/fastapi-documentary.jpg" alt="FastAPI Mini Documentary"></a> + ## **Typer**, el FastAPI de las CLIs { #typer-the-fastapi-of-clis } <a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> @@ -268,7 +274,7 @@ Verás la documentación interactiva automática de la API (proporcionada por <a ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Documentación de API Alternativa { #alternative-api-docs } +### Documentación alternativa de la API { #alternative-api-docs } Y ahora, ve a <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. @@ -276,7 +282,7 @@ Verás la documentación alternativa automática (proporcionada por <a href="htt ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Actualización del Ejemplo { #example-upgrade } +## Actualización del ejemplo { #example-upgrade } Ahora modifica el archivo `main.py` para recibir un body desde un request `PUT`. @@ -314,7 +320,7 @@ def update_item(item_id: int, item: Item): El servidor `fastapi dev` debería recargarse automáticamente. -### Actualización de la Documentación Interactiva de la API { #interactive-api-docs-upgrade } +### Actualización de la documentación interactiva de la API { #interactive-api-docs-upgrade } Ahora ve a <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. @@ -330,7 +336,7 @@ Ahora ve a <a href="http://127.0.0.1:8000/docs" class="external-link" target="_b ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Actualización de la Documentación Alternativa de la API { #alternative-api-docs-upgrade } +### Actualización de la documentación alternativa de la API { #alternative-api-docs-upgrade } Y ahora, ve a <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. @@ -393,13 +399,13 @@ Volviendo al ejemplo de código anterior, **FastAPI**: * Validará que haya un `item_id` en el path para requests `GET` y `PUT`. * Validará que el `item_id` sea del tipo `int` para requests `GET` y `PUT`. * Si no lo es, el cliente verá un error útil y claro. -* Comprobará si hay un parámetro de query opcional llamado `q` (como en `http://127.0.0.1:8000/items/foo?q=somequery`) para requests `GET`. +* Revisa si hay un parámetro de query opcional llamado `q` (como en `http://127.0.0.1:8000/items/foo?q=somequery`) para requests `GET`. * Como el parámetro `q` está declarado con `= None`, es opcional. * Sin el `None` sería requerido (como lo es el body en el caso con `PUT`). * Para requests `PUT` a `/items/{item_id}`, leerá el body como JSON: - * Comprobará que tiene un atributo requerido `name` que debe ser un `str`. - * Comprobará que tiene un atributo requerido `price` que debe ser un `float`. - * Comprobará que tiene un atributo opcional `is_offer`, que debe ser un `bool`, si está presente. + * Revisa que tiene un atributo requerido `name` que debe ser un `str`. + * Revisa que tiene un atributo requerido `price` que debe ser un `float`. + * Revisa que tiene un atributo opcional `is_offer`, que debe ser un `bool`, si está presente. * Todo esto también funcionaría para objetos JSON profundamente anidados. * Convertirá de y a JSON automáticamente. * Documentará todo con OpenAPI, que puede ser usado por: diff --git a/docs/es/docs/tutorial/bigger-applications.md b/docs/es/docs/tutorial/bigger-applications.md index eacdb13c7..7938a1215 100644 --- a/docs/es/docs/tutorial/bigger-applications.md +++ b/docs/es/docs/tutorial/bigger-applications.md @@ -56,7 +56,7 @@ from app.routers import items La misma estructura de archivos con comentarios: -``` +```bash . ├── app # "app" es un paquete de Python │   ├── __init__.py # este archivo hace que "app" sea un "paquete de Python" @@ -185,7 +185,7 @@ El resultado final es que los paths de item son ahora: * Todos incluirán las `responses` predefinidas. * Todas estas *path operations* tendrán la lista de `dependencies` evaluadas/ejecutadas antes de ellas. * Si también declaras dependencias en una *path operation* específica, **también se ejecutarán**. - * Las dependencias del router se ejecutan primero, luego las [dependencias en el decorador](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, y luego las dependencias de parámetros normales. + * Las dependencias del router se ejecutan primero, luego las [`dependencies` en el decorador](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, y luego las dependencias de parámetros normales. * También puedes agregar [dependencias de `Security` con `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}. /// tip | Consejo @@ -214,7 +214,7 @@ Así que usamos un import relativo con `..` para las dependencias: /// tip | Consejo -Si sabes perfectamente cómo funcionan los imports, continúa a la siguiente sección. +Si sabes perfectamente cómo funcionan los imports, continúa a la siguiente sección abajo. /// @@ -271,7 +271,7 @@ eso significaría: Eso se referiría a algún paquete arriba de `app/`, con su propio archivo `__init__.py`, etc. Pero no tenemos eso. Así que, eso lanzaría un error en nuestro ejemplo. 🚨 -Pero ahora sabes cómo funciona, para que puedas usar imports relativos en tus propias aplicaciones sin importar cuán complejas sean. 🤓 +Pero ahora sabes cómo funciona, para que puedas usar imports relativos en tus propias apps sin importar cuán complejas sean. 🤓 ### Agregar algunos `tags`, `responses`, y `dependencies` personalizados { #add-some-custom-tags-responses-and-dependencies } @@ -283,7 +283,7 @@ Pero aún podemos agregar _más_ `tags` que se aplicarán a una *path operation* /// tip | Consejo -Esta última *path operation* tendrá la combinación de tags: `["items", "custom"]`. +Esta última path operation tendrá la combinación de tags: `["items", "custom"]`. Y también tendrá ambas responses en la documentación, una para `404` y otra para `403`. @@ -301,7 +301,7 @@ Y como la mayor parte de tu lógica ahora vivirá en su propio módulo específi ### Importar `FastAPI` { #import-fastapi } -Importas y creas una clase `FastAPI` como de costumbre. +Importas y creas una clase `FastAPI` como normalmente. Y podemos incluso declarar [dependencias globales](dependencies/global-dependencies.md){.internal-link target=_blank} que se combinarán con las dependencias para cada `APIRouter`: @@ -398,7 +398,7 @@ Incluirá todas las rutas de ese router como parte de ella. En realidad creará internamente una *path operation* para cada *path operation* que fue declarada en el `APIRouter`. -Así, detrás de escena, funcionará como si todo fuera la misma única aplicación. +Así, detrás de escena, funcionará como si todo fuera la misma única app. /// @@ -430,20 +430,20 @@ Podemos declarar todo eso sin tener que modificar el `APIRouter` original pasand De esa manera, el `APIRouter` original permanecerá sin modificar, por lo que aún podemos compartir ese mismo archivo `app/internal/admin.py` con otros proyectos en la organización. -El resultado es que, en nuestra aplicación, cada una de las *path operations* del módulo `admin` tendrá: +El resultado es que, en nuestra app, cada una de las *path operations* del módulo `admin` tendrá: * El prefix `/admin`. * El tag `admin`. * La dependencia `get_token_header`. * La response `418`. 🍵 -Pero eso solo afectará a ese `APIRouter` en nuestra aplicación, no en ningún otro código que lo utilice. +Pero eso solo afectará a ese `APIRouter` en nuestra app, no en ningún otro código que lo utilice. Así, por ejemplo, otros proyectos podrían usar el mismo `APIRouter` con un método de autenticación diferente. ### Incluir una *path operation* { #include-a-path-operation } -También podemos agregar *path operations* directamente a la aplicación de `FastAPI`. +También podemos agregar *path operations* directamente a la app de `FastAPI`. Aquí lo hacemos... solo para mostrar que podemos 🤷: @@ -461,13 +461,13 @@ Los `APIRouter`s no están "montados", no están aislados del resto de la aplica Esto se debe a que queremos incluir sus *path operations* en el esquema de OpenAPI y las interfaces de usuario. -Como no podemos simplemente aislarlos y "montarlos" independientemente del resto, se "clonan" las *path operations* (se vuelven a crear), no se incluyen directamente. +Como no podemos simplemente aislarlos y "montarlos" independientemente del resto, las *path operations* se "clonan" (se vuelven a crear), no se incluyen directamente. /// ## Revisa la documentación automática de la API { #check-the-automatic-api-docs } -Ahora, ejecuta tu aplicación: +Ahora, ejecuta tu app: <div class="termy"> @@ -481,7 +481,7 @@ $ fastapi dev app/main.py Y abre la documentación en <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. -Verás la documentación automática de la API, incluyendo los paths de todos los submódulos, usando los paths correctos (y prefijos) y las tags correctas: +Verás la documentación automática de la API, incluyendo los paths de todos los submódulos, usando los paths correctos (y prefijos) y los tags correctos: <img src="/img/tutorial/bigger-applications/image01.png"> @@ -501,4 +501,4 @@ De la misma manera que puedes incluir un `APIRouter` en una aplicación `FastAPI router.include_router(other_router) ``` -Asegúrate de hacerlo antes de incluir `router` en la aplicación de `FastAPI`, para que las *path operations* de `other_router` también se incluyan. +Asegúrate de hacerlo antes de incluir `router` en la app de `FastAPI`, para que las *path operations* de `other_router` también se incluyan. diff --git a/docs/es/docs/tutorial/body-updates.md b/docs/es/docs/tutorial/body-updates.md index 1f4713f35..e75e29b54 100644 --- a/docs/es/docs/tutorial/body-updates.md +++ b/docs/es/docs/tutorial/body-updates.md @@ -1,4 +1,4 @@ -# Cuerpo - Actualizaciones { #body-updates } +# Body - Actualizaciones { #body-updates } ## Actualización reemplazando con `PUT` { #update-replacing-with-put } @@ -50,14 +50,6 @@ Si quieres recibir actualizaciones parciales, es muy útil usar el parámetro `e Como `item.model_dump(exclude_unset=True)`. -/// info | Información - -En Pydantic v1 el método se llamaba `.dict()`, fue deprecado (pero aún soportado) en Pydantic v2, y renombrado a `.model_dump()`. - -Los ejemplos aquí usan `.dict()` para compatibilidad con Pydantic v1, pero deberías usar `.model_dump()` si puedes usar Pydantic v2. - -/// - Eso generaría un `dict` solo con los datos que se establecieron al crear el modelo `item`, excluyendo los valores por defecto. Luego puedes usar esto para generar un `dict` solo con los datos que se establecieron (enviados en el request), omitiendo los valores por defecto: @@ -68,14 +60,6 @@ Luego puedes usar esto para generar un `dict` solo con los datos que se establec Ahora, puedes crear una copia del modelo existente usando `.model_copy()`, y pasar el parámetro `update` con un `dict` que contenga los datos a actualizar. -/// info | Información - -En Pydantic v1 el método se llamaba `.copy()`, fue deprecado (pero aún soportado) en Pydantic v2, y renombrado a `.model_copy()`. - -Los ejemplos aquí usan `.copy()` para compatibilidad con Pydantic v1, pero deberías usar `.model_copy()` si puedes usar Pydantic v2. - -/// - Como `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} @@ -90,9 +74,9 @@ En resumen, para aplicar actualizaciones parciales deberías: * Generar un `dict` sin valores por defecto del modelo de entrada (usando `exclude_unset`). * De esta manera puedes actualizar solo los valores realmente establecidos por el usuario, en lugar de sobrescribir valores ya almacenados con valores por defecto en tu modelo. * Crear una copia del modelo almacenado, actualizando sus atributos con las actualizaciones parciales recibidas (usando el parámetro `update`). -* Convertir el modelo copiado en algo que pueda almacenarse en tu base de datos (por ejemplo, usando el `jsonable_encoder`). +* Convertir el modelo copiado en algo que pueda almacenarse en tu DB (por ejemplo, usando el `jsonable_encoder`). * Esto es comparable a usar el método `.model_dump()` del modelo de nuevo, pero asegura (y convierte) los valores a tipos de datos que pueden convertirse a JSON, por ejemplo, `datetime` a `str`. -* Guardar los datos en tu base de datos. +* Guardar los datos en tu DB. * Devolver el modelo actualizado. {* ../../docs_src/body_updates/tutorial002_py310.py hl[28:35] *} diff --git a/docs/es/docs/tutorial/body.md b/docs/es/docs/tutorial/body.md index 06a70dbc7..dde39f78c 100644 --- a/docs/es/docs/tutorial/body.md +++ b/docs/es/docs/tutorial/body.md @@ -32,7 +32,8 @@ Usa tipos estándar de Python para todos los atributos: {* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} -Al igual que al declarar parámetros de query, cuando un atributo del modelo tiene un valor por defecto, no es obligatorio. De lo contrario, es obligatorio. Usa `None` para hacerlo opcional. + +Al igual que al declarar parámetros de query, cuando un atributo del modelo tiene un valor por defecto, no es obligatorio. De lo contrario, es obligatorio. Usa `None` para hacerlo solo opcional. Por ejemplo, el modelo anterior declara un “`object`” JSON (o `dict` en Python) como: @@ -127,14 +128,6 @@ Dentro de la función, puedes acceder a todos los atributos del objeto modelo di {* ../../docs_src/body/tutorial002_py310.py *} -/// info | Información - -En Pydantic v1 el método se llamaba `.dict()`, se marcó como obsoleto (pero sigue soportado) en Pydantic v2, y se renombró a `.model_dump()`. - -Los ejemplos aquí usan `.dict()` por compatibilidad con Pydantic v1, pero deberías usar `.model_dump()` si puedes usar Pydantic v2. - -/// - ## Request body + parámetros de path { #request-body-path-parameters } Puedes declarar parámetros de path y request body al mismo tiempo. @@ -143,6 +136,7 @@ Puedes declarar parámetros de path y request body al mismo tiempo. {* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} + ## Request body + path + parámetros de query { #request-body-path-query-parameters } También puedes declarar parámetros de **body**, **path** y **query**, todos al mismo tiempo. @@ -155,7 +149,7 @@ Los parámetros de la función se reconocerán de la siguiente manera: * Si el parámetro también se declara en el **path**, se utilizará como un parámetro de path. * Si el parámetro es de un **tipo singular** (como `int`, `float`, `str`, `bool`, etc.), se interpretará como un parámetro de **query**. -* Si el parámetro se declara como del tipo de un **modelo de Pydantic**, se interpretará como un **request body**. +* Si el parámetro se declara como del tipo de un **modelo de Pydantic**, se interpretará como un **body** de request. /// note | Nota diff --git a/docs/es/docs/tutorial/extra-models.md b/docs/es/docs/tutorial/extra-models.md index ed5bc80d9..d72c73e24 100644 --- a/docs/es/docs/tutorial/extra-models.md +++ b/docs/es/docs/tutorial/extra-models.md @@ -22,21 +22,13 @@ Aquí tienes una idea general de cómo podrían ser los modelos con sus campos d {* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} -/// info | Información +### Acerca de `**user_in.model_dump()` { #about-user-in-model-dump } -En Pydantic v1 el método se llamaba `.dict()`, fue deprecado (pero aún soportado) en Pydantic v2, y renombrado a `.model_dump()`. - -Los ejemplos aquí usan `.dict()` para compatibilidad con Pydantic v1, pero deberías usar `.model_dump()` en su lugar si puedes usar Pydantic v2. - -/// - -### Acerca de `**user_in.dict()` { #about-user-in-dict } - -#### `.dict()` de Pydantic { #pydantics-dict } +#### `.model_dump()` de Pydantic { #pydantics-model-dump } `user_in` es un modelo Pydantic de la clase `UserIn`. -Los modelos Pydantic tienen un método `.dict()` que devuelve un `dict` con los datos del modelo. +Los modelos Pydantic tienen un método `.model_dump()` que devuelve un `dict` con los datos del modelo. Así que, si creamos un objeto Pydantic `user_in` como: @@ -47,7 +39,7 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com y luego llamamos a: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() ``` ahora tenemos un `dict` con los datos en la variable `user_dict` (es un `dict` en lugar de un objeto modelo Pydantic). @@ -58,7 +50,7 @@ Y si llamamos a: print(user_dict) ``` -obtendremos un `dict` de Python con: +obtendríamos un `dict` de Python con: ```Python { @@ -103,20 +95,20 @@ UserInDB( #### Un modelo Pydantic a partir del contenido de otro { #a-pydantic-model-from-the-contents-of-another } -Como en el ejemplo anterior obtuvimos `user_dict` de `user_in.dict()`, este código: +Como en el ejemplo anterior obtuvimos `user_dict` de `user_in.model_dump()`, este código: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() UserInDB(**user_dict) ``` sería equivalente a: ```Python -UserInDB(**user_in.dict()) +UserInDB(**user_in.model_dump()) ``` -...porque `user_in.dict()` es un `dict`, y luego hacemos que Python lo "desempaquete" al pasarlo a `UserInDB` con el prefijo `**`. +...porque `user_in.model_dump()` es un `dict`, y luego hacemos que Python lo "desempaquete" al pasarlo a `UserInDB` con el prefijo `**`. Así, obtenemos un modelo Pydantic a partir de los datos en otro modelo Pydantic. @@ -125,7 +117,7 @@ Así, obtenemos un modelo Pydantic a partir de los datos en otro modelo Pydantic Y luego agregando el argumento de palabra clave adicional `hashed_password=hashed_password`, como en: ```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) ``` ...termina siendo como: @@ -156,7 +148,7 @@ Y estos modelos están compartiendo muchos de los datos y duplicando nombres y t Podríamos hacerlo mejor. -Podemos declarar un modelo `UserBase` que sirva como base para nuestros otros modelos. Y luego podemos hacer subclases de ese modelo que heredan sus atributos (anotaciones de tipos, validación, etc). +Podemos declarar un modelo `UserBase` que sirva como base para nuestros otros modelos. Y luego podemos hacer subclases de ese modelo que heredan sus atributos (declaraciones de tipos, validación, etc). Toda la conversión de datos, validación, documentación, etc. seguirá funcionando normalmente. @@ -180,20 +172,19 @@ Al definir una <a href="https://docs.pydantic.dev/latest/concepts/types/#unions" {* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *} - ### `Union` en Python 3.10 { #union-in-python-3-10 } En este ejemplo pasamos `Union[PlaneItem, CarItem]` como el valor del argumento `response_model`. -Porque lo estamos pasando como un **valor a un argumento** en lugar de ponerlo en **anotaciones de tipos**, tenemos que usar `Union` incluso en Python 3.10. +Porque lo estamos pasando como un **valor a un argumento** en lugar de ponerlo en una **anotación de tipos**, tenemos que usar `Union` incluso en Python 3.10. -Si estuviera en anotaciones de tipos podríamos haber usado la barra vertical, como: +Si estuviera en una anotación de tipos podríamos haber usado la barra vertical, como: ```Python some_variable: PlaneItem | CarItem ``` -Pero si ponemos eso en la asignación `response_model=PlaneItem | CarItem` obtendríamos un error, porque Python intentaría realizar una **operación inválida** entre `PlaneItem` y `CarItem` en lugar de interpretar eso como anotaciones de tipos. +Pero si ponemos eso en la asignación `response_model=PlaneItem | CarItem` obtendríamos un error, porque Python intentaría realizar una **operación inválida** entre `PlaneItem` y `CarItem` en lugar de interpretar eso como una anotación de tipos. ## Lista de modelos { #list-of-models } @@ -203,7 +194,6 @@ Para eso, usa el `typing.List` estándar de Python (o simplemente `list` en Pyth {* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *} - ## Response con `dict` arbitrario { #response-with-arbitrary-dict } También puedes declarar un response usando un `dict` arbitrario plano, declarando solo el tipo de las claves y valores, sin usar un modelo Pydantic. @@ -214,7 +204,6 @@ En este caso, puedes usar `typing.Dict` (o solo `dict` en Python 3.9 y posterior {* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *} - ## Recapitulación { #recap } Usa múltiples modelos Pydantic y hereda libremente para cada caso. diff --git a/docs/es/docs/tutorial/query-params-str-validations.md b/docs/es/docs/tutorial/query-params-str-validations.md index e3f8cb594..4af4782f8 100644 --- a/docs/es/docs/tutorial/query-params-str-validations.md +++ b/docs/es/docs/tutorial/query-params-str-validations.md @@ -109,7 +109,7 @@ FastAPI ahora: ## Alternativa (antigua): `Query` como valor por defecto { #alternative-old-query-as-the-default-value } -Versiones anteriores de FastAPI (antes de <abbr title="antes de 2023-03">0.95.0</abbr>) requerían que usaras `Query` como el valor por defecto de tu parámetro, en lugar de ponerlo en `Annotated`, hay una alta probabilidad de que veas código usándolo alrededor, así que te lo explicaré. +Versiones anteriores de FastAPI (antes de <abbr title="before 2023-03 - antes de 2023-03">0.95.0</abbr>) requerían que usaras `Query` como el valor por defecto de tu parámetro, en lugar de ponerlo en `Annotated`, hay una alta probabilidad de que veas código usándolo alrededor, así que te lo explicaré. /// tip | Consejo @@ -192,7 +192,7 @@ También puedes agregar un parámetro `min_length`: ## Agregar expresiones regulares { #add-regular-expressions } -Puedes definir un <abbr title="Una expresión regular, regex o regexp es una secuencia de caracteres que define un patrón de búsqueda para strings.">expresión regular</abbr> `pattern` que el parámetro debe coincidir: +Puedes definir una <abbr title="Una expresión regular, regex o regexp es una secuencia de caracteres que define un patrón de búsqueda para strings.">expresión regular</abbr> `pattern` que el parámetro debe coincidir: {* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} @@ -206,20 +206,6 @@ Si te sientes perdido con todas estas ideas de **"expresión regular"**, no te p Ahora sabes que cuando las necesites puedes usarlas en **FastAPI**. -### Pydantic v1 `regex` en lugar de `pattern` { #pydantic-v1-regex-instead-of-pattern } - -Antes de la versión 2 de Pydantic y antes de FastAPI 0.100.0, el parámetro se llamaba `regex` en lugar de `pattern`, pero ahora está en desuso. - -Todavía podrías ver algo de código que lo usa: - -//// tab | Pydantic v1 - -{* ../../docs_src/query_params_str_validations/tutorial004_regex_an_py310.py hl[11] *} - -//// - -Pero que sepas que esto está deprecado y debería actualizarse para usar el nuevo parámetro `pattern`. 🤓 - ## Valores por defecto { #default-values } Puedes, por supuesto, usar valores por defecto diferentes de `None`. @@ -280,7 +266,7 @@ Entonces, con una URL como: http://localhost:8000/items/?q=foo&q=bar ``` -recibirías los múltiples valores del *query parameter* `q` (`foo` y `bar`) en una `list` de Python dentro de tu *path operation function*, en el *parámetro de función* `q`. +recibirías los múltiples valores de los *query parameters* `q` (`foo` y `bar`) en una `list` de Python dentro de tu *path operation function*, en el *parámetro de función* `q`. Entonces, el response a esa URL sería: @@ -386,7 +372,7 @@ Entonces puedes declarar un `alias`, y ese alias será usado para encontrar el v Ahora digamos que ya no te gusta este parámetro. -Tienes que dejarlo allí por un tiempo porque hay clientes usándolo, pero quieres que la documentación lo muestre claramente como <abbr title="obsoleto, se recomienda no usarlo">deprecated</abbr>. +Tienes que dejarlo allí por un tiempo porque hay clientes usándolo, pero quieres que la documentación lo muestre claramente como <abbr title="obsolete, recommended not to use it - obsoleto, se recomienda no usarlo">deprecated</abbr>. Luego pasa el parámetro `deprecated=True` a `Query`: @@ -416,7 +402,7 @@ Pydantic también tiene <a href="https://docs.pydantic.dev/latest/concepts/valid /// -Por ejemplo, este validador personalizado comprueba que el ID del ítem empiece con `isbn-` para un número de libro <abbr title="International Standard Book Number – Número Estándar Internacional de Libro">ISBN</abbr> o con `imdb-` para un ID de URL de película de <abbr title="IMDB (Internet Movie Database) es un sitio web con información sobre películas">IMDB</abbr>: +Por ejemplo, este validador personalizado comprueba que el ID del ítem empiece con `isbn-` para un número de libro <abbr title="ISBN means International Standard Book Number - ISBN significa International Standard Book Number">ISBN</abbr> o con `imdb-` para un ID de URL de película de <abbr title="IMDB (Internet Movie Database) is a website with information about movies - IMDB (Internet Movie Database) es un sitio web con información sobre películas">IMDB</abbr>: {* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} @@ -450,7 +436,7 @@ Pero si te da curiosidad este ejemplo de código específico y sigues entretenid #### Un ítem aleatorio { #a-random-item } -Con `data.items()` obtenemos un <abbr title="Algo que podemos iterar con un for, como una list, set, etc.">objeto iterable</abbr> con tuplas que contienen la clave y el valor para cada elemento del diccionario. +Con `data.items()` obtenemos un <abbr title="Algo que podemos iterar con un for loop, como una list, set, etc.">objeto iterable</abbr> con tuplas que contienen la clave y el valor para cada elemento del diccionario. Convertimos este objeto iterable en una `list` propiamente dicha con `list(data.items())`. diff --git a/docs/es/docs/tutorial/response-model.md b/docs/es/docs/tutorial/response-model.md index 8f0ad5652..8cfe69e78 100644 --- a/docs/es/docs/tutorial/response-model.md +++ b/docs/es/docs/tutorial/response-model.md @@ -2,7 +2,7 @@ Puedes declarar el tipo utilizado para el response anotando el **tipo de retorno** de la *path operation function*. -Puedes utilizar **anotaciones de tipos** de la misma manera que lo harías para datos de entrada en **parámetros** de función, puedes utilizar modelos de Pydantic, list, diccionarios, valores escalares como enteros, booleanos, etc. +Puedes utilizar **anotaciones de tipos** de la misma manera que lo harías para datos de entrada en **parámetros** de función, puedes utilizar modelos de Pydantic, lists, diccionarios, valores escalares como enteros, booleanos, etc. {* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} @@ -27,7 +27,7 @@ Por ejemplo, podrías querer **devolver un diccionario** u objeto de base de dat Si añadiste la anotación del tipo de retorno, las herramientas y editores se quejarían con un error (correcto) diciéndote que tu función está devolviendo un tipo (por ejemplo, un dict) que es diferente de lo que declaraste (por ejemplo, un modelo de Pydantic). -En esos casos, puedes usar el parámetro del decorador de path operation `response_model` en lugar del tipo de retorno. +En esos casos, puedes usar el parámetro del *decorador de path operation* `response_model` en lugar del tipo de retorno. Puedes usar el parámetro `response_model` en cualquiera de las *path operations*: @@ -153,7 +153,7 @@ Primero vamos a ver cómo los editores, mypy y otras herramientas verían esto. `BaseUser` tiene los campos base. Luego `UserIn` hereda de `BaseUser` y añade el campo `password`, por lo que incluirá todos los campos de ambos modelos. -Anotamos el tipo de retorno de la función como `BaseUser`, pero en realidad estamos devolviendo un instance de `UserIn`. +Anotamos el tipo de retorno de la función como `BaseUser`, pero en realidad estamos devolviendo un `UserIn` instance. El editor, mypy y otras herramientas no se quejarán de esto porque, en términos de tipificación, `UserIn` es una subclase de `BaseUser`, lo que significa que es un tipo *válido* cuando se espera algo que es un `BaseUser`. @@ -252,20 +252,6 @@ Entonces, si envías un request a esa *path operation* para el ítem con ID `foo /// info | Información -En Pydantic v1 el método se llamaba `.dict()`, fue deprecado (pero aún soportado) en Pydantic v2, y renombrado a `.model_dump()`. - -Los ejemplos aquí usan `.dict()` para compatibilidad con Pydantic v1, pero deberías usar `.model_dump()` en su lugar si puedes usar Pydantic v2. - -/// - -/// info | Información - -FastAPI usa el método `.dict()` del modelo de Pydantic con <a href="https://docs.pydantic.dev/1.10/usage/exporting_models/#modeldict" class="external-link" target="_blank">su parámetro `exclude_unset`</a> para lograr esto. - -/// - -/// info | Información - También puedes usar: * `response_model_exclude_defaults=True` diff --git a/docs/es/docs/tutorial/schema-extra-example.md b/docs/es/docs/tutorial/schema-extra-example.md index 686ea1a23..396a2a6bf 100644 --- a/docs/es/docs/tutorial/schema-extra-example.md +++ b/docs/es/docs/tutorial/schema-extra-example.md @@ -8,35 +8,13 @@ Aquí tienes varias formas de hacerlo. Puedes declarar `examples` para un modelo de Pydantic que se añadirá al JSON Schema generado. -//// tab | Pydantic v2 - {* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -//// +Esa información extra se añadirá tal cual al **JSON Schema** resultante para ese modelo, y se usará en la documentación de la API. -//// tab | Pydantic v1 +Puedes usar el atributo `model_config` que toma un `dict` como se describe en <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">la documentación de Pydantic: Configuración</a>. -{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *} - -//// - -Esa información extra se añadirá tal cual al **JSON Schema** generado para ese modelo, y se usará en la documentación de la API. - -//// tab | Pydantic v2 - -En Pydantic versión 2, usarías el atributo `model_config`, que toma un `dict` como se describe en <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">la documentación de Pydantic: Configuración</a>. - -Puedes establecer `"json_schema_extra"` con un `dict` que contenga cualquier dato adicional que desees que aparezca en el JSON Schema generado, incluyendo `examples`. - -//// - -//// tab | Pydantic v1 - -En Pydantic versión 1, usarías una clase interna `Config` y `schema_extra`, como se describe en <a href="https://docs.pydantic.dev/1.10/usage/schema/#schema-customization" class="external-link" target="_blank">la documentación de Pydantic: Personalización de Esquema</a>. - -Puedes establecer `schema_extra` con un `dict` que contenga cualquier dato adicional que desees que aparezca en el JSON Schema generado, incluyendo `examples`. - -//// +Puedes establecer `"json_schema_extra"` con un `dict` que contenga cualquier dato adicional que te gustaría que aparezca en el JSON Schema generado, incluyendo `examples`. /// tip | Consejo @@ -50,7 +28,7 @@ Por ejemplo, podrías usarlo para añadir metadatos para una interfaz de usuario OpenAPI 3.1.0 (usado desde FastAPI 0.99.0) añadió soporte para `examples`, que es parte del estándar de **JSON Schema**. -Antes de eso, solo soportaba la palabra clave `example` con un solo ejemplo. Eso aún es soportado por OpenAPI 3.1.0, pero está obsoleto y no es parte del estándar de JSON Schema. Así que se recomienda migrar de `example` a `examples`. 🤓 +Antes de eso, solo soportaba la palabra clave `example` con un solo ejemplo. Eso aún es soportado por OpenAPI 3.1.0, pero está obsoleto y no es parte del estándar de JSON Schema. Así que se te anima a migrar `example` a `examples`. 🤓 Puedes leer más al final de esta página. @@ -94,7 +72,7 @@ Por supuesto, también puedes pasar múltiples `examples`: {* ../../docs_src/schema_extra_example/tutorial004_an_py310.py hl[23:38] *} -Cuando haces esto, los ejemplos serán parte del **JSON Schema** interno para esos datos de body. +Cuando haces esto, los ejemplos serán parte del **JSON Schema** interno para esos datos del body. Sin embargo, al <abbr title="2023-08-26">momento de escribir esto</abbr>, Swagger UI, la herramienta encargada de mostrar la interfaz de documentación, no soporta mostrar múltiples ejemplos para los datos en **JSON Schema**. Pero lee más abajo para una solución alternativa. @@ -203,17 +181,17 @@ Debido a eso, las versiones de FastAPI anteriores a 0.99.0 todavía usaban versi ### `examples` de Pydantic y FastAPI { #pydantic-and-fastapi-examples } -Cuando añades `examples` dentro de un modelo de Pydantic, usando `schema_extra` o `Field(examples=["algo"])`, ese ejemplo se añade al **JSON Schema** para ese modelo de Pydantic. +Cuando añades `examples` dentro de un modelo de Pydantic, usando `schema_extra` o `Field(examples=["something"])`, ese ejemplo se añade al **JSON Schema** para ese modelo de Pydantic. Y ese **JSON Schema** del modelo de Pydantic se incluye en el **OpenAPI** de tu API, y luego se usa en la interfaz de documentación. -En las versiones de FastAPI antes de 0.99.0 (0.99.0 y superior usan el nuevo OpenAPI 3.1.0) cuando usabas `example` o `examples` con cualquiera de las otras utilidades (`Query()`, `Body()`, etc.) esos ejemplos no se añadían al JSON Schema que describe esos datos (ni siquiera a la propia versión de JSON Schema de OpenAPI), se añadían directamente a la declaración de la *path operation* en OpenAPI (fuera de las partes de OpenAPI que usan JSON Schema). +En las versiones de FastAPI antes de 0.99.0 (0.99.0 y superiores usan el nuevo OpenAPI 3.1.0) cuando usabas `example` o `examples` con cualquiera de las otras utilidades (`Query()`, `Body()`, etc.) esos ejemplos no se añadían al JSON Schema que describe esos datos (ni siquiera a la propia versión de JSON Schema de OpenAPI), se añadían directamente a la declaración de la *path operation* en OpenAPI (fuera de las partes de OpenAPI que usan JSON Schema). Pero ahora que FastAPI 0.99.0 y superiores usa OpenAPI 3.1.0, que usa JSON Schema 2020-12, y Swagger UI 5.0.0 y superiores, todo es más consistente y los ejemplos se incluyen en JSON Schema. ### Swagger UI y `examples` específicos de OpenAPI { #swagger-ui-and-openapi-specific-examples } -Ahora, como Swagger UI no soportaba múltiples ejemplos de JSON Schema (a fecha de 2023-08-26), los usuarios no tenían una forma de mostrar múltiples ejemplos en los documentos. +Ahora, como Swagger UI no soportaba múltiples ejemplos de JSON Schema (a fecha de 2023-08-26), los usuarios no tenían una forma de mostrar múltiples ejemplos en la documentación. Para resolver eso, FastAPI `0.103.0` **añadió soporte** para declarar el mismo viejo campo **específico de OpenAPI** `examples` con el nuevo parámetro `openapi_examples`. 🤓 From f03a1502a0a72d466cf4b08c900b886bdda146c8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:41:44 +0000 Subject: [PATCH 058/243] =?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 08bc2927c..5498039f9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Update translations for es (update-outdated). PR [#14686](https://github.com/fastapi/fastapi/pull/14686) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add LLM prompt file for Turkish, generated from the existing translations. PR [#14547](https://github.com/fastapi/fastapi/pull/14547) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add LLM prompt file for Traditional Chinese, generated from the existing translations. PR [#14550](https://github.com/fastapi/fastapi/pull/14550) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add LLM prompt file for Simplified Chinese, generated from the existing translations. PR [#14549](https://github.com/fastapi/fastapi/pull/14549) by [@tiangolo](https://github.com/tiangolo). From 49653aa295eedd1f0deeae6d61f4324c70d74e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 10 Jan 2026 16:03:50 -0800 Subject: [PATCH 059/243] =?UTF-8?q?=F0=9F=94=A8=20Refactor=20translation?= =?UTF-8?q?=20script=20to=20allow=20committing=20in=20place=20(#14687)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/translate.yml | 6 ++++ scripts/translate.py | 52 +++++++++++++++++++++------------ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index e6edc5aab..734a3782f 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -30,6 +30,11 @@ on: type: string required: false default: "" + commit_in_place: + description: Whether to commit changes directly instead of making a PR + type: boolean + required: false + default: false jobs: langs: @@ -109,3 +114,4 @@ jobs: LANGUAGE: ${{ matrix.lang }} EN_PATH: ${{ github.event.inputs.en_path }} COMMAND: ${{ matrix.command }} + COMMIT_IN_PLACE: ${{ github.event.inputs.commit_in_place }} diff --git a/scripts/translate.py b/scripts/translate.py index c7e6c7ea1..eba4ad6a2 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -360,6 +360,9 @@ def make_pr( command: Annotated[str | None, typer.Option(envvar="COMMAND")] = None, github_token: Annotated[str, typer.Option(envvar="GITHUB_TOKEN")], github_repository: Annotated[str, typer.Option(envvar="GITHUB_REPOSITORY")], + commit_in_place: Annotated[ + bool, typer.Option(envvar="COMMIT_IN_PLACE", show_default=True) + ] = False, ) -> None: print("Setting up GitHub Actions git user") repo = git.Repo(Path(__file__).absolute().parent.parent) @@ -371,14 +374,22 @@ def make_pr( ["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"], check=True, ) - branch_name = "translate" - if language: - branch_name += f"-{language}" - if command: - branch_name += f"-{command}" - branch_name += f"-{secrets.token_hex(4)}" - print(f"Creating a new branch {branch_name}") - subprocess.run(["git", "checkout", "-b", branch_name], check=True) + current_branch = repo.active_branch.name + if current_branch == "master" and commit_in_place: + print("Can't commit directly to master") + raise typer.Exit(code=1) + + if not commit_in_place: + branch_name = "translate" + if language: + branch_name += f"-{language}" + if command: + branch_name += f"-{command}" + branch_name += f"-{secrets.token_hex(4)}" + print(f"Creating a new branch {branch_name}") + subprocess.run(["git", "checkout", "-b", branch_name], check=True) + else: + print(f"Committing in place on branch {current_branch}") print("Adding updated files") git_path = Path("docs") subprocess.run(["git", "add", str(git_path)], check=True) @@ -391,17 +402,20 @@ def make_pr( subprocess.run(["git", "commit", "-m", message], check=True) print("Pushing branch") subprocess.run(["git", "push", "origin", branch_name], check=True) - print("Creating PR") - g = Github(github_token) - gh_repo = g.get_repo(github_repository) - body = ( - message - + "\n\nThis PR was created automatically using LLMs." - + f"\n\nIt uses the prompt file https://github.com/fastapi/fastapi/blob/master/docs/{language}/llm-prompt.md." - + "\n\nIn most cases, it's better to make PRs updating that file so that the LLM can do a better job generating the translations than suggesting changes in this PR." - ) - pr = gh_repo.create_pull(title=message, body=body, base="master", head=branch_name) - print(f"Created PR: {pr.number}") + if not commit_in_place: + print("Creating PR") + g = Github(github_token) + gh_repo = g.get_repo(github_repository) + body = ( + message + + "\n\nThis PR was created automatically using LLMs." + + f"\n\nIt uses the prompt file https://github.com/fastapi/fastapi/blob/master/docs/{language}/llm-prompt.md." + + "\n\nIn most cases, it's better to make PRs updating that file so that the LLM can do a better job generating the translations than suggesting changes in this PR." + ) + pr = gh_repo.create_pull( + title=message, body=body, base="master", head=branch_name + ) + print(f"Created PR: {pr.number}") print("Finished") From 154ce03ff0560fe43fa4f5c9715fcd6b82dbb8cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:04:10 +0000 Subject: [PATCH 060/243] =?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 5498039f9..a25c94b7f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Internal +* 🔨 Refactor translation script to allow committing in place. PR [#14687](https://github.com/fastapi/fastapi/pull/14687) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix translation script path. PR [#14685](https://github.com/fastapi/fastapi/pull/14685) by [@tiangolo](https://github.com/tiangolo). * ✅ Enable tests in CI for scripts. PR [#14684](https://github.com/fastapi/fastapi/pull/14684) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add pre-commit local script to fix language translations. PR [#14683](https://github.com/fastapi/fastapi/pull/14683) by [@tiangolo](https://github.com/tiangolo). From 6f977366a4154b762f257460b028f44e9686e5a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 10 Jan 2026 16:15:06 -0800 Subject: [PATCH 061/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20uk=20(update-outdated)=20(#14587)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Yurii Motov <yurii.motov.monte@gmail.com> --- docs/en/mkdocs.yml | 2 + docs/uk/docs/index.md | 378 +++++++++------ docs/uk/docs/learn/index.md | 4 +- docs/uk/docs/python-types.md | 433 +++++++++--------- docs/uk/docs/tutorial/background-tasks.md | 65 ++- docs/uk/docs/tutorial/body-fields.md | 45 +- docs/uk/docs/tutorial/body-multiple-params.md | 75 +-- docs/uk/docs/tutorial/body-nested-models.md | 94 ++-- docs/uk/docs/tutorial/body-updates.md | 50 +- docs/uk/docs/tutorial/body.md | 58 +-- docs/uk/docs/tutorial/cookie-param-models.md | 46 +- docs/uk/docs/tutorial/cookie-params.md | 27 +- docs/uk/docs/tutorial/cors.md | 37 +- docs/uk/docs/tutorial/debugging.md | 57 +-- docs/uk/docs/tutorial/encoder.md | 20 +- docs/uk/docs/tutorial/extra-data-types.md | 28 +- docs/uk/docs/tutorial/first-steps.md | 302 +++++++----- docs/uk/docs/tutorial/handling-errors.md | 153 +++---- docs/uk/docs/tutorial/header-param-models.md | 34 +- docs/uk/docs/tutorial/header-params.md | 48 +- docs/uk/docs/tutorial/index.md | 78 ++-- docs/uk/docs/tutorial/metadata.md | 64 +-- docs/uk/docs/tutorial/middleware.md | 84 ++-- .../path-params-numeric-validations.md | 97 ++-- docs/uk/docs/tutorial/path-params.md | 179 ++++---- docs/uk/docs/tutorial/query-param-models.md | 14 +- .../tutorial/query-params-str-validations.md | 219 ++++----- docs/uk/docs/tutorial/query-params.md | 85 ++-- docs/uk/docs/tutorial/request-files.md | 109 ++--- docs/uk/docs/tutorial/request-form-models.md | 34 +- .../docs/tutorial/request-forms-and-files.md | 18 +- docs/uk/docs/tutorial/request-forms.md | 24 +- docs/uk/docs/tutorial/response-model.md | 211 ++++----- docs/uk/docs/tutorial/response-status-code.md | 77 ++-- docs/uk/docs/tutorial/schema-extra-example.md | 130 +++--- docs/uk/docs/tutorial/security/index.md | 96 ++-- docs/uk/docs/tutorial/static-files.md | 24 +- docs/uk/docs/tutorial/testing.md | 123 ++--- scripts/docs.py | 1 + 39 files changed, 1833 insertions(+), 1790 deletions(-) diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 66094c81e..84255f0f8 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -321,6 +321,8 @@ extra: name: pt - português - link: /ru/ name: ru - русский язык + - link: /uk/ + name: uk - українська мова extra_css: - css/termynal.css - css/custom.css diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md index 0811a4c7b..526409c5c 100644 --- a/docs/uk/docs/index.md +++ b/docs/uk/docs/index.md @@ -1,8 +1,14 @@ +# FastAPI { #fastapi } + +<style> +.md-content .md-typeset h1 { display: none; } +</style> + <p align="center"> - <a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> + <a href="https://fastapi.tiangolo.com/uk"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> </p> <p align="center"> - <em>Готовий до продакшину, високопродуктивний, простий у вивченні та швидкий для написання коду фреймворк</em> + <em>Фреймворк FastAPI: висока продуктивність, легко вивчати, швидко писати код, готовий до продакшину</em> </p> <p align="center"> <a href="https://github.com/fastapi/fastapi/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank"> @@ -21,46 +27,51 @@ --- -**Документація**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a> +**Документація**: <a href="https://fastapi.tiangolo.com/uk" target="_blank">https://fastapi.tiangolo.com</a> -**Програмний код**: <a href="https://github.com/fastapi/fastapi" target="_blank">https://github.com/fastapi/fastapi</a> +**Вихідний код**: <a href="https://github.com/fastapi/fastapi" target="_blank">https://github.com/fastapi/fastapi</a> --- -FastAPI - це сучасний, швидкий (високопродуктивний), вебфреймворк для створення API за допомогою Python,в основі якого лежить стандартна анотація типів Python. +FastAPI — це сучасний, швидкий (високопродуктивний) вебфреймворк для створення API за допомогою Python, що базується на стандартних підказках типів Python. Ключові особливості: -* **Швидкий**: Дуже висока продуктивність, на рівні з **NodeJS** та **Go** (завдяки Starlette та Pydantic). [Один із найшвидших фреймворків](#performance). +* **Швидкий**: дуже висока продуктивність, на рівні з **NodeJS** та **Go** (завдяки Starlette та Pydantic). [Один із найшвидших Python-фреймворків](#performance). +* **Швидке написання коду**: пришвидшує розробку функціоналу приблизно на 200%–300%. * +* **Менше помилок**: зменшує приблизно на 40% кількість помилок, спричинених людиною (розробником). * +* **Інтуїтивний**: чудова підтримка редакторами коду. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Автодоповнення</abbr> всюди. Менше часу на налагодження. +* **Простий**: спроєктований так, щоб бути простим у використанні та вивченні. Менше часу на читання документації. +* **Короткий**: мінімізує дублювання коду. Кілька можливостей з кожного оголошення параметра. Менше помилок. +* **Надійний**: ви отримуєте код, готовий до продакшину. З автоматичною інтерактивною документацією. +* **Заснований на стандартах**: базується на (і повністю сумісний з) відкритими стандартами для API: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (раніше відомий як Swagger) та <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. -* **Швидке написання коду**: Пришвидшує розробку функціоналу приблизно на 200%-300%. * -* **Менше помилок**: Зменшить кількість помилок спричинених людиною (розробником) на 40%. * -* **Інтуїтивний**: Чудова підтримка редакторами коду. <abbr title="Також відоме як auto-complete, autocompletion, IntelliSense.">Доповнення</abbr> всюди. Зменште час на налагодження. -* **Простий**: Спроектований, для легкого використання та навчання. Знадобиться менше часу на читання документації. -* **Короткий**: Зведе до мінімуму дублювання коду. Кожен оголошений параметр може виконувати кілька функцій. -* **Надійний**: Ви матимете стабільний код готовий до продакшину з автоматичною інтерактивною документацією. -* **Стандартизований**: Оснований та повністю сумісний з відкритими стандартами для API: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (попередньо відомий як Swagger) та <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. +<small>* оцінка на основі тестів, проведених внутрішньою командою розробників, що створює продакшн-застосунки.</small> -<small>* оцінка на основі тестів внутрішньої команди розробників, створення продуктових застосунків.</small> - -## Спонсори +## Спонсори { #sponsors } <!-- sponsors --> -{% if sponsors %} +### Ключовий спонсор { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor -%} + +### Золоті та срібні спонсори { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> {% endfor -%} {%- for sponsor in sponsors.silver -%} <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> {% endfor %} -{% endif %} <!-- /sponsors --> -<a href="https://fastapi.tiangolo.com/fastapi-people/#sponsors" class="external-link" target="_blank">Other sponsors</a> +<a href="https://fastapi.tiangolo.com/uk/fastapi-people/#sponsors" class="external-link" target="_blank">Інші спонсори</a> -## Враження +## Враження { #opinions } "_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" @@ -88,7 +99,7 @@ FastAPI - це сучасний, швидкий (високопродуктив "_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" -<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://github.com/hugapi/hug" target="_blank">Hug</a> creator</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> +<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://github.com/hugapi/hug" target="_blank">Hug</a> creator</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> --- @@ -100,50 +111,54 @@ FastAPI - це сучасний, швидкий (високопродуктив --- -## **Typer**, FastAPI CLI +"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" + +<div style="text-align: right; margin-right: 10%;">Deon Pillsbury - <strong>Cisco</strong> <a href="https://www.linkedin.com/posts/deonpillsbury_cisco-cx-python-activity-6963242628536487936-trAp/" target="_blank"><small>(ref)</small></a></div> + +--- + +## Міні-документальний фільм про FastAPI { #fastapi-mini-documentary } + +Наприкінці 2025 року вийшов <a href="https://www.youtube.com/watch?v=mpR8ngthqiE" class="external-link" target="_blank">міні-документальний фільм про FastAPI</a>, ви можете переглянути його онлайн: + +<a href="https://www.youtube.com/watch?v=mpR8ngthqiE" target="_blank"><img src="https://fastapi.tiangolo.com/img/fastapi-documentary.jpg" alt="FastAPI Mini Documentary"></a> + +## **Typer**, FastAPI для CLI { #typer-the-fastapi-of-clis } <a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> -Створюючи <abbr title="Command Line Interface">CLI</abbr> застосунок для використання в терміналі, замість веб-API зверніть увагу на <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>. +Якщо ви створюєте застосунок <abbr title="Command Line Interface">CLI</abbr> для використання в терміналі замість веб-API, зверніть увагу на <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>. -**Typer** є молодшим братом FastAPI. І це **FastAPI для CLI**. ⌨️ 🚀 +**Typer** — молодший брат FastAPI. І його задумано як **FastAPI для CLI**. ⌨️ 🚀 -## Вимоги +## Вимоги { #requirements } FastAPI стоїть на плечах гігантів: -* <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a> для web частини. +* <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a> для вебчастини. * <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> для частини даних. -## Вставновлення +## Встановлення { #installation } + +Створіть і активуйте <a href="https://fastapi.tiangolo.com/uk/virtual-environments/" class="external-link" target="_blank">віртуальне середовище</a>, а потім встановіть FastAPI: <div class="termy"> ```console -$ pip install fastapi +$ pip install "fastapi[standard]" ---> 100% ``` </div> -Вам також знадобиться сервер ASGI для продакшину, наприклад <a href="https://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a> або <a href="https://github.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>. +**Примітка**: переконайтеся, що ви взяли `"fastapi[standard]"` у лапки, щоб це працювало в усіх терміналах. -<div class="termy"> +## Приклад { #example } -```console -$ pip install uvicorn[standard] +### Створіть { #create-it } ----> 100% -``` - -</div> - -## Приклад - -### Створіть - -* Створіть файл `main.py` з: +Створіть файл `main.py` з: ```Python from typing import Union @@ -188,22 +203,35 @@ async def read_item(item_id: int, q: Union[str, None] = None): **Примітка**: -Стикнувшись з проблемами, не зайвим буде ознайомитися з розділом _"In a hurry?"_ про <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` та `await` у документації</a>. +Якщо ви не знаєте, перегляньте розділ _"In a hurry?"_ про <a href="https://fastapi.tiangolo.com/uk/async/#in-a-hurry" target="_blank">`async` та `await` у документації</a>. </details> -### Запустіть +### Запустіть { #run-it } -Запустіть server з: +Запустіть сервер за допомогою: <div class="termy"> ```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. ``` @@ -211,21 +239,21 @@ INFO: Application startup complete. </div> <details markdown="1"> -<summary>Про команди <code>uvicorn main:app --reload</code>...</summary> +<summary>Про команду <code>fastapi dev main.py</code>...</summary> -Команда `uvicorn main:app` посилається на: +Команда `fastapi dev` читає ваш файл `main.py`, знаходить у ньому застосунок **FastAPI** і запускає сервер за допомогою <a href="https://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a>. -* `main`: файл `main.py` ("Модуль" Python). -* `app`: об’єкт створений усередині `main.py` рядком `app = FastAPI()`. -* `--reload`: перезапускає сервер після зміни коду. Використовуйте виключно для розробки. +За замовчуванням `fastapi dev` запускається з авто-перезавантаженням для локальної розробки. + +Докладніше читайте в <a href="https://fastapi.tiangolo.com/uk/fastapi-cli/" target="_blank">документації FastAPI CLI</a>. </details> -### Перевірте +### Перевірте { #check-it } -Відкрийте браузер та введіть адресу <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>. +Відкрийте браузер і перейдіть на <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>. -Ви побачите у відповідь подібний JSON: +Ви побачите JSON-відповідь: ```JSON {"item_id": 5, "q": "somequery"} @@ -233,32 +261,32 @@ INFO: Application startup complete. Ви вже створили API, який: -* Отримує HTTP запити за _шляхами_ `/` та `/items/{item_id}`. +* Отримує HTTP-запити за _шляхами_ `/` та `/items/{item_id}`. * Обидва _шляхи_ приймають `GET` <em>операції</em> (також відомі як HTTP _методи_). -* _Шлях_ `/items/{item_id}` містить _параметр шляху_ `item_id` який має бути типу `int`. +* _Шлях_ `/items/{item_id}` містить _параметр шляху_ `item_id`, який має бути типу `int`. * _Шлях_ `/items/{item_id}` містить необовʼязковий `str` _параметр запиту_ `q`. -### Інтерактивні документації API +### Інтерактивна документація API { #interactive-api-docs } -Перейдемо сюди <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. +Тепер перейдіть на <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. -Ви побачите автоматичну інтерактивну API документацію (створену завдяки <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>): +Ви побачите автоматичну інтерактивну документацію API (надану <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Альтернативні документації API +### Альтернативна документація API { #alternative-api-docs } -Тепер перейдемо сюди <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. +А тепер перейдіть на <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. -Ви побачите альтернативну автоматичну документацію (створену завдяки <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>): +Ви побачите альтернативну автоматичну документацію (надану <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Приклад оновлення +## Приклад оновлення { #example-upgrade } -Тепер модифікуйте файл `main.py`, щоб отримати вміст запиту `PUT`. +Тепер змініть файл `main.py`, щоб отримувати тіло `PUT`-запиту. -Оголошуйте вміст запиту за допомогою стандартних типів Python завдяки Pydantic. +Оголосіть тіло, використовуючи стандартні типи Python, завдяки Pydantic. ```Python hl_lines="4 9-12 25-27" from typing import Union @@ -290,41 +318,41 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -Сервер повинен автоматично перезавантажуватися (тому що Ви додали `--reload` до `uvicorn` команди вище). +Сервер `fastapi dev` має автоматично перезавантажитися. -### Оновлення інтерактивної API документації +### Оновлення інтерактивної документації API { #interactive-api-docs-upgrade } -Тепер перейдемо сюди <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. +Тепер перейдіть на <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. -* Інтерактивна документація API буде автоматично оновлена, включаючи новий вміст: +* Інтерактивна документація API буде автоматично оновлена, включно з новим тілом: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* Натисніть кнопку "Try it out", це дозволить вам заповнити параметри та безпосередньо взаємодіяти з API: +* Натисніть кнопку "Try it out", вона дозволяє заповнити параметри та безпосередньо взаємодіяти з API: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Потім натисніть кнопку "Execute", інтерфейс користувача зв'яжеться з вашим API, надішле параметри, у відповідь отримає результати та покаже їх на екрані: +* Потім натисніть кнопку "Execute", інтерфейс користувача зв'яжеться з вашим API, надішле параметри, отримає результати та покаже їх на екрані: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Оновлення альтернативної API документації +### Оновлення альтернативної документації API { #alternative-api-docs-upgrade } -Зараз перейдемо <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. +А тепер перейдіть на <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. -* Альтернативна документація також показуватиме новий параметр і вміст запиту: +* Альтернативна документація також відобразить новий параметр запиту та тіло: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Підсумки +### Підсумки { #recap } -Таким чином, Ви **один раз** оголошуєте типи параметрів, тіла тощо, як параметри функції. +Отже, ви оголошуєте **один раз** типи параметрів, тіла тощо як параметри функції. Ви робите це за допомогою стандартних сучасних типів Python. Вам не потрібно вивчати новий синтаксис, методи чи класи конкретної бібліотеки тощо. -Використовуючи стандартний **Python**. +Лише стандартний **Python**. Наприклад, для `int`: @@ -332,35 +360,35 @@ def update_item(item_id: int, item: Item): item_id: int ``` -або для більш складної моделі `Item`: +або для складнішої моделі `Item`: ```Python item: Item ``` -...і з цим єдиним оголошенням Ви отримуєте: +...і з цим єдиним оголошенням ви отримуєте: -* Підтримку редактора, включаючи: - * Варіанти заповнення. - * Перевірку типів. -* Перевірку даних: - * Автоматичні та зрозумілі помилки, у разі некоректних даних. - * Перевірка навіть для JSON з високим рівнем вкладеності. -* <abbr title="також відомий як: serialization, parsing, marshalling">Перетворення</abbr> вхідних даних: з мережі до даних і типів Python. Читання з: +* Підтримку редактора, включно з: + * Автодоповненням. + * Перевіркою типів. +* Валідацію даних: + * Автоматичні та зрозумілі помилки, коли дані некоректні. + * Валідацію навіть для глибоко вкладених JSON-обʼєктів. +* <abbr title="also known as: serialization, parsing, marshalling">Перетворення</abbr> вхідних даних: з мережі до даних і типів Python. Читання з: * JSON. * Параметрів шляху. * Параметрів запиту. * Cookies. * Headers. * Forms. - * Файлів. -* <abbr title="також відомий як: serialization, parsing, marshalling">Перетворення</abbr> вихідних даних: з типів і даних Python до мережевих даних (як JSON): - * Конвертація Python типів (`str`, `int`, `float`, `bool`, `list`, тощо). - * `datetime` об'єкти. - * `UUID` об'єкти. - * Моделі бази даних. + * Files. +* <abbr title="also known as: serialization, parsing, marshalling">Перетворення</abbr> вихідних даних: перетворення з даних і типів Python у мережеві дані (як JSON): + * Перетворення типів Python (`str`, `int`, `float`, `bool`, `list`, тощо). + * Обʼєктів `datetime`. + * Обʼєктів `UUID`. + * Моделей бази даних. * ...та багато іншого. -* Автоматичну інтерактивну документацію API, включаючи 2 альтернативні інтерфейси користувача: +* Автоматичну інтерактивну документацію API, включно з 2 альтернативними інтерфейсами користувача: * Swagger UI. * ReDoc. @@ -368,26 +396,26 @@ item: Item Повертаючись до попереднього прикладу коду, **FastAPI**: -* Підтвердить наявність `item_id` у шляху для запитів `GET` та `PUT`. -* Підтвердить, що `item_id` має тип `int` для запитів `GET` and `PUT`. +* Перевірить, що `item_id` є у шляху для `GET` та `PUT`-запитів. +* Перевірить, що `item_id` має тип `int` для `GET` та `PUT`-запитів. * Якщо це не так, клієнт побачить корисну, зрозумілу помилку. -* Перевірить, чи є необов'язковий параметр запиту з назвою `q` (а саме `http://127.0.0.1:8000/items/foo?q=somequery`) для запитів `GET`. +* Перевірить, чи є необов'язковий параметр запиту з назвою `q` (як у `http://127.0.0.1:8000/items/foo?q=somequery`) для `GET`-запитів. * Оскільки параметр `q` оголошено як `= None`, він необов'язковий. - * За відсутності `None` він був би обов'язковим (як і вміст у випадку з `PUT`). -* Для запитів `PUT` із `/items/{item_id}`, читає вміст як JSON: - * Перевірить, чи має обов'язковий атрибут `name` тип `str`. - * Перевірить, чи має обов'язковий атрибут `price` тип `float`. - * Перевірить, чи існує необов'язковий атрибут `is_offer` та чи має він тип `bool`. - * Усе це також працюватиме для глибоко вкладених об'єктів JSON. -* Автоматично конвертує із та в JSON. -* Документує все за допомогою OpenAPI, який може бути використано в: + * Без `None` він був би обов'язковим (як і тіло у випадку з `PUT`). +* Для `PUT`-запитів до `/items/{item_id}` прочитає тіло як JSON: + * Перевірить, що є обовʼязковий атрибут `name`, який має бути типу `str`. + * Перевірить, що є обовʼязковий атрибут `price`, який має бути типу `float`. + * Перевірить, що є необовʼязковий атрибут `is_offer`, який має бути типу `bool`, якщо він присутній. + * Усе це також працюватиме для глибоко вкладених JSON-обʼєктів. +* Автоматично перетворюватиме з та в JSON. +* Документуватиме все за допомогою OpenAPI, який може бути використано в: * Інтерактивних системах документації. * Системах автоматичної генерації клієнтського коду для багатьох мов. -* Надає безпосередньо 2 вебінтерфейси інтерактивної документації. +* Надаватиме безпосередньо 2 вебінтерфейси інтерактивної документації. --- -Ми лише трішки доторкнулися до коду, але Ви вже маєте уявлення про те, як все працює. +Ми лише трішки доторкнулися до поверхні, але ви вже маєте уявлення про те, як усе працює. Спробуйте змінити рядок: @@ -407,57 +435,131 @@ item: Item ... "item_price": item.price ... ``` -...і побачите, як ваш редактор автоматично заповнюватиме атрибути та знатиме їхні типи: +...і побачите, як ваш редактор автоматично доповнюватиме атрибути та знатиме їхні типи: ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -Для більш повного ознайомлення з додатковими функціями, перегляньте <a href="https://fastapi.tiangolo.com/tutorial/">Туторіал - Посібник Користувача</a>. +Для більш повного прикладу, що включає більше можливостей, перегляньте <a href="https://fastapi.tiangolo.com/uk/tutorial/">Туторіал — Посібник користувача</a>. -**Spoiler alert**: туторіал - посібник користувача містить: +**Spoiler alert**: туторіал — посібник користувача містить: -* Оголошення **параметрів** з інших місць як: **headers**, **cookies**, **form fields** та **files**. -* Як встановити **перевірку обмежень** як `maximum_length` або `regex`. -* Дуже потужна і проста у використанні система **<abbr title="також відома як: components, resources, providers, services, injectables">Ін'єкція Залежностей</abbr>**. -* Безпека та автентифікація, включаючи підтримку **OAuth2** з **JWT tokens** та **HTTP Basic** автентифікацію. +* Оголошення **параметрів** з інших різних місць, як-от: **headers**, **cookies**, **form fields** та **files**. +* Як встановлювати **обмеження валідації** як `maximum_length` або `regex`. +* Дуже потужну і просту у використанні систему **<abbr title="also known as components, resources, providers, services, injectables">Dependency Injection</abbr>**. +* Безпеку та автентифікацію, включно з підтримкою **OAuth2** з **JWT tokens** та **HTTP Basic** auth. * Досконаліші (але однаково прості) техніки для оголошення **глибоко вкладених моделей JSON** (завдяки Pydantic). -* Багато додаткових функцій (завдяки Starlette) як-от: +* Інтеграцію **GraphQL** з <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a> та іншими бібліотеками. +* Багато додаткових можливостей (завдяки Starlette) як-от: * **WebSockets** * надзвичайно прості тести на основі HTTPX та `pytest` * **CORS** * **Cookie Sessions** * ...та більше. -## Продуктивність +### Розгортання застосунку (необовʼязково) { #deploy-your-app-optional } -Незалежні тести TechEmpower показують що застосунки **FastAPI**, які працюють під керуванням Uvicorn <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">є одними з найшвидших серед доступних фреймворків в Python</a>, поступаючись лише Starlette та Uvicorn (які внутрішньо використовуються в FastAPI). (*) +За бажання ви можете розгорнути ваш застосунок FastAPI у <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, перейдіть і приєднайтеся до списку очікування, якщо ви ще цього не зробили. 🚀 -Щоб дізнатися більше про це, перегляньте розділ <a href="https://fastapi.tiangolo.com/benchmarks/" class="internal-link" target="_blank">Benchmarks</a>. +Якщо у вас вже є обліковий запис **FastAPI Cloud** (ми запросили вас зі списку очікування 😉), ви можете розгорнути ваш застосунок однією командою. -## Необов'язкові залежності +Перед розгортанням переконайтеся, що ви ввійшли в систему: -Pydantic використовує: +<div class="termy"> + +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +</div> + +Потім розгорніть ваш застосунок: + +<div class="termy"> + +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +</div> + +Ось і все! Тепер ви можете отримати доступ до вашого застосунку за цією URL-адресою. ✨ + +#### Про FastAPI Cloud { #about-fastapi-cloud } + +**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** створено тим самим автором і командою, що стоять за **FastAPI**. + +Він спрощує процес **створення**, **розгортання** та **доступу** до API з мінімальними зусиллями. + +Він забезпечує той самий **developer experience** створення застосунків на FastAPI під час їх **розгортання** у хмарі. 🎉 + +FastAPI Cloud — основний спонсор і джерело фінансування open source проєктів *FastAPI and friends*. ✨ + +#### Розгортання в інших хмарних провайдерів { #deploy-to-other-cloud-providers } + +FastAPI — open source і базується на стандартах. Ви можете розгортати застосунки FastAPI в будь-якому хмарному провайдері, який ви оберете. + +Дотримуйтеся інструкцій вашого хмарного провайдера, щоб розгорнути застосунки FastAPI у нього. 🤓 + +## Продуктивність { #performance } + +Незалежні тести TechEmpower показують застосунки **FastAPI**, які працюють під керуванням Uvicorn, як <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">одні з найшвидших доступних Python-фреймворків</a>, поступаючись лише Starlette та Uvicorn (які внутрішньо використовуються в FastAPI). (*) + +Щоб дізнатися більше, перегляньте розділ <a href="https://fastapi.tiangolo.com/uk/benchmarks/" class="internal-link" target="_blank">Benchmarks</a>. + +## Залежності { #dependencies } + +FastAPI залежить від Pydantic і Starlette. + +### Залежності `standard` { #standard-dependencies } + +Коли ви встановлюєте FastAPI за допомогою `pip install "fastapi[standard]"`, ви отримуєте групу необовʼязкових залежностей `standard`: + +Використовується Pydantic: * <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email-validator</code></a> - для валідації електронної пошти. -* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - для управління налаштуваннями. + +Використовується Starlette: + +* <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - потрібно, якщо ви хочете використовувати `TestClient`. +* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - потрібно, якщо ви хочете використовувати конфігурацію шаблонів за замовчуванням. +* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - потрібно, якщо ви хочете підтримувати <abbr title="converting the string that comes from an HTTP request into Python data">«parsing»</abbr> форм за допомогою `request.form()`. + +Використовується FastAPI: + +* <a href="https://www.uvicorn.dev" target="_blank"><code>uvicorn</code></a> - для сервера, який завантажує та обслуговує ваш застосунок. Це включає `uvicorn[standard]`, до якого входять деякі залежності (наприклад, `uvloop`), потрібні для високопродуктивної роботи сервера. +* `fastapi-cli[standard]` - щоб надати команду `fastapi`. + * Це включає `fastapi-cloud-cli`, який дозволяє розгортати ваш застосунок FastAPI у <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>. + +### Без залежностей `standard` { #without-standard-dependencies } + +Якщо ви не хочете включати необовʼязкові залежності `standard`, ви можете встановити через `pip install fastapi` замість `pip install "fastapi[standard]"`. + +### Без `fastapi-cloud-cli` { #without-fastapi-cloud-cli } + +Якщо ви хочете встановити FastAPI зі стандартними залежностями, але без `fastapi-cloud-cli`, ви можете встановити через `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. + +### Додаткові необовʼязкові залежності { #additional-optional-dependencies } + +Є ще деякі додаткові залежності, які ви можете захотіти встановити. + +Додаткові необовʼязкові залежності Pydantic: + +* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - для керування налаштуваннями. * <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - для додаткових типів, що можуть бути використані з Pydantic. +Додаткові необовʼязкові залежності FastAPI: -Starlette використовує: +* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - потрібно, якщо ви хочете використовувати `ORJSONResponse`. +* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - потрібно, якщо ви хочете використовувати `UJSONResponse`. -* <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - Необхідно, якщо Ви хочете використовувати `TestClient`. -* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - Необхідно, якщо Ви хочете використовувати шаблони як конфігурацію за замовчуванням. -* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - Необхідно, якщо Ви хочете підтримувати <abbr title="перетворення рядка, який надходить із запиту HTTP, на дані Python">"розбір"</abbr> форми за допомогою `request.form()`. -* <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - Необхідно для підтримки `SessionMiddleware`. -* <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - Необхідно для підтримки Starlette `SchemaGenerator` (ймовірно, вам це не потрібно з FastAPI). - -FastAPI / Starlette використовують: - -* <a href="https://www.uvicorn.dev" target="_blank"><code>uvicorn</code></a> - для сервера, який завантажує та обслуговує вашу програму. -* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - Необхідно, якщо Ви хочете використовувати `ORJSONResponse`. -* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - Необхідно, якщо Ви хочете використовувати `UJSONResponse`. - -Ви можете встановити все це за допомогою `pip install fastapi[all]`. - -## Ліцензія +## Ліцензія { #license } Цей проєкт ліцензовано згідно з умовами ліцензії MIT. diff --git a/docs/uk/docs/learn/index.md b/docs/uk/docs/learn/index.md index 7f9f21e57..6e28d414a 100644 --- a/docs/uk/docs/learn/index.md +++ b/docs/uk/docs/learn/index.md @@ -1,5 +1,5 @@ -# Навчання +# Навчання { #learn } -У цьому розділі надані вступні та навчальні матеріали для вивчення FastAPI. +У цьому розділі надані вступні розділи та навчальні матеріали для вивчення **FastAPI**. Це можна розглядати як **книгу**, **курс**, або **офіційний** та рекомендований спосіб освоїти FastAPI. 😎 diff --git a/docs/uk/docs/python-types.md b/docs/uk/docs/python-types.md index 676bafb15..a82d13a28 100644 --- a/docs/uk/docs/python-types.md +++ b/docs/uk/docs/python-types.md @@ -1,29 +1,28 @@ -# Вступ до типів Python +# Вступ до типів Python { #python-types-intro } -Python підтримує додаткові "підказки типу" ("type hints") (також звані "анотаціями типу" ("type annotations")). +Python підтримує додаткові «підказки типів» (також звані «анотаціями типів»). -Ці **"type hints"** є спеціальним синтаксисом, що дозволяє оголошувати <abbr title="наприклад: str, int, float, bool">тип</abbr> змінної. +Ці **«підказки типів»** або анотації — це спеціальний синтаксис, що дозволяє оголошувати <abbr title="наприклад: str, int, float, bool">тип</abbr> змінної. -За допомогою оголошення типів для ваших змінних, редактори та інструменти можуть надати вам кращу підтримку. +За допомогою оголошення типів для ваших змінних редактори та інструменти можуть надати вам кращу підтримку. -Це просто **швидкий посібник / нагадування** про анотації типів у Python. Він покриває лише мінімум, необхідний щоб використовувати їх з **FastAPI**... що насправді дуже мало. +Це лише **швидкий туторіал / нагадування** про підказки типів у Python. Він покриває лише мінімум, необхідний щоб використовувати їх з **FastAPI**... що насправді дуже мало. -**FastAPI** повністю базується на цих анотаціях типів, вони дають йому багато переваг. +**FastAPI** повністю базується на цих підказках типів, вони дають йому багато переваг і користі. Але навіть якщо ви ніколи не використаєте **FastAPI**, вам буде корисно дізнатись трохи про них. -/// note +/// note | Примітка -Якщо ви експерт у Python і ви вже знаєте усе про анотації типів - перейдіть до наступного розділу. +Якщо ви експерт у Python і ви вже знаєте все про підказки типів, перейдіть до наступного розділу. /// -## Мотивація +## Мотивація { #motivation } Давайте почнемо з простого прикладу: -{* ../../docs_src/python_types/tutorial001.py *} - +{* ../../docs_src/python_types/tutorial001_py39.py *} Виклик цієї програми виводить: @@ -34,13 +33,12 @@ John Doe Функція виконує наступне: * Бере `first_name` та `last_name`. -* Конвертує кожну літеру кожного слова у верхній регістр за допомогою `title()`. -* <abbr title="З’єднує їх, як одне ціле. З вмістом один за одним.">Конкатенує</abbr> їх разом із пробілом по середині. +* Перетворює першу літеру кожного з них у верхній регістр за допомогою `title()`. +* <abbr title="Об’єднує їх разом, як одне ціле. З вмістом один за одним.">Конкатенує</abbr> їх разом із пробілом по середині. -{* ../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} - -### Редагуйте це +### Редагуйте це { #edit-it } Це дуже проста програма. @@ -48,11 +46,11 @@ John Doe У певний момент ви розпочали б визначення функції, у вас були б готові параметри... -Але тоді вам потрібно викликати "той метод, який переводить першу літеру у верхній регістр". +Але тоді вам потрібно викликати «той метод, який перетворює першу літеру у верхній регістр». Це буде `upper`? Чи `uppercase`? `first_uppercase`? `capitalize`? -Тоді ви спробуєте давнього друга програміста - автозаповнення редактора коду. +Тоді ви спробуєте давнього друга програміста — автозаповнення редактора коду. Ви надрукуєте перший параметр функції, `first_name`, тоді крапку (`.`), а тоді натиснете `Ctrl+Space`, щоб запустити автозаповнення. @@ -60,7 +58,7 @@ John Doe <img src="/img/python-types/image01.png"> -### Додайте типи +### Додайте типи { #add-types } Давайте змінимо один рядок з попередньої версії. @@ -78,10 +76,9 @@ John Doe Ось і все. -Це "type hints": - -{* ../../docs_src/python_types/tutorial002.py hl[1] *} +Це «підказки типів»: +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} Це не те саме, що оголошення значень за замовчуванням, як це було б з: @@ -91,43 +88,41 @@ John Doe Це зовсім інше. -Ми використовуємо двокрапку (`:`), не дорівнює (`=`). +Ми використовуємо двокрапку (`:`), не знак дорівнює (`=`). -І додавання анотації типу зазвичай не змінює того, що сталось би без них. +І додавання підказок типів зазвичай не змінює того, що відбувається, порівняно з тим, що відбувалося б без них. -Але тепер, уявіть що ви посеред процесу створення функції, але з анотаціями типів. +Але тепер уявіть, що ви знову посеред процесу створення функції, але з підказками типів. -В цей же момент, ви спробуєте викликати автозаповнення з допомогою `Ctrl+Space` і побачите: +У той самий момент ви спробуєте викликати автозаповнення за допомогою `Ctrl+Space` і побачите: <img src="/img/python-types/image02.png"> -Разом з цим, ви можете прокручувати, переглядати опції, допоки ви не знайдете одну, що звучить схоже: +Разом з цим ви можете прокручувати, переглядаючи опції, допоки не знайдете ту, що «щось вам підказує»: <img src="/img/python-types/image03.png"> -## Більше мотивації +## Більше мотивації { #more-motivation } -Перевірте цю функцію, вона вже має анотацію типу: - -{* ../../docs_src/python_types/tutorial003.py hl[1] *} +Перевірте цю функцію, вона вже має підказки типів: +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} Оскільки редактор знає типи змінних, ви не тільки отримаєте автозаповнення, ви також отримаєте перевірку помилок: <img src="/img/python-types/image04.png"> -Тепер ви знаєте, щоб виправити це, вам потрібно перетворити `age` у строку з допомогою `str(age)`: +Тепер ви знаєте, щоб виправити це, вам потрібно перетворити `age` у рядок за допомогою `str(age)`: -{* ../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} +## Оголошення типів { #declaring-types } -## Оголошення типів - -Щойно ви побачили основне місце для оголошення анотацій типу. Як параметри функції. +Щойно ви побачили основне місце для оголошення підказок типів. Як параметри функції. Це також основне місце, де ви б їх використовували у **FastAPI**. -### Прості типи +### Прості типи { #simple-types } Ви можете оголошувати усі стандартні типи у Python, не тільки `str`. @@ -138,78 +133,51 @@ John Doe * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} - -### Generic-типи з параметрами типів +### Generic-типи з параметрами типів { #generic-types-with-type-parameters } Існують деякі структури даних, які можуть містити інші значення, наприклад `dict`, `list`, `set` та `tuple`. І внутрішні значення також можуть мати свій тип. -Ці типи, які мають внутрішні типи, називаються "**generic**" типами. І оголосити їх можна навіть із внутрішніми типами. +Ці типи, які мають внутрішні типи, називаються «**generic**» типами. І оголосити їх можна навіть із внутрішніми типами. -Щоб оголосити ці типи та внутрішні типи, ви можете використовувати стандартний модуль Python `typing`. Він існує спеціально для підтримки анотацій типів. +Щоб оголосити ці типи та внутрішні типи, ви можете використовувати стандартний модуль Python `typing`. Він існує спеціально для підтримки цих підказок типів. -#### Новіші версії Python +#### Новіші версії Python { #newer-versions-of-python } Синтаксис із використанням `typing` **сумісний** з усіма версіями, від Python 3.6 до останніх, включаючи Python 3.9, Python 3.10 тощо. -У міру розвитку Python **новіші версії** мають покращену підтримку анотацій типів і в багатьох випадках вам навіть не потрібно буде імпортувати та використовувати модуль `typing` для оголошення анотацій типу. +У міру розвитку Python **новіші версії** мають покращену підтримку цих анотацій типів і в багатьох випадках вам навіть не потрібно буде імпортувати та використовувати модуль `typing` для оголошення анотацій типів. -Якщо ви можете вибрати новішу версію Python для свого проекту, ви зможете скористатися цією додатковою простотою. Дивіться кілька прикладів нижче. +Якщо ви можете вибрати новішу версію Python для свого проекту, ви зможете скористатися цією додатковою простотою. -#### List (список) +У всій документації є приклади, сумісні з кожною версією Python (коли є різниця). + +Наприклад, «**Python 3.6+**» означає, що це сумісно з Python 3.6 або вище (включно з 3.7, 3.8, 3.9, 3.10 тощо). А «**Python 3.9+**» означає, що це сумісно з Python 3.9 або вище (включаючи 3.10 тощо). + +Якщо ви можете використовувати **останні версії Python**, використовуйте приклади для останньої версії — вони матимуть **найкращий і найпростіший синтаксис**, наприклад, «**Python 3.10+**». + +#### List { #list } Наприклад, давайте визначимо змінну, яка буде `list` із `str`. -//// tab | Python 3.8 і вище +Оголосіть змінну з тим самим синтаксисом двокрапки (`:`). -З модуля `typing`, імпортуємо `List` (з великої літери `L`): - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006.py!} -``` - -Оголосимо змінну з тим самим синтаксисом двокрапки (`:`). - -Як тип вкажемо `List`, який ви імпортували з `typing`. +Як тип вкажіть `list`. Оскільки список є типом, який містить деякі внутрішні типи, ви поміщаєте їх у квадратні дужки: -```Python hl_lines="4" -{!> ../../docs_src/python_types/tutorial006.py!} -``` +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} -//// +/// info | Інформація -//// tab | Python 3.9 і вище +Ці внутрішні типи в квадратних дужках називаються «параметрами типу». -Оголосимо змінну з тим самим синтаксисом двокрапки (`:`). - -Як тип вкажемо `list`. - -Оскільки список є типом, який містить деякі внутрішні типи, ви поміщаєте їх у квадратні дужки: - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial006_py39.py!} -``` - -//// - -/// info - -Ці внутрішні типи в квадратних дужках називаються "параметрами типу". - -У цьому випадку, `str` це параметр типу переданий у `List` (або `list` у Python 3.9 і вище). +У цьому випадку `str` — це параметр типу, переданий у `list`. /// -Це означає: "змінна `items` це `list`, і кожен з елементів у цьому списку - `str`". - -/// tip - -Якщо ви використовуєте Python 3.9 і вище, вам не потрібно імпортувати `List` з `typing`, ви можете використовувати натомість тип `list`. - -/// +Це означає: «змінна `items` — це `list`, і кожен з елементів у цьому списку — `str`». Зробивши це, ваш редактор може надати підтримку навіть під час обробки елементів зі списку: @@ -221,78 +189,42 @@ John Doe І все ж редактор знає, що це `str`, і надає підтримку для цього. -#### Tuple and Set (кортеж та набір) +#### Tuple and Set { #tuple-and-set } Ви повинні зробити те ж саме, щоб оголосити `tuple` і `set`: -//// tab | Python 3.8 і вище - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial007.py!} -``` - -//// - -//// tab | Python 3.9 і вище - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial007_py39.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} Це означає: -* Змінна `items_t` це `tuple` з 3 елементами, `int`, ще `int`, та `str`. -* Змінна `items_s` це `set`, і кожен його елемент типу `bytes`. +* Змінна `items_t` — це `tuple` з 3 елементами: `int`, ще `int`, та `str`. +* Змінна `items_s` — це `set`, і кожен його елемент має тип `bytes`. -#### Dict (словник) +#### Dict { #dict } Щоб оголосити `dict`, вам потрібно передати 2 параметри типу, розділені комами. -Перший параметр типу для ключа у `dict`. +Перший параметр типу для ключів у `dict`. -Другий параметр типу для значення у `dict`: +Другий параметр типу для значень у `dict`: -//// tab | Python 3.8 і вище - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008.py!} -``` - -//// - -//// tab | Python 3.9 і вище - -```Python hl_lines="1" -{!> ../../docs_src/python_types/tutorial008_py39.py!} -``` - -//// +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} Це означає: -* Змінна `prices` це `dict`: - * Ключі цього `dict` типу `str` (наприклад, назва кожного елементу). - * Значення цього `dict` типу `float` (наприклад, ціна кожного елементу). +* Змінна `prices` — це `dict`: + * Ключі цього `dict` мають тип `str` (скажімо, назва кожного елементу). + * Значення цього `dict` мають тип `float` (скажімо, ціна кожного елементу). -#### Union (об'єднання) +#### Union { #union } -Ви можете оголосити, що змінна може бути будь-яким із **кількох типів**, наприклад, `int` або `str`. +Ви можете оголосити, що змінна може бути будь-яким із **кількох типів**, наприклад `int` або `str`. У Python 3.6 і вище (включаючи Python 3.10) ви можете використовувати тип `Union` з `typing` і вставляти в квадратні дужки можливі типи, які можна прийняти. -У Python 3.10 також є **альтернативний синтаксис**, у якому ви можете розділити можливі типи за допомогою <abbr title='також називають «побітовим "або" оператором», але це значення тут не актуальне'>вертикальної смуги (`|`)</abbr>. +У Python 3.10 також є **новий синтаксис**, у якому ви можете розділити можливі типи за допомогою <abbr title='також називають «побітовим "або" оператором», але це значення тут не актуальне'>вертикальної смуги (`|`)</abbr>. -//// tab | Python 3.8 і вище - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial008b.py!} -``` - -//// - -//// tab | Python 3.10 і вище +//// tab | Python 3.10+ ```Python hl_lines="1" {!> ../../docs_src/python_types/tutorial008b_py310.py!} @@ -300,16 +232,24 @@ John Doe //// +//// tab | Python 3.9+ + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial008b_py39.py!} +``` + +//// + В обох випадках це означає, що `item` може бути `int` або `str`. -#### Possibly `None` (Optional) +#### Можливо `None` { #possibly-none } Ви можете оголосити, що значення може мати тип, наприклад `str`, але також може бути `None`. У Python 3.6 і вище (включаючи Python 3.10) ви можете оголосити його, імпортувавши та використовуючи `Optional` з модуля `typing`. ```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} +{!../../docs_src/python_types/tutorial009_py39.py!} ``` Використання `Optional[str]` замість просто `str` дозволить редактору допомогти вам виявити помилки, коли ви могли б вважати, що значенням завжди є `str`, хоча насправді воно також може бути `None`. @@ -318,23 +258,7 @@ John Doe Це також означає, що в Python 3.10 ви можете використовувати `Something | None`: -//// tab | Python 3.8 і вище - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009.py!} -``` - -//// - -//// tab | Python 3.8 і вище - альтернатива - -```Python hl_lines="1 4" -{!> ../../docs_src/python_types/tutorial009b.py!} -``` - -//// - -//// tab | Python 3.10 і вище +//// tab | Python 3.10+ ```Python hl_lines="1" {!> ../../docs_src/python_types/tutorial009_py310.py!} @@ -342,32 +266,90 @@ John Doe //// -#### Generic типи +//// tab | Python 3.9+ + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009_py39.py!} +``` + +//// + +//// tab | Python 3.9+ alternative + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009b_py39.py!} +``` + +//// + +#### Використання `Union` або `Optional` { #using-union-or-optional } + +Якщо ви використовуєте версію Python нижче 3.10, ось порада з моєї дуже **суб’єктивної** точки зору: + +* 🚨 Уникайте використання `Optional[SomeType]` +* Натомість ✨ **використовуйте `Union[SomeType, None]`** ✨. + +Обидва варіанти еквівалентні й «під капотом» це одне й те саме, але я рекомендую `Union` замість `Optional`, тому що слово «**optional**» може створювати враження, ніби значення є необов’язковим, хоча насправді це означає «воно може бути `None`», навіть якщо воно не є необов’язковим і все одно є обов’язковим. + +Я вважаю, що `Union[SomeType, None]` більш явно показує, що саме мається на увазі. + +Це лише про слова й назви. Але ці слова можуть впливати на те, як ви та ваша команда думаєте про код. + +Як приклад, розгляньмо цю функцію: + +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} + +Параметр `name` визначено як `Optional[str]`, але він **не є необов’язковим**, ви не можете викликати функцію без параметра: + +```Python +say_hi() # Ой, ні, це викликає помилку! 😱 +``` + +Параметр `name` **все ще є обов’язковим** (не *optional*), тому що він не має значення за замовчуванням. Водночас `name` приймає `None` як значення: + +```Python +say_hi(name=None) # Це працює, None є валідним 🎉 +``` + +Добра новина: щойно ви перейдете на Python 3.10, вам не доведеться про це хвилюватися, адже ви зможете просто використовувати `|` для визначення об’єднань типів: + +{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *} + +І тоді вам не доведеться хвилюватися про назви на кшталт `Optional` і `Union`. 😎 + +#### Generic типи { #generic-types } Ці типи, які приймають параметри типу у квадратних дужках, називаються **Generic types** or **Generics**, наприклад: -//// tab | Python 3.8 і вище +//// tab | Python 3.10+ -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Union` -* `Optional` -* ...та інші. - -//// - -//// tab | Python 3.9 і вище - -Ви можете використовувати ті самі вбудовані типи, як generic (з квадратними дужками та типами всередині): +Ви можете використовувати ті самі вбудовані типи як generic (з квадратними дужками та типами всередині): * `list` * `tuple` * `set` * `dict` -І те саме, що й у Python 3.8, із модуля `typing`: +І так само, як і в попередніх версіях Python, з модуля `typing`: + +* `Union` +* `Optional` +* ...та інші. + +У Python 3.10 як альтернативу використанню generic `Union` та `Optional` ви можете використовувати <abbr title='також називають «побітовим "або" оператором», але це значення тут не актуальне'>вертикальну смугу (`|`)</abbr> для оголошення об’єднань типів — це значно краще й простіше. + +//// + +//// tab | Python 3.9+ + +Ви можете використовувати ті самі вбудовані типи як generic (з квадратними дужками та типами всередині): + +* `list` +* `tuple` +* `set` +* `dict` + +І generic з модуля `typing`: * `Union` * `Optional` @@ -375,46 +357,29 @@ John Doe //// -//// tab | Python 3.10 і вище - -Ви можете використовувати ті самі вбудовані типи, як generic (з квадратними дужками та типами всередині): - -* `list` -* `tuple` -* `set` -* `dict` - -І те саме, що й у Python 3.8, із модуля `typing`: - -* `Union` -* `Optional` (так само як у Python 3.8) -* ...та інші. - -У Python 3.10, як альтернатива використанню `Union` та `Optional`, ви можете використовувати <abbr title='також називають «побітовим "або" оператором», але це значення тут не актуальне'>вертикальну смугу (`|`)</abbr> щоб оголосити об'єднання типів. - -//// - -### Класи як типи +### Класи як типи { #classes-as-types } Ви також можете оголосити клас як тип змінної. Скажімо, у вас є клас `Person` з імʼям: -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} - +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} Потім ви можете оголосити змінну типу `Person`: -{* ../../docs_src/python_types/tutorial010.py hl[6] *} - +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} І знову ж таки, ви отримуєте всю підтримку редактора: <img src="/img/python-types/image06.png"> -## Pydantic моделі +Зверніть увагу, що це означає: «`one_person` — це **екземпляр** класу `Person`». -<a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> це бібліотека Python для валідації даних. +Це не означає: «`one_person` — це **клас** з назвою `Person`». + +## Pydantic моделі { #pydantic-models } + +<a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> — це бібліотека Python для валідації даних. Ви оголошуєте «форму» даних як класи з атрибутами. @@ -424,33 +389,11 @@ John Doe І ви отримуєте всю підтримку редактора з цим отриманим об’єктом. -Приклад з документації Pydantic: +Приклад з офіційної документації Pydantic: -//// tab | Python 3.8 і вище +{* ../../docs_src/python_types/tutorial011_py310.py *} -```Python -{!> ../../docs_src/python_types/tutorial011.py!} -``` - -//// - -//// tab | Python 3.9 і вище - -```Python -{!> ../../docs_src/python_types/tutorial011_py39.py!} -``` - -//// - -//// tab | Python 3.10 і вище - -```Python -{!> ../../docs_src/python_types/tutorial011_py310.py!} -``` - -//// - -/// info +/// info | Інформація Щоб дізнатись більше про <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic, перегляньте його документацію</a>. @@ -460,11 +403,43 @@ John Doe Ви побачите набагато більше цього всього на практиці в [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. -## Анотації типів у **FastAPI** +/// tip | Порада -**FastAPI** використовує ці підказки для виконання кількох речей. +Pydantic має спеціальну поведінку, коли ви використовуєте `Optional` або `Union[Something, None]` без значення за замовчуванням; детальніше про це можна прочитати в документації Pydantic про <a href="https://docs.pydantic.dev/2.3/usage/models/#required-fields" class="external-link" target="_blank">Required Optional fields</a>. -З **FastAPI** ви оголошуєте параметри з підказками типу, і отримуєте: +/// + +## Підказки типів з анотаціями метаданих { #type-hints-with-metadata-annotations } + +У Python також є можливість додавати **додаткові <abbr title="Дані про дані, у цьому випадку — інформація про тип, наприклад опис.">метадані</abbr>** до цих підказок типів за допомогою `Annotated`. + +Починаючи з Python 3.9, `Annotated` є частиною стандартної бібліотеки, тож ви можете імпортувати його з `typing`. + +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} + +Сам Python нічого не робить із цим `Annotated`. А для редакторів та інших інструментів тип усе ще є `str`. + +Але ви можете використати це місце в `Annotated`, щоб надати **FastAPI** додаткові метадані про те, як ви хочете, щоб ваш застосунок поводився. + +Важливо пам’ятати, що **перший *параметр типу***, який ви передаєте в `Annotated`, — це **фактичний тип**. Решта — це лише метадані для інших інструментів. + +Наразі вам просто потрібно знати, що `Annotated` існує і що це стандартний Python. 😎 + +Пізніше ви побачите, наскільки **потужним** це може бути. + +/// tip | Порада + +Той факт, що це **стандартний Python**, означає, що ви й надалі отримуватимете **найкращий можливий досвід розробки** у вашому редакторі, з інструментами, які ви використовуєте для аналізу та рефакторингу коду тощо. ✨ + +А також те, що ваш код буде дуже сумісним із багатьма іншими інструментами та бібліотеками Python. 🚀 + +/// + +## Анотації типів у **FastAPI** { #type-hints-in-fastapi } + +**FastAPI** використовує ці підказки типів для виконання кількох речей. + +З **FastAPI** ви оголошуєте параметри з підказками типів, і отримуєте: * **Підтримку редактора**. * **Перевірку типів**. @@ -473,17 +448,17 @@ John Doe * **Визначення вимог**: з параметрів шляху запиту, параметрів запиту, заголовків, тіл, залежностей тощо. * **Перетворення даних**: із запиту в необхідний тип. -* **Перевірка даних**: що надходять від кожного запиту: +* **Перевірки даних**: що надходять від кожного запиту: * Генерування **автоматичних помилок**, що повертаються клієнту, коли дані недійсні. * **Документування** API за допомогою OpenAPI: * який потім використовується для автоматичної інтерактивної документації користувальницьких інтерфейсів. -Все це може здатися абстрактним. Не хвилюйтеся. Ви побачите все це в дії в [Туторіал - Посібник користувача](tutorial/index.md){.internal-link target=_blank}. +Все це може здатися абстрактним. Не хвилюйтеся. Ви побачите все це в дії в [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. Важливо те, що за допомогою стандартних типів Python в одному місці (замість того, щоб додавати більше класів, декораторів тощо), **FastAPI** зробить багато роботи за вас. -/// info +/// info | Інформація -Якщо ви вже пройшли весь навчальний посібник і повернулися, щоб дізнатися більше про типи, ось хороший ресурс <a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank">"шпаргалка" від `mypy`</a>. +Якщо ви вже пройшли весь туторіал і повернулися, щоб дізнатися більше про типи, ось хороший ресурс: <a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank">«шпаргалка» від `mypy`</a>. /// diff --git a/docs/uk/docs/tutorial/background-tasks.md b/docs/uk/docs/tutorial/background-tasks.md index 0a9349650..6d7804195 100644 --- a/docs/uk/docs/tutorial/background-tasks.md +++ b/docs/uk/docs/tutorial/background-tasks.md @@ -1,28 +1,27 @@ -# Фонові задачі +# Фонові задачі { #background-tasks } Ви можете створювати фонові задачі, які будуть виконуватися *після* повернення відповіді. Це корисно для операцій, які потрібно виконати після обробки запиту, але клієнту не обов’язково чекати завершення цієї операції перед отриманням відповіді. -Приклади використання: +Це включає, наприклад: * Надсилання email-сповіщень після виконання певної дії: - * Підключення до поштового сервера та надсилання листа може займати кілька секунд. Ви можете відразу повернути відповідь, а email відправити у фоні. + * Підключення до поштового сервера та надсилання листа може займати кілька секунд. Ви можете відразу повернути відповідь, а email-сповіщення надіслати у фоні. * Обробка даних: - * Наприклад, якщо отримано файл, який потрібно обробити довготривалим процесом, можна повернути відповідь "Accepted" ("Прийнято", HTTP 202) і виконати обробку файлу у фоні. + * Наприклад, якщо ви отримали файл, який потрібно обробити довготривалим процесом, можна повернути відповідь «Accepted» (HTTP 202) і виконати обробку файлу у фоні. -## Використання `BackgroundTasks` +## Використання `BackgroundTasks` { #using-backgroundtasks } -Спочатку імпортуйте `BackgroundTasks` і додайте його як параметр у Вашу *функцію операції шляху* (path operation function) до `BackgroundTasks`: +Спочатку імпортуйте `BackgroundTasks` і оголосіть параметр у вашій *функції операції шляху* з анотацією типу `BackgroundTasks`: -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} -**FastAPI** автоматично створить об'єкт `BackgroundTasks` і передасть його у цей параметр. +**FastAPI** створить для вас об’єкт типу `BackgroundTasks` і передасть його як цей параметр. +## Створення функції задачі { #create-a-task-function } -## Створення функції задачі - -Створіть функцію, яка буде виконувати фонову задачу. +Створіть функцію, яка буде виконуватися як фонова задача. Це звичайна функція, яка може отримувати параметри. @@ -32,54 +31,54 @@ І оскільки операція запису не використовує `async` та `await`, ми визначаємо функцію як звичайну `def`: -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} -## Додавання фонової задачі +## Додавання фонової задачі { #add-the-background-task } -Усередині Вашої *функції обробки шляху*, передайте функцію задачі в об'єкт *background tasks*, використовуючи метод `.add_task()`: +Усередині вашої *функції операції шляху*, передайте функцію задачі в об'єкт *background tasks*, використовуючи метод `.add_task()`: -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} `.add_task()` приймає аргументи: -* Функція задача, яка буде виконуватися у фоновому режимі (`write_notification`). Зверніть увагу, що передається обʼєкт без дужок. -* Будь-яка послідовність аргументів, які потрібно передати у функцію завдання у відповідному порядку (`email`). -* Будь-які іменовані аргументи, які потрібно передати у функцію задачу (`message="some notification"`). +* Функцію задачі, яка буде виконуватися у фоновому режимі (`write_notification`). +* Будь-яку послідовність аргументів, які потрібно передати у функцію задачі у відповідному порядку (`email`). +* Будь-які іменовані аргументи, які потрібно передати у функцію задачі (`message="some notification"`). -## Впровадження залежностей +## Впровадження залежностей { #dependency-injection } -Використання `BackgroundTasks` також працює з системою впровадження залежностей. Ви можете оголосити параметр типу `BackgroundTasks` на різних рівнях: у *функції операції шляху*, у залежності (dependable), у під залежності тощо. +Використання `BackgroundTasks` також працює з системою впровадження залежностей. Ви можете оголосити параметр типу `BackgroundTasks` на різних рівнях: у *функції операції шляху*, у залежності (dependable), у підзалежності тощо. -**FastAPI** знає, як діяти в кожному випадку і як повторно використовувати один і той самий об'єкт, щоб усі фонові задачі були об’єднані та виконувалися у фоновому режимі після завершення основного запиту. +**FastAPI** знає, як діяти в кожному випадку і як повторно використовувати один і той самий об'єкт, щоб усі фонові задачі були об’єднані та виконувалися у фоновому режимі після завершення основного запиту: {* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} У цьому прикладі повідомлення будуть записані у файл `log.txt` *після* того, як відповідь буде надіслана. -Якщо у запиті був переданий query-параметр, він буде записаний у лог у фоновій задачі. +Якщо у запиті був переданий query, він буде записаний у лог у фоновій задачі. -А потім інша фонова задача, яка створюється у *функції операції шляху*, запише повідомлення з використанням path параметра `email`. +А потім інша фонова задача, згенерована у *функції операції шляху*, запише повідомлення з використанням path параметра `email`. -## Технічні деталі +## Технічні деталі { #technical-details } Клас `BackgroundTasks` походить безпосередньо з <a href="https://www.starlette.dev/background/" class="external-link" target="_blank">`starlette.background`</a>. -Він імпортується безпосередньо у FastAPI, щоб Ви могли використовувати його з `fastapi` і випадково не імпортували `BackgroundTask` (без s в кінці) з `starlette.background`. +Він імпортується/включається безпосередньо у FastAPI, щоб ви могли імпортувати його з `fastapi` і випадково не імпортували альтернативний `BackgroundTask` (без `s` в кінці) з `starlette.background`. Якщо використовувати лише `BackgroundTasks` (а не `BackgroundTask`), то його можна передавати як параметр у *функції операції шляху*, і **FastAPI** подбає про все інше, так само як і про використання об'єкта `Request`. -Також можна використовувати `BackgroundTask` окремо в FastAPI, але для цього Вам доведеться створити об'єкт у коді та повернути Starlette `Response`, включаючи його. +Також можна використовувати `BackgroundTask` окремо в FastAPI, але для цього вам доведеться створити об'єкт у коді та повернути Starlette `Response`, включаючи його. -Детальніше можна почитати в <a href="https://www.starlette.dev/background/" class="external-link" target="_blank">офіційній документації Starlette про фонові задачі </a>. +Детальніше можна почитати в <a href="https://www.starlette.dev/background/" class="external-link" target="_blank">офіційній документації Starlette про Background Tasks</a>. -## Застереження +## Застереження { #caveat } -Якщо Вам потрібно виконувати складні фонові обчислення, і при цьому нема потреби запускати їх у тому ж процесі (наприклад, не потрібно спільного доступу до пам’яті чи змінних), можливо, варто скористатися більш потужними інструментами, такими як <a href="https://docs.celeryq.dev" class="external-link" target="_blank">Celery</a>. +Якщо вам потрібно виконувати складні фонові обчислення, і при цьому нема потреби запускати їх у тому ж процесі (наприклад, не потрібно спільного доступу до пам’яті чи змінних), можливо, варто скористатися більш потужними інструментами, такими як <a href="https://docs.celeryq.dev" class="external-link" target="_blank">Celery</a>. -Такі інструменти зазвичай потребують складнішої конфігурації та менеджера черги повідомлень/завдань, наприклад, RabbitMQ або Redis. Однак вони дозволяють виконувати фонові задачі в кількох процесах і навіть на кількох серверах. +Такі інструменти зазвичай потребують складнішої конфігурації та менеджера черги повідомлень/завдань, наприклад, RabbitMQ або Redis. Однак вони дозволяють виконувати фонові задачі в кількох процесах і особливо — на кількох серверах. -Якщо ж Вам потрібно отримати доступ до змінних і об’єктів із тієї ж **FastAPI** - програми або виконувати невеликі фонові завдання (наприклад, надсилати сповіщення електронною поштою), достатньо просто використовувати `BackgroundTasks`. +Якщо ж вам потрібно отримати доступ до змінних і об’єктів із тієї ж **FastAPI**-програми або виконувати невеликі фонові завдання (наприклад, надсилати email-сповіщення), достатньо просто використовувати `BackgroundTasks`. -## Підсумок +## Підсумок { #recap } -Імпортуйте та використовуйте `BackgroundTasks` як параметр у *функціях операції шляху* та залежностях, щоб додавати фонові задачі. +Імпортуйте та використовуйте `BackgroundTasks` як параметри у *функціях операції шляху* та залежностях, щоб додавати фонові задачі. diff --git a/docs/uk/docs/tutorial/body-fields.md b/docs/uk/docs/tutorial/body-fields.md index 7ddd9d104..70d94f3d6 100644 --- a/docs/uk/docs/tutorial/body-fields.md +++ b/docs/uk/docs/tutorial/body-fields.md @@ -1,60 +1,61 @@ -# Тіло - Поля +# Тіло — Поля { #body-fields } -Так само як ви можете визначати додаткову валідацію та метадані у параметрах *функції обробки шляху* за допомогою `Query`, `Path` та `Body`, ви можете визначати валідацію та метадані всередині моделей Pydantic за допомогою `Field` від Pydantic. +Так само як ви можете оголошувати додаткову валідацію та метадані в параметрах *функції операції шляху* за допомогою `Query`, `Path` та `Body`, ви можете оголошувати валідацію та метадані всередині моделей Pydantic, використовуючи `Field` від Pydantic. -## Імпорт `Field` +## Імпорт `Field` { #import-field } Спочатку вам потрібно імпортувати це: {* ../../docs_src/body_fields/tutorial001_an_py310.py hl[4] *} -/// warning -Зверніть увагу, що `Field` імпортується прямо з `pydantic`, а не з `fastapi`, як всі інші (`Query`, `Path`, `Body` тощо). +/// warning | Попередження + +Зверніть увагу, що `Field` імпортується безпосередньо з `pydantic`, а не з `fastapi`, як усе інше (`Query`, `Path`, `Body` тощо). /// -## Оголошення атрибутів моделі +## Оголошення атрибутів моделі { #declare-model-attributes } -Ви можете використовувати `Field` з атрибутами моделі: +Потім ви можете використовувати `Field` з атрибутами моделі: {* ../../docs_src/body_fields/tutorial001_an_py310.py hl[11:14] *} -`Field` працює так само, як `Query`, `Path` і `Body`, у нього такі самі параметри тощо. +`Field` працює так само, як `Query`, `Path` і `Body`, має ті самі параметри тощо. /// note | Технічні деталі -Насправді, `Query`, `Path` та інші, що ви побачите далі, створюють об'єкти підкласів загального класу `Param`, котрий сам є підкласом класу `FieldInfo` з Pydantic. +Насправді `Query`, `Path` та інші, які ви побачите далі, створюють об'єкти підкласів спільного класу `Param`, який сам є підкласом класу `FieldInfo` з Pydantic. І `Field` від Pydantic також повертає екземпляр `FieldInfo`. -`Body` також безпосередньо повертає об'єкти підкласу `FieldInfo`. І є інші підкласи, які ви побачите пізніше, що є підкласами класу Body. +`Body` також безпосередньо повертає об'єкти підкласу `FieldInfo`. І є інші, які ви побачите пізніше, що є підкласами класу `Body`. -Пам'ятайте, що коли ви імпортуєте 'Query', 'Path' та інше з 'fastapi', вони фактично є функціями, які повертають спеціальні класи. +Пам'ятайте, що коли ви імпортуєте `Query`, `Path` та інші з `fastapi`, це фактично функції, які повертають спеціальні класи. /// -/// tip +/// tip | Порада -Зверніть увагу, що кожен атрибут моделі із типом, значенням за замовчуванням та `Field` має ту саму структуру, що й параметр *функції обробки шляху*, з `Field` замість `Path`, `Query` і `Body`. +Зверніть увагу, що кожен атрибут моделі з типом, значенням за замовчуванням і `Field` має ту саму структуру, що й параметр *функції операції шляху*, з `Field` замість `Path`, `Query` і `Body`. /// -## Додавання додаткової інформації +## Додавання додаткової інформації { #add-extra-information } -Ви можете визначити додаткову інформацію у `Field`, `Query`, `Body` тощо. І вона буде включена у згенеровану JSON схему. +Ви можете оголошувати додаткову інформацію в `Field`, `Query`, `Body` тощо. І вона буде включена до згенерованої JSON Schema. -Ви дізнаєтеся більше про додавання додаткової інформації пізніше у документації, коли вивчатимете визначення прикладів. +Ви дізнаєтеся більше про додавання додаткової інформації пізніше в документації, коли вивчатимете, як оголошувати приклади. -/// warning +/// warning | Попередження -Додаткові ключі, передані в `Field`, також будуть присутні у згенерованій схемі OpenAPI для вашого додатка. -Оскільки ці ключі не обов'язково можуть бути частиною специфікації OpenAPI, деякі інструменти OpenAPI, наприклад, [OpenAPI валідатор](https://validator.swagger.io/), можуть не працювати з вашою згенерованою схемою. +Додаткові ключі, передані в `Field`, також будуть присутні в отриманій схемі OpenAPI для вашого застосунку. +Оскільки ці ключі не обов'язково є частиною специфікації OpenAPI, деякі інструменти OpenAPI, наприклад [валідатор OpenAPI](https://validator.swagger.io/), можуть не працювати з вашою згенерованою схемою. /// -## Підсумок +## Підсумок { #recap } -Ви можете використовувати `Field` з Pydantic для визначення додаткових перевірок та метаданих для атрибутів моделі. +Ви можете використовувати `Field` від Pydantic, щоб оголошувати додаткову валідацію та метадані для атрибутів моделі. -Ви також можете використовувати додаткові іменовані аргументи для передачі додаткових метаданих JSON схеми. +Ви також можете використовувати додаткові keyword arguments, щоб передавати додаткові метадані JSON Schema. diff --git a/docs/uk/docs/tutorial/body-multiple-params.md b/docs/uk/docs/tutorial/body-multiple-params.md index e2acf8a70..dc9a768c3 100644 --- a/docs/uk/docs/tutorial/body-multiple-params.md +++ b/docs/uk/docs/tutorial/body-multiple-params.md @@ -1,24 +1,24 @@ -# Тіло запиту - Декілька параметрів +# Тіло - Декілька параметрів { #body-multiple-parameters } -Тепер, коли ми розглянули використання `Path` та `Query`, розгляньмо більш просунуті способи оголошення тіла запиту в **FastAPI**. +Тепер, коли ми побачили, як використовувати `Path` і `Query`, розгляньмо більш просунуті варіанти оголошення тіла запиту. -## Змішування `Path`, `Query` та параметрів тіла запиту +## Змішування `Path`, `Query` та параметрів тіла { #mix-path-query-and-body-parameters } -По-перше, звісно, Ви можете вільно змішувати оголошення параметрів `Path`, `Query` та тіла запиту, і **FastAPI** правильно їх обробить. +По-перше, звісно, ви можете вільно змішувати оголошення параметрів `Path`, `Query` та тіла запиту, і **FastAPI** знатиме, що робити. -Також Ви можете оголосити параметри тіла як необов’язкові, встановивши для них значення за замовчуванням `None`: +Також ви можете оголошувати параметри тіла як необов’язкові, встановивши для них значення за замовчуванням `None`: {* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} /// note | Примітка -Зверніть увагу, що в цьому випадку параметр `item`, який береться з тіла запиту, є необов'язковим, оскільки має значення за замовчуванням `None`. +Зверніть увагу, що в цьому випадку параметр `item`, який береться з тіла, є необов'язковим. Оскільки має значення за замовчуванням `None`. /// -## Декілька параметрів тіла запиту +## Декілька параметрів тіла { #multiple-body-parameters } -У попередньому прикладі *операція шляху* очікувала JSON з атрибутами `Item`, наприклад: +У попередньому прикладі *операції шляху* очікували б JSON-тіло з атрибутами `Item`, наприклад: ```JSON { @@ -28,13 +28,15 @@ "tax": 3.2 } ``` -Але Ви також можете оголосити декілька параметрів тіла, наприклад `item` та `user`: + +Але ви також можете оголосити декілька параметрів тіла, наприклад `item` та `user`: {* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *} -У цьому випадку **FastAPI** розпізнає, що є кілька параметрів тіла (два параметри є моделями Pydantic). -Тому він використає назви параметрів як ключі (назви полів) у тілі запиту, очікуючи: +У цьому випадку **FastAPI** помітить, що у функції є більше ніж один параметр тіла (є два параметри, які є моделями Pydantic). + +Тож він використає назви параметрів як ключі (назви полів) у тілі та очікуватиме тіло такого вигляду: ```JSON { @@ -53,27 +55,28 @@ /// note | Примітка -Зверніть увагу, що хоча `item` оголошено, так само як і раніше, тепер він очікується в тілі під ключем `item`. +Зверніть увагу, що хоча `item` оголошено так само, як і раніше, тепер він очікується всередині тіла з ключем `item`. /// -**FastAPI** автоматично конвертує дані із запиту таким чином, щоб параметр `item` отримав свій вміст, і те ж саме стосується `user`. +**FastAPI** виконає автоматичне перетворення із запиту, щоб параметр `item` отримав свій конкретний вміст, і те ж саме для `user`. -Він виконає валідацію складених даних і задокументує їх відповідним чином у схемі OpenAPI та в автоматичній документації. +Він виконає валідацію складених даних і задокументує це таким чином у схемі OpenAPI та в автоматичній документації. -## Одиничні значення в тілі запиту +## Одиничні значення в тілі { #singular-values-in-body } Так само як є `Query` і `Path` для визначення додаткових даних для параметрів запиту та шляху, **FastAPI** надає еквівалентний `Body`. -Наприклад, розширюючи попередню модель, Ви можете вирішити додати ще один ключ `importance` в те ж саме тіло запиту разом із `item` і `user`. +Наприклад, розширивши попередню модель, ви можете вирішити додати ще один ключ `importance` у те саме тіло, окрім `item` і `user`. -Якщо Ви оголосите його як є, то, оскільки це одиничне значення, **FastAPI** припускатиме, що це параметр запиту (query parameter). +Якщо оголосити його як є, оскільки це одиничне значення, **FastAPI** припустить, що це параметр запиту. -Але Ви можете вказати **FastAPI** обробляти його як інший ключ тіла (body key), використовуючи `Body`: +Але ви можете вказати **FastAPI** обробляти його як інший ключ тіла, використовуючи `Body`: {* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} -У цьому випадку **FastAPI** очікуватиме тіло запиту у такому вигляді: + +У цьому випадку **FastAPI** очікуватиме тіло такого вигляду: ```JSON { @@ -90,19 +93,20 @@ "importance": 5 } ``` -Знову ж таки, **FastAPI** конвертуватиме типи даних, перевірятиме їх, створюватиме документацію тощо. -## Декілька body та query параметрів +Знову ж таки, він перетворюватиме типи даних, перевірятиме, документуватиме тощо. -Звісно, Ви можете оголошувати додаткові query параметри запиту, коли це необхідно, на додаток до будь-яких параметрів тіла запиту. +## Декілька параметрів тіла та query { #multiple-body-params-and-query } -Оскільки за замовчуванням окремі значення інтерпретуються як параметри запиту, Вам не потрібно явно додавати `Query`, можна просто використати: +Звісно, ви також можете оголошувати додаткові query параметри щоразу, коли це потрібно, додатково до будь-яких параметрів тіла. + +Оскільки за замовчуванням одиничні значення інтерпретуються як параметри запиту, вам не потрібно явно додавати `Query`, ви можете просто зробити: ```Python q: Union[str, None] = None ``` -Або в Python 3.10 та вище: +Або в Python 3.10 і вище: ```Python q: str | None = None @@ -115,17 +119,17 @@ q: str | None = None /// info | Інформація -`Body` також має ті самі додаткові параметри валідації та метаданих, що й `Query`, `Path` та інші, які Ви побачите пізніше. +`Body` також має всі ті самі додаткові параметри валідації та метаданих, що й `Query`, `Path` та інші, які ви побачите пізніше. /// -## Вкладений поодинокий параметр тіла запиту +## Вбудувати один параметр тіла { #embed-a-single-body-parameter } -Припустимо, у вас є лише один параметр тіла запиту `item` з моделі Pydantic `Item`. +Скажімо, у вас є лише один параметр тіла `item` з моделі Pydantic `Item`. -За замовчуванням **FastAPI** очікуватиме, що тіло запиту міститиме вміст безпосередньо. +За замовчуванням **FastAPI** очікуватиме його тіло безпосередньо. -Але якщо Ви хочете, щоб він очікував JSON з ключем `item`, а всередині — вміст моделі (так, як це відбувається при оголошенні додаткових параметрів тіла), Ви можете використати спеціальний параметр `Body` — `embed`: +Але якщо ви хочете, щоб він очікував JSON з ключем `item`, а всередині нього — вміст моделі, як це відбувається, коли ви оголошуєте додаткові параметри тіла, ви можете використати спеціальний параметр `Body` — `embed`: ```Python item: Item = Body(embed=True) @@ -135,7 +139,8 @@ item: Item = Body(embed=True) {* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} -У цьому випадку **FastAPI** очікуватиме тіло запиту такого вигляду: + +У цьому випадку **FastAPI** очікуватиме тіло такого вигляду: ```JSON hl_lines="2" { @@ -159,12 +164,12 @@ item: Item = Body(embed=True) } ``` -## Підсумок +## Підсумок { #recap } -Ви можете додавати кілька параметрів тіла до Вашої *функції операції шляху* (*path operation function*), навіть якщо запит може мати лише одне тіло. +Ви можете додавати кілька параметрів тіла до вашої *функції операції шляху*, навіть якщо запит може мати лише одне тіло. -Але **FastAPI** обробить це, надасть Вам потрібні дані у функції, перевірить їх та задокументує коректну схему в *операції шляху*. +Але **FastAPI** обробить це, надасть вам правильні дані у функції та перевірить і задокументує правильну схему в *операції шляху*. -Також Ви можете оголошувати окремі значення, які будуть отримані як частина тіла запиту. +Також ви можете оголошувати одиничні значення, щоб отримувати їх як частину тіла. -Крім того, Ви можете вказати **FastAPI** вбудовувати тіло в ключ, навіть якщо оголошено лише один параметр. +І ви можете вказати **FastAPI** вбудовувати тіло в ключ, навіть коли оголошено лише один параметр. diff --git a/docs/uk/docs/tutorial/body-nested-models.md b/docs/uk/docs/tutorial/body-nested-models.md index abc33f2eb..6d0669358 100644 --- a/docs/uk/docs/tutorial/body-nested-models.md +++ b/docs/uk/docs/tutorial/body-nested-models.md @@ -1,8 +1,8 @@ -# Тіло запиту - Вкладені моделі +# Тіло - Вкладені моделі { #body-nested-models } -З **FastAPI** Ви можете визначати, перевіряти, документувати та використовувати моделі, які можуть бути вкладені на будь-яку глибину (завдяки Pydantic). +З **FastAPI** ви можете визначати, перевіряти, документувати та використовувати моделі, які можуть бути вкладені на будь-яку глибину (завдяки Pydantic). -## Поля списку +## Поля списку { #list-fields } Ви можете визначити атрибут як підтип. Наприклад, Python-список (`list`): @@ -10,47 +10,28 @@ Це зробить `tags` списком, хоча не визначається тип елементів списку. -## Поля списку з параметром типу +## Поля списку з параметром типу { #list-fields-with-type-parameter } -Але Python має специфічний спосіб оголошення списків з внутрішніми типами або "параметрами типу": -### Імпортуємо `List` з модуля typing +Але Python має специфічний спосіб оголошення списків з внутрішніми типами або «параметрами типу»: -У Python 3.9 і вище можна використовувати стандартний `list` для оголошення таких типів, як ми побачимо нижче. 💡 +### Оголошення `list` з параметром типу { #declare-a-list-with-a-type-parameter } -Але в Python версії до 3.9 (від 3.6 і вище) спочатку потрібно імпортувати `List` з модуля стандартної бібліотеки Python `typing`: - -{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} - -### Оголошення `list` з параметром типу - -Щоб оголосити типи з параметрами типу (внутрішніми типами), такими як `list`, `dict`, `tuple`: - -* Якщо Ви використовуєте версію Python до 3.9, імпортуйте їх відповідну версію з модуля `typing`. -* Передайте внутрішні типи як "параметри типу", використовуючи квадратні дужки: `[` and `]`. - -У Python 3.9 це буде виглядати так: +Щоб оголосити типи з параметрами типу (внутрішніми типами), такими як `list`, `dict`, `tuple`, +передайте внутрішні тип(и) як «параметри типу», використовуючи квадратні дужки: `[` and `]` ```Python my_list: list[str] ``` -У версіях Python до 3.9 це виглядає так: - -```Python -from typing import List - -my_list: List[str] -``` - Це стандартний синтаксис Python для оголошення типів. Використовуйте той самий стандартний синтаксис для атрибутів моделей з внутрішніми типами. -Отже, у нашому прикладі, ми можемо зробити `tags` саме "списком рядків": +Отже, у нашому прикладі, ми можемо зробити `tags` саме «списком рядків»: {* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} -## Типи множин +## Типи множин { #set-types } Але потім ми подумали, що теги не повинні повторюватися, вони, ймовірно, повинні бути унікальними рядками. @@ -60,29 +41,29 @@ my_list: List[str] {* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} -Навіть якщо Ви отримаєте запит з дубльованими даними, він буде перетворений у множину унікальних елементів. +Навіть якщо ви отримаєте запит з дубльованими даними, він буде перетворений у множину унікальних елементів. -І коли Ви будете виводити ці дані, навіть якщо джерело містить дублікати, вони будуть виведені як множина унікальних елементів. +І коли ви будете виводити ці дані, навіть якщо джерело містить дублікати, вони будуть виведені як множина унікальних елементів. І це буде анотовано/документовано відповідно. -## Вкладені моделі +## Вкладені моделі { #nested-models } Кожен атрибут моделі Pydantic має тип. Але цей тип сам може бути іншою моделлю Pydantic. -Отже, Ви можете оголосити глибоко вкладені JSON "об'єкти" з конкретними іменами атрибутів, типами та перевірками. +Отже, ви можете оголосити глибоко вкладені JSON «об'єкти» з конкретними іменами атрибутів, типами та перевірками. Усе це, вкладене без обмежень. -### Визначення підмоделі +### Визначення підмоделі { #define-a-submodel } Наприклад, ми можемо визначити модель `Image`: {* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} -### Використання підмоделі як типу +### Використання підмоделі як типу { #use-the-submodel-as-a-type } А потім ми можемо використовувати її як тип атрибута: @@ -104,16 +85,16 @@ my_list: List[str] } ``` -Завдяки такій декларації у **FastAPI** Ви отримуєте: +Завдяки такій декларації у **FastAPI** ви отримуєте: * Підтримку в редакторі (автозавершення тощо), навіть для вкладених моделей * Конвертацію даних * Валідацію даних * Автоматичну документацію -## Спеціальні типи та валідація +## Спеціальні типи та валідація { #special-types-and-validation } -Окрім звичайних типів, таких як `str`, `int`, `float`, та ін. Ви можете використовувати складніші типи, які наслідують `str`. +Окрім звичайних типів, таких як `str`, `int`, `float`, та ін. ви можете використовувати складніші типи, які наслідують `str`. Щоб побачити всі доступні варіанти, ознайомтеся з оглядом <a href="https://docs.pydantic.dev/latest/concepts/types/" class="external-link" target="_blank">типів у Pydantic</a>. Деякі приклади будуть у наступних розділах. @@ -123,9 +104,9 @@ my_list: List[str] Рядок буде перевірено як дійсну URL-адресу і задокументовано в JSON Schema / OpenAPI як URL. -## Атрибути зі списками підмоделей +## Атрибути зі списками підмоделей { #attributes-with-lists-of-submodels } -У Pydantic Ви можете використовувати моделі як підтипи для `list`, `set` тощо: +У Pydantic ви можете використовувати моделі як підтипи для `list`, `set` тощо: {* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *} @@ -161,7 +142,7 @@ my_list: List[str] /// -## Глибоко вкладені моделі +## Глибоко вкладені моделі { #deeply-nested-models } Ви можете визначати вкладені моделі довільної глибини: @@ -173,14 +154,9 @@ my_list: List[str] /// -## Тіла запитів, що складаються зі списків +## Тіла запитів, що складаються зі списків { #bodies-of-pure-lists } -Якщо верхній рівень JSON тіла, яке Ви очікуєте, є JSON `масивом` (у Python — `list`), Ви можете оголосити тип у параметрі функції, як і в моделях Pydantic: - -```Python -images: List[Image] -``` -або в Python 3.9 і вище: +Якщо верхній рівень JSON тіла, яке ви очікуєте, є JSON `масивом` (у Python — `list`), ви можете оголосити тип у параметрі функції, як і в моделях Pydantic: ```Python images: list[Image] @@ -190,7 +166,7 @@ images: list[Image] {* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *} -## Підтримка в редакторі всюди +## Підтримка в редакторі всюди { #editor-support-everywhere } Ви отримаєте підтримку в редакторі всюди. @@ -200,23 +176,23 @@ images: list[Image] Ви не змогли б отримати таку підтримку в редакторі, якби працювали напряму зі `dict`, а не з моделями Pydantic. -Але Вам не потрібно турбуватися про це: вхідні dict'и автоматично конвертуються, а вихідні дані автоматично перетворюються в JSON. +Але вам не потрібно турбуватися про це: вхідні dict'и автоматично конвертуються, а вихідні дані автоматично перетворюються в JSON. -## Тіла з довільними `dict` +## Тіла з довільними `dict` { #bodies-of-arbitrary-dicts } Ви також можете оголосити тіло як `dict` з ключами одного типу та значеннями іншого типу. -Це корисно, якщо Ви не знаєте наперед, які імена полів будуть дійсними (як у випадку з моделями Pydantic). +Це корисно, якщо ви не знаєте наперед, які імена полів будуть дійсними (як у випадку з моделями Pydantic). -Це буде корисно, якщо Ви хочете приймати ключі, які заздалегідь невідомі. +Це буде корисно, якщо ви хочете приймати ключі, які заздалегідь невідомі. --- -Це також зручно, якщо Ви хочете мати ключі іншого типу (наприклад, `int`). +Це також зручно, якщо ви хочете мати ключі іншого типу (наприклад, `int`). Ось що ми розглянемо далі. -У цьому випадку Ви можете приймати будь-який `dict`, якщо його ключі — це `int`, а значення — `float`: +У цьому випадку ви можете приймати будь-який `dict`, якщо його ключі — це `int`, а значення — `float`: {* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *} @@ -228,18 +204,18 @@ images: list[Image] Це означає, що навіть якщо клієнти вашого API надсилатимуть ключі у вигляді рядків, якщо вони містять цілі числа, Pydantic конвертує їх і проведе валідацію. -Тобто `dict`, який Ви отримаєте як `weights`, матиме ключі типу `int` та значення типу `float`. +Тобто `dict`, який ви отримаєте як `weights`, матиме ключі типу `int` та значення типу `float`. /// -## Підсумок +## Підсумок { #recap } -З **FastAPI** Ви маєте максимальну гнучкість завдяки моделям Pydantic, зберігаючи при цьому код простим, коротким та елегантним. +З **FastAPI** ви маєте максимальну гнучкість завдяки моделям Pydantic, зберігаючи при цьому код простим, коротким та елегантним. А також отримуєте всі переваги: * Підтримка в редакторі (автодоповнення всюди!) * Конвертація даних (парсинг/сериалізація) -* Валідація даних +* Валідацію даних * Документація схем * Автоматичне створення документації diff --git a/docs/uk/docs/tutorial/body-updates.md b/docs/uk/docs/tutorial/body-updates.md index e78b5a5bf..2ae68291c 100644 --- a/docs/uk/docs/tutorial/body-updates.md +++ b/docs/uk/docs/tutorial/body-updates.md @@ -1,8 +1,8 @@ -# Тіло – Оновлення +# Тіло — Оновлення { #body-updates } -## Оновлення з використанням `PUT` +## Оновлення із заміною за допомогою `PUT` { #update-replacing-with-put } -Щоб оновити елемент, Ви можете використати <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT" class="external-link" target="_blank">HTTP `PUT`</a> операцію. +Щоб оновити елемент, ви можете використати <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT" class="external-link" target="_blank">HTTP `PUT`</a> операцію. Ви можете використати `jsonable_encoder`, щоб перетворити вхідні дані на такі, які можна зберігати як JSON (наприклад, у NoSQL базі даних). Наприклад, перетворюючи `datetime` у `str`. @@ -10,7 +10,7 @@ `PUT` використовується для отримання даних, які мають замінити чинні дані. -### Попередження про заміну +### Попередження про заміну { #warning-about-replacing } Це означає, що якщо Ви хочете оновити елемент `bar`, використовуючи `PUT` з тілом: @@ -26,7 +26,7 @@ І дані будуть збережені з цим "новим" значенням `tax` = `10.5`. -## Часткові оновлення з `PATCH` +## Часткові оновлення з `PATCH` { #partial-updates-with-patch } Ви також можете використовувати операцію <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH" class="external-link" target="_blank">HTTP `PATCH`</a> для *часткового* оновлення даних. @@ -34,53 +34,37 @@ /// note | Примітка -`PATCH` менш відомий і рідше використовується, ніж `PUT`. +`PATCH` менш поширений і менш відомий, ніж `PUT`. І багато команд використовують лише `PUT`, навіть для часткових оновлень. -Ви **вільні** використовувати їх так, як хочете, **FastAPI** не накладає обмежень. +Ви **вільні** використовувати їх так, як хочете, **FastAPI** не накладає жодних обмежень. -Але цей посібник показує Вам більш-менш як їх задумано використовувати. +Але цей посібник показує вам, більш-менш, як їх задумано використовувати. /// -### Використання параметра `exclude_unset` у Pydantic +### Використання параметра `exclude_unset` у Pydantic { #using-pydantics-exclude-unset-parameter } -Якщо Ви хочете отримати часткові оновлення, дуже зручно використовувати параметр `exclude_unset` у методі `.model_dump()` моделі Pydantic. +Якщо Ви хочете отримувати часткові оновлення, дуже корисно використовувати параметр `exclude_unset` у `.model_dump()` моделі Pydantic. Наприклад: `item.model_dump(exclude_unset=True)`. -/// info | Інформація +Це згенерує `dict` лише з тими даними, які були встановлені під час створення моделі `item`, виключаючи значення за замовчуванням. -У Pydantic v1 цей метод називався `.dict()`, він був застарілий (але все ще підтримується) у Pydantic v2, і був перейменований у `.model_dump()`. - -Приклади тут використовують `.dict()` для сумісності з Pydantic v1, але Вам слід використовувати `.model_dump()`, якщо можете використовувати Pydantic v2. - -/// - -Це створить `dict` лише з тими даними, які були явно встановлені під час створення моделі `item`, виключаючи значення за замовчуванням. - -Тоді Ви можете використовувати це, щоб створити `dict` лише з даними, які були встановлені (надіслані у запиті), пропускаючи значення за замовчуванням: +Тоді Ви можете використовувати це, щоб згенерувати `dict` лише з даними, які були встановлені (надіслані у запиті), пропускаючи значення за замовчуванням: {* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} -### Використання параметра `update` у Pydantic +### Використання параметра `update` у Pydantic { #using-pydantics-update-parameter } -Тепер Ви можете створити копію наявної моделі за допомогою `.model_copy()`, і передати параметр `update` з `dict` , який містить дані для оновлення. - -/// info | Інформація - -У Pydantic v1 метод називався `.copy()`, він був застарілий (але все ще підтримується) у Pydantic v2, і був перейменований у `.model_copy()`. - -Приклади тут використовують `.copy()` для сумісності з Pydantic v1, але якщо Ви можете використовувати Pydantic v2 — Вам слід використовувати `.model_copy()` замість цього. - -/// +Тепер Ви можете створити копію наявної моделі за допомогою `.model_copy()`, і передати параметр `update` з `dict`, який містить дані для оновлення. Наприклад: `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} -### Підсумок часткових оновлень +### Підсумок часткових оновлень { #partial-updates-recap } У підсумку, щоб застосувати часткові оновлення, Ви: @@ -101,7 +85,7 @@ Насправді Ви можете використовувати цю саму техніку і з операцією HTTP `PUT`. -Але приклад тут використовує `PATCH`, тому що він був створений саме для таких випадків. +Але приклад тут використовує `PATCH`, тому що він був створений для таких випадків. /// @@ -109,7 +93,7 @@ Зверніть увагу, що модель запиту все ще проходить валідацію. -Тож, якщо Ви хочете отримувати часткові оновлення, які можуть не містити жодного атрибута, Вам потрібно мати модель, де всі атрибути позначені як необов’язкові (зі значеннями за замовчуванням або `None`). +Тож, якщо Ви хочете отримувати часткові оновлення, які можуть пропускати всі атрибути, Вам потрібно мати модель, де всі атрибути позначені як необов’язкові (зі значеннями за замовчуванням або `None`). Щоб розрізняти моделі з усіма необов’язковими значеннями для **оновлення** і моделі з обов’язковими значеннями для **створення**, Ви можете скористатись ідеями, описаними у [Додаткові моделі](extra-models.md){.internal-link target=_blank}. diff --git a/docs/uk/docs/tutorial/body.md b/docs/uk/docs/tutorial/body.md index 38fed7bb8..ca1f308ab 100644 --- a/docs/uk/docs/tutorial/body.md +++ b/docs/uk/docs/tutorial/body.md @@ -1,14 +1,14 @@ -# Тіло запиту +# Тіло запиту { #request-body } Коли вам потрібно надіслати дані з клієнта (скажімо, браузера) до вашого API, ви надсилаєте їх як **тіло запиту**. Тіло **запиту** — це дані, надіслані клієнтом до вашого API. Тіло **відповіді** — це дані, які ваш API надсилає клієнту. -Ваш API майже завжди має надсилати тіло **відповіді**. Але клієнтам не обов’язково потрібно постійно надсилати тіла **запитів**. +Ваш API майже завжди має надсилати тіло **відповіді**. Але клієнтам не обов’язково потрібно постійно надсилати тіла **запитів** — інколи вони лише запитують шлях, можливо з деякими параметрами запиту, але не надсилають тіло. Щоб оголосити тіло **запиту**, ви використовуєте <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> моделі з усією їх потужністю та перевагами. -/// info +/// info | Інформація Щоб надіслати дані, ви повинні використовувати один із: `POST` (більш поширений), `PUT`, `DELETE` або `PATCH`. @@ -18,21 +18,22 @@ /// -## Імпортуйте `BaseModel` від Pydantic +## Імпортуйте `BaseModel` від Pydantic { #import-pydantics-basemodel } Спочатку вам потрібно імпортувати `BaseModel` з `pydantic`: -{* ../../docs_src/body/tutorial001.py hl[4] *} +{* ../../docs_src/body/tutorial001_py310.py hl[2] *} -## Створіть свою модель даних +## Створіть свою модель даних { #create-your-data-model } Потім ви оголошуєте свою модель даних як клас, який успадковується від `BaseModel`. Використовуйте стандартні типи Python для всіх атрибутів: -{* ../../docs_src/body/tutorial001.py hl[7:11] *} +{* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} -Так само, як і при оголошенні параметрів запиту, коли атрибут моделі має значення за замовчуванням, він не є обов’язковим. В іншому випадку це потрібно. Використовуйте `None`, щоб зробити його необов'язковим. + +Так само, як і при оголошенні параметрів запиту, коли атрибут моделі має значення за замовчуванням, він не є обов’язковим. В іншому випадку це потрібно. Використовуйте `None`, щоб зробити його просто необов'язковим. Наприклад, ця модель вище оголошує JSON "`об'єкт`" (або Python `dict`), як: @@ -54,15 +55,15 @@ } ``` -## Оголоси її як параметр +## Оголосіть її як параметр { #declare-it-as-a-parameter } Щоб додати модель даних до вашої *операції шляху*, оголосіть її так само, як ви оголосили параметри шляху та запиту: -{* ../../docs_src/body/tutorial001.py hl[18] *} +{* ../../docs_src/body/tutorial001_py310.py hl[16] *} ...і вкажіть її тип як модель, яку ви створили, `Item`. -## Результати +## Результати { #results } Лише з цим оголошенням типу Python **FastAPI** буде: @@ -73,9 +74,9 @@ * Надавати отримані дані у параметрі `item`. * Оскільки ви оголосили його у функції як тип `Item`, ви також матимете всю підтримку редактора (автозаповнення, тощо) для всіх атрибутів та їх типів. * Генерувати <a href="https://json-schema.org" class="external-link" target="_blank">JSON Schema</a> визначення для вашої моделі, ви також можете використовувати їх де завгодно, якщо це має сенс для вашого проекту. -* Ці схеми будуть частиною згенерованої схеми OpenAPI і використовуватимуться автоматичною документацією інтерфейсу користувача. +* Ці схеми будуть частиною згенерованої схеми OpenAPI і використовуватимуться автоматичною документацією <abbr title="User Interfaces – Інтерфейси користувача">UIs</abbr>. -## Автоматична документація +## Автоматична документація { #automatic-docs } Схеми JSON ваших моделей будуть частиною вашої схеми, згенерованої OpenAPI, і будуть показані в інтерактивній API документації: @@ -85,7 +86,7 @@ <img src="/img/tutorial/body/image02.png"> -## Підтримка редактора +## Підтримка редактора { #editor-support } У вашому редакторі, всередині вашої функції, ви будете отримувати підказки типу та завершення скрізь (це б не сталося, якби ви отримали `dict` замість моделі Pydantic): @@ -107,7 +108,7 @@ <img src="/img/tutorial/body/image05.png"> -/// tip +/// tip | Порада Якщо ви використовуєте <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a> як ваш редактор, ви можете використати <a href="https://github.com/koxudaxi/pydantic-pycharm-plugin/" class="external-link" target="_blank">Pydantic PyCharm Plugin</a>. @@ -121,42 +122,45 @@ /// -## Використовуйте модель +## Використовуйте модель { #use-the-model } Усередині функції ви можете отримати прямий доступ до всіх атрибутів об’єкта моделі: -{* ../../docs_src/body/tutorial002.py hl[21] *} +{* ../../docs_src/body/tutorial002_py310.py *} -## Тіло запиту + параметри шляху +## Тіло запиту + параметри шляху { #request-body-path-parameters } Ви можете одночасно оголошувати параметри шляху та тіло запиту. **FastAPI** розпізнає, що параметри функції, які відповідають параметрам шляху, мають бути **взяті з шляху**, а параметри функції, які оголошуються як моделі Pydantic, **взяті з тіла запиту**. -{* ../../docs_src/body/tutorial003.py hl[17:18] *} +{* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} -## Тіло запиту + шлях + параметри запиту + +## Тіло запиту + шлях + параметри запиту { #request-body-path-query-parameters } Ви також можете оголосити параметри **тіло**, **шлях** і **запит** одночасно. **FastAPI** розпізнає кожен з них і візьме дані з потрібного місця. -{* ../../docs_src/body/tutorial004.py hl[18] *} +{* ../../docs_src/body/tutorial004_py310.py hl[16] *} Параметри функції будуть розпізнаватися наступним чином: * Якщо параметр також оголошено в **шляху**, він використовуватиметься як параметр шляху. * Якщо параметр має **сингулярний тип** (наприклад, `int`, `float`, `str`, `bool` тощо), він буде інтерпретуватися як параметр **запиту**. -* Якщо параметр оголошується як тип **Pydantic моделі**, він інтерпретується як **тіло** запиту. +* Якщо параметр оголошується як тип **Pydantic моделі**, він інтерпретується як **тіло** **запиту**. -/// note +/// note | Примітка -FastAPI буде знати, що значення "q" не є обов'язковим через значення за замовчуванням "= None". +FastAPI буде знати, що значення `q` не є обов'язковим через значення за замовчуванням `= None`. -`Optional` у `Optional[str]` не використовується FastAPI, але дозволить вашому редактору надати вам кращу підтримку та виявляти помилки. +`str | None` (Python 3.10+) або `Union` у `Union[str, None]` (Python 3.9+) FastAPI не використовує, щоб визначити, що значення не є обов’язковим — він знатиме, що воно не є обов’язковим, тому що має значення за замовчуванням `= None`. + +Але додавання анотацій типів дозволить вашому редактору надати вам кращу підтримку та виявляти помилки. /// -## Без Pydantic +## Без Pydantic { #without-pydantic } -Якщо ви не хочете використовувати моделі Pydantic, ви також можете використовувати параметри **Body**. Перегляньте документацію для [Тіло – Кілька параметрів: сингулярні значення в тілі](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. +Якщо ви не хочете використовувати моделі Pydantic, ви також можете використовувати параметри **Body**. Перегляньте документацію для [Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. diff --git a/docs/uk/docs/tutorial/cookie-param-models.md b/docs/uk/docs/tutorial/cookie-param-models.md index f070b6ac8..3c6407716 100644 --- a/docs/uk/docs/tutorial/cookie-param-models.md +++ b/docs/uk/docs/tutorial/cookie-param-models.md @@ -1,32 +1,32 @@ -# Моделі для Cookie-параметрів +# Моделі параметрів Cookie { #cookie-parameter-models } -Якщо у Вас є група **cookies** параметрів, які пов'язані між собою, Ви можете створити **Pydantic-модель**, щоб оголосити їх. 🍪 +Якщо у Вас є група **cookies**, які пов'язані між собою, Ви можете створити **Pydantic-модель**, щоб оголосити їх. 🍪 Це дозволить Вам повторно **використовувати модель** у **різних місцях**, а також оголосити валідацію та метадані для всіх параметрів одночасно. 😎 -/// note | Нотатки +/// note | Примітка -Це підтримується з версії FastAPI `0.115.0`. 🤓 +Це підтримується з версії FastAPI `0.115.0`. 🤓 /// /// tip | Порада -Ця ж техніка застосовується до `Query`, `Cookie`, та `Header`. 😎 +Ця ж техніка застосовується до `Query`, `Cookie` та `Header`. 😎 /// -## Cookie з Pydantic-моделлю +## Cookie з Pydantic-моделлю { #cookies-with-a-pydantic-model } -Оголосіть **cookie-параметри**, які Вам потрібні, у **Pydantic-моделі**, а потім оголосіть параметр як `Cookie`: +Оголосіть **cookie**-параметри, які Вам потрібні, у **Pydantic-моделі**, а потім оголосіть параметр як `Cookie`: {* ../../docs_src/cookie_param_models/tutorial001_an_py310.py hl[9:12,16] *} -**FastAPI** буде **витягувати** дані для **кожного поля** з **cookie** параметрів, отриманих у запиті, і передавати Вам Pydantic-модель, яку Ви визначили. +**FastAPI** буде **витягувати** дані для **кожного поля** з **cookies**, отриманих у запиті, і передавати Вам Pydantic-модель, яку Ви визначили. -## Перевірка у документації +## Перевірка у документації { #check-the-docs } -Ви можете побачити визначені cookie в інтерфейсі документації за адресою `/docs`: +Ви можете побачити визначені cookies в інтерфейсі документації за адресою `/docs`: <div class="screenshot"> <img src="/img/tutorial/cookie-param-models/image01.png"> @@ -34,29 +34,29 @@ /// info | Інформація -Майте на увазі, що оскільки **браузери обробляють cookie** особливим чином і "за лаштунками", вони **не** дозволяють **JavaScript** легко з ними працювати. +Майте на увазі, що оскільки **браузери обробляють cookies** особливим чином і «за лаштунками», вони **не** дозволяють **JavaScript** легко з ними працювати. -Якщо Ви зайдете до **інтерфейсу документації API** за адресою `/docs`, Ви зможете побачити **документацію** для cookie у Ваших **операціях шляху**. +Якщо Ви зайдете до **інтерфейсу документації API** за адресою `/docs`, Ви зможете побачити **документацію** для cookies у Ваших *операціях шляху*. -Але навіть якщо Ви заповните дані й натиснете "Execute", оскільки інтерфейс документації працює з **JavaScript**, cookie не будуть відправлені, і Ви побачите **помилку**, ніби Ви не ввели жодних значень. +Але навіть якщо Ви заповните дані й натиснете "Execute", оскільки інтерфейс документації працює з **JavaScript**, cookies не будуть відправлені, і Ви побачите **помилку**, ніби Ви не ввели жодних значень. /// -## Заборона додаткових cookie +## Заборона додаткових cookie { #forbid-extra-cookies } -У деяких спеціальних випадках (ймовірно, не дуже поширених) Ви можете захотіти **обмежити** список cookie, які хочете отримувати. +У деяких спеціальних випадках (ймовірно, не дуже поширених) Ви можете захотіти **обмежити** cookies, які хочете отримувати. -Ваша API тепер має можливість контролювати власну <abbr title="Це жарт, якщо що. Це не має нічого спільного зі згодою на використання cookie, але це кумедно, що навіть API тепер може відхиляти бідні cookie. Ловіть печиво. 🍪">згоду на cookie</abbr>. 🤪🍪 +Ваша API тепер має можливість контролювати власну <abbr title="This is a joke, just in case. It has nothing to do with cookie consents, but it's funny that even the API can now reject the poor cookies. Have a cookie. 🍪">згоду на cookie</abbr>. 🤪🍪 -Ви можете використовувати налаштування моделі Pydantic, щоб `заборонити` будь-які `додаткові` поля: +Ви можете використовувати налаштування моделі Pydantic, щоб `forbid` будь-які `extra` поля: -{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} +{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *} -Якщо клієнт спробує надіслати якісь **додаткові cookie**, він отримає відповідь з **помилкою**. +Якщо клієнт спробує надіслати якісь **додаткові cookies**, він отримає відповідь з **помилкою**. -Бідні банери cookie, які так старанно намагаються отримати Вашу згоду, щоб <abbr title="Це ще один жарт. Не звертайте уваги. Візьміть каву для свого печива. ☕">API її відхилила</abbr>. 🍪 +Бідні банери cookie, які так старанно намагаються отримати Вашу згоду, щоб <abbr title="This is another joke. Don't pay attention to me. Have some coffee for your cookie. ☕">API її відхилила</abbr>. 🍪 -Наприклад, якщо клієнт спробує надіслати cookie `santa_tracker` зі значенням `good-list-please`, він отримає відповідь з помилкою, яка повідомить, що <abbr title="Санта не схвалює відсутність cookie. 🎅 Гаразд, більше жартів не буде.">cookie `santa_tracker` не дозволено</abbr>: +Наприклад, якщо клієнт спробує надіслати cookie `santa_tracker` зі значенням `good-list-please`, він отримає відповідь з помилкою, яка повідомить, що `santa_tracker` <abbr title="Santa disapproves the lack of cookies. 🎅 Okay, no more cookie jokes.">cookie не дозволено</abbr>: ```json { @@ -71,6 +71,6 @@ } ``` -## Підсумок +## Підсумок { #summary } -Ви можете використовувати **Pydantic-моделі** для оголошення <abbr title="Отримайте останнє печиво перед тим, як піти. 🍪">cookie</abbr> у FastAPI. 😎 +Ви можете використовувати **Pydantic-моделі** для оголошення <abbr title="Have a last cookie before you go. 🍪">**cookies**</abbr> у **FastAPI**. 😎 diff --git a/docs/uk/docs/tutorial/cookie-params.md b/docs/uk/docs/tutorial/cookie-params.md index b320645cb..8a5b44e8a 100644 --- a/docs/uk/docs/tutorial/cookie-params.md +++ b/docs/uk/docs/tutorial/cookie-params.md @@ -1,24 +1,25 @@ -# Параметри Cookie +# Параметри Cookie { #cookie-parameters } -Ви можете визначити параметри Cookie таким же чином, як визначаються параметри `Query` і `Path`. +Ви можете визначати параметри Cookie таким же чином, як визначаються параметри `Query` і `Path`. -## Імпорт `Cookie` +## Імпорт `Cookie` { #import-cookie } Спочатку імпортуйте `Cookie`: {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[3] *} -## Визначення параметрів `Cookie` +## Визначення параметрів `Cookie` { #declare-cookie-parameters } Потім визначте параметри cookie, використовуючи таку ж конструкцію як для `Path` і `Query`. -Перше значення це значення за замовчуванням, ви можете також передати всі додаткові параметри валідації чи анотації: +Ви можете визначити значення за замовчуванням, а також усі додаткові параметри валідації чи анотації: {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[9] *} -/// note | Технічні Деталі +/// note | Технічні деталі + +`Cookie` це "сестра" класів `Path` і `Query`. Вони також наслідуються від одного спільного класу `Param`. -`Cookie` це "сестра" класів `Path` і `Query`. Вони наслідуються від одного батьківського класу `Param`. Але пам'ятайте, що коли ви імпортуєте `Query`, `Path`, `Cookie` та інше з `fastapi`, це фактично функції, що повертають спеціальні класи. /// @@ -29,6 +30,16 @@ /// -## Підсумки +/// info + +Майте на увазі, що оскільки **браузери обробляють cookies** спеціальним чином і за лаштунками, вони **не** дозволяють **JavaScript** легко взаємодіяти з ними. + +Якщо ви перейдете до **інтерфейсу документації API** за адресою `/docs`, ви зможете побачити **документацію** для cookies для ваших *операцій шляху*. + +Але навіть якщо ви **заповните дані** і натиснете "Execute", оскільки інтерфейс документації працює з **JavaScript**, cookies не буде надіслано, і ви побачите повідомлення про **помилку**, ніби ви не ввели жодних значень. + +/// + +## Підсумки { #recap } Визначайте cookies за допомогою `Cookie`, використовуючи той же спільний шаблон, що і `Query` та `Path`. diff --git a/docs/uk/docs/tutorial/cors.md b/docs/uk/docs/tutorial/cors.md index 95b204d0f..f3ed8a7d9 100644 --- a/docs/uk/docs/tutorial/cors.md +++ b/docs/uk/docs/tutorial/cors.md @@ -1,8 +1,8 @@ -# CORS (Обмін ресурсами між різними джерелами) +# CORS (Обмін ресурсами між різними джерелами) { #cors-cross-origin-resource-sharing } -<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">CORS або "Обмін ресурсами між різними джерелами"</a> є ситуація, коли фронтенд, що працює в браузері, містить JavaScript-код, який взаємодіє з бекендом, розташованим в іншому "джерелі" (origin). +<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">CORS або "Cross-Origin Resource Sharing"</a> є ситуація, коли фронтенд, що працює в браузері, містить JavaScript-код, який взаємодіє з бекендом, розташованим в іншому "джерелі" (origin). -## Джерело (Origin) +## Джерело (Origin) { #origin } Джерело визначається комбінацією протоколу (`http`, `https`), домену (`myapp.com`, `localhost`, `localhost.tiangolo.com`), порту (`80`, `443`, `8080`). @@ -15,7 +15,7 @@ Навіть якщо вони всі містять `localhost`, вони мають різні протоколи або порти, що робить їх окремими "джерелами". -## Кроки +## Кроки { #steps } Припустимо, що Ваш фронтенд працює в браузері на `http://localhost:8080`, а його JavaScript намагається відправити запит до бекенду, який працює на `http://localhost` (Оскільки ми не вказуємо порт, браузер за замовчуванням припускає порт `80`). @@ -25,15 +25,15 @@ У цьому випадку список має містити `http://localhost:8080`, щоб фронтенд на порту `:8080` працював коректно. -## Символьне підставляння +## Символьне підставляння { #wildcards } Можна також оголосити список як `"*"` ("символьне підставляння"), що означає дозвіл для всіх джерел. -Однак це дозволить лише певні типи комунікації, виключаючи все, що пов'язане з обліковими даними: Cookies, заголовки авторизації, такі як ті, що використовуються з Bearer токенами тощо. +Однак це дозволить лише певні типи комунікації, виключаючи все, що пов'язане з обліковими даними: Cookies, заголовки авторизації, такі як ті, що використовуються з Bearer Tokens, тощо. Тому для коректної роботи краще явно вказувати дозволені джерела. -## Використання `CORSMiddleware` +## Використання `CORSMiddleware` { #use-corsmiddleware } Ви можете налаштувати це у Вашому додатку **FastAPI** за допомогою `CORSMiddleware`. @@ -44,41 +44,44 @@ Також можна вказати, чи дозволяє Ваш бекенд: -* Облікові дані (заголовки авторизації, сookies, тощо). +* Облікові дані (заголовки авторизації, Cookies, тощо). * Конкретні HTTP-методи (`POST`, `PUT`) або всі за допомогою `"*"` * Конкретні HTTP-заголовки або всі за допомогою `"*"`. -{* ../../docs_src/cors/tutorial001.py hl[2,6:11,13:19] *} +{* ../../docs_src/cors/tutorial001_py39.py hl[2,6:11,13:19] *} -Параметри за замовчуванням у `CORSMiddleware` є досить обмеженими, тому Вам потрібно явно вказати конкретні джерела, методи або заголовки, щоб браузери могли використовувати їх у контексті запитів між різними доменами. +Параметри за замовчуванням у реалізації `CORSMiddleware` є досить обмеженими, тому Вам потрібно явно увімкнути конкретні джерела, методи або заголовки, щоб браузерам було дозволено використовувати їх у міждоменному контексті. Підтримуються такі аргументи: -* `allow_origins` - Список джерел, яким дозволено здійснювати міждоменні запити. Наприклад `['https://example.org', 'https://www.example.org']`. Ви можете використовувати ['*'], щоб дозволити всі джерела. +* `allow_origins` - Список джерел, яким дозволено здійснювати міждоменні запити. Наприклад `['https://example.org', 'https://www.example.org']`. Ви можете використовувати `['*']`, щоб дозволити будь-яке джерело. * `allow_origin_regex` - Рядок регулярного виразу для відповідності джерелам, яким дозволено здійснювати міждоменні запити. Наприклад, `'https://.*\.example\.org'`. * `allow_methods` - Список HTTP-методів, дозволених для міждоменних запитів. За замовчуванням `['GET']`. Ви можете використовувати `['*']`, щоб дозволити всі стандартні методи. -* `allow_headers` - Список HTTP-заголовків, які підтримуються для міждоменних запитів. За замовчуванням `[]`. Ви можете використовувати `['*']`, щоб дозволити всі заголовки. Заголовки `Accept`, `Accept-Language`, `Content-Language` і `Content-Type` завжди дозволені для <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests" class="external-link" rel="noopener" target="_blank">простих CORS-запитів</a>. -* `allow_credentials` - Визначає, чи підтримуються файли cookie для міждоменних запитів. За замовчуванням `False`. Також, якщо потрібно дозволити обмін обліковими даними (`allow_credentials = True`), параметр `allow_origins` не може бути встановлений як `['*']`, необхідно вказати конкретні джерела. +* `allow_headers` - Список HTTP-заголовків запиту, які підтримуються для міждоменних запитів. За замовчуванням `[]`. Ви можете використовувати `['*']`, щоб дозволити всі заголовки. Заголовки `Accept`, `Accept-Language`, `Content-Language` і `Content-Type` завжди дозволені для <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests" class="external-link" rel="noopener" target="_blank">простих CORS-запитів</a>. +* `allow_credentials` - Визначає, чи повинні підтримуватися cookies для міждоменних запитів. За замовчуванням `False`. + + Жоден із параметрів `allow_origins`, `allow_methods` і `allow_headers` не можна встановлювати як `['*']`, якщо `allow_credentials` встановлено як `True`. Усі вони мають бути <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#credentialed_requests_and_wildcards" class="external-link" rel="noopener" target="_blank">явно вказані</a>. + * `expose_headers` - Вказує, які заголовки відповіді повинні бути доступні для браузера. За замовчуванням `[]`. * `max_age` - Встановлює максимальний час (у секундах) для кешування CORS-відповідей у браузерах. За замовчуванням `600`. Цей middleware обробляє два типи HTTP-запитів... -### Попередні CORS-запити (preflight requests) +### Попередні CORS-запити { #cors-preflight-requests } Це будь-які `OPTIONS` - запити, що містять заголовки `Origin` та `Access-Control-Request-Method`. У такому випадку middleware перехопить вхідний запит і відповість відповідними CORS-заголовками, повертаючи або `200`, або `400` для інформаційних цілей. -### Прості запити +### Прості запити { #simple-requests } Будь-які запити із заголовком `Origin`. У цьому випадку middleware пропустить запит як звичайний, але додасть відповідні CORS-заголовки у відповідь. -## Додаткова інформація +## Додаткова інформація { #more-info } -Більше про <abbr title="Cross-Origin Resource Sharing">CORS</abbr> можна дізнатися в <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">документації Mozilla</a>. +Більше про <abbr title="Cross-Origin Resource Sharing">CORS</abbr> можна дізнатися в <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">документації Mozilla про CORS</a>. /// note | Технічні деталі diff --git a/docs/uk/docs/tutorial/debugging.md b/docs/uk/docs/tutorial/debugging.md index b0e5344f8..0db418dcc 100644 --- a/docs/uk/docs/tutorial/debugging.md +++ b/docs/uk/docs/tutorial/debugging.md @@ -1,16 +1,16 @@ -# Налагодження (Debugging) +# Налагодження { #debugging } -Ви можете під'єднати дебагер у Вашому редакторі коду, наприклад, у Visual Studio Code або PyCharm. +Ви можете під'єднати дебагер у вашому редакторі коду, наприклад, у Visual Studio Code або PyCharm. -## Виклик `uvicorn` +## Виклик `uvicorn` { #call-uvicorn } -У Вашому FastAPI-додатку імпортуйте та запустіть `uvicorn` безпосередньо: +У вашому FastAPI-додатку імпортуйте та запустіть `uvicorn` безпосередньо: -{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} -### Про `__name__ == "__main__"` +### Про `__name__ == "__main__"` { #about-name-main } -Головна мета використання `__name__ == "__main__"` — це забезпечення виконання певного коду тільки тоді, коли файл запускається безпосередньо: +Головна мета використання `__name__ == "__main__"` — це забезпечення виконання певного коду лише тоді, коли ваш файл запускається так: <div class="termy"> @@ -20,17 +20,17 @@ $ python myapp.py </div> -але не виконується при його імпорті в інший файл, наприклад: +але не виконується, коли інший файл імпортує його, наприклад: ```Python from myapp import app ``` -#### Детальніше +#### Детальніше { #more-details } -Припустимо, Ваш файл називається `myapp.py`. +Припустимо, ваш файл називається `myapp.py`. -Якщо Ви запустите його так: +Якщо ви запустите його так: <div class="termy"> @@ -40,7 +40,7 @@ $ python myapp.py </div> -тоді внутрішня змінна `__name__`, яка створюється автоматично Python, матиме значення `"__main__"`. +тоді внутрішня змінна `__name__` у вашому файлі, яка створюється автоматично Python, матиме значення рядка `"__main__"`. Отже, цей блок коду: @@ -52,17 +52,17 @@ $ python myapp.py --- -Це не станеться, якщо Ви імпортуєте цей модуль (файл). +Це не станеться, якщо ви імпортуєте цей модуль (файл). -Якщо у Вас є інший файл, наприклад `importer.py`, з наступним кодом: +Отже, якщо у вас є інший файл `importer.py` з: ```Python from myapp import app -# Додатковий код +# Some more code ``` -У цьому випадку автоматично створена змінна у файлі `myapp.py` не матиме значення змінної `__name__` як `"__main__"`. +у цьому випадку автоматично створена змінна `__name__` всередині `myapp.py` не матиме значення `"__main__"`. Отже, рядок: @@ -74,38 +74,39 @@ from myapp import app /// info | Інформація -Більш детальну інформацію можна знайти в <a href="https://docs.python.org/3/library/__main__.html" class="external-link" target="_blank">офіційній документації Python</a>. +Для отримання додаткової інформації дивіться <a href="https://docs.python.org/3/library/__main__.html" class="external-link" target="_blank">офіційну документацію Python</a>. /// -## Запуск коду з вашим дебагером +## Запуск коду з вашим дебагером { #run-your-code-with-your-debugger } -Оскільки Ви запускаєте сервер Uvicorn безпосередньо з Вашого коду, Ви можете запустити вашу Python програму (ваш FastAPI додаток) безпосередньо з дебагера. +Оскільки ви запускаєте сервер Uvicorn безпосередньо з вашого коду, ви можете запустити вашу Python програму (ваш FastAPI-додаток) безпосередньо з дебагера. --- -Наприклад, у Visual Studio Code Ви можете: +Наприклад, у Visual Studio Code ви можете: -* Перейдіть на вкладку "Debug". -* Натисніть "Add configuration...". -* Виберіть "Python" +* Перейдіть на панель «Debug». +* «Add configuration...». +* Виберіть «Python» * Запустіть дебагер з опцією "`Python: Current File (Integrated Terminal)`". -Це запустить сервер з Вашим **FastAPI** кодом, зупиниться на точках зупину тощо. +Після цього він запустить сервер з вашим кодом **FastAPI**, зупиниться на точках зупину тощо. Ось як це може виглядати: <img src="/img/tutorial/debugging/image01.png"> --- -Якщо Ви використовуєте PyCharm, ви можете: -* Відкрити меню "Run". -* Вибрати опцію "Debug...". +Якщо ви використовуєте PyCharm, ви можете: + +* Відкрити меню «Run». +* Вибрати опцію «Debug...». * Потім з'явиться контекстне меню. * Вибрати файл для налагодження (у цьому випадку, `main.py`). -Це запустить сервер з Вашим **FastAPI** кодом, зупиниться на точках зупину тощо. +Після цього він запустить сервер з вашим кодом **FastAPI**, зупиниться на точках зупину тощо. Ось як це може виглядати: diff --git a/docs/uk/docs/tutorial/encoder.md b/docs/uk/docs/tutorial/encoder.md index f202c7989..1b403d5bb 100644 --- a/docs/uk/docs/tutorial/encoder.md +++ b/docs/uk/docs/tutorial/encoder.md @@ -1,32 +1,32 @@ -# JSON Compatible Encoder +# JSON-сумісний кодувальник { #json-compatible-encoder } -Існують випадки, коли вам може знадобитися перетворити тип даних (наприклад, модель Pydantic) в щось сумісне з JSON (наприклад, `dict`, `list`, і т. д.). +Існують випадки, коли вам може знадобитися перетворити тип даних (наприклад, модель Pydantic) на щось сумісне з JSON (наприклад, `dict`, `list` тощо). Наприклад, якщо вам потрібно зберегти це в базі даних. -Для цього, **FastAPI** надає `jsonable_encoder()` функцію. +Для цього **FastAPI** надає функцію `jsonable_encoder()`. -## Використання `jsonable_encoder` +## Використання `jsonable_encoder` { #using-the-jsonable-encoder } Давайте уявимо, що у вас є база даних `fake_db`, яка приймає лише дані, сумісні з JSON. Наприклад, вона не приймає об'єкти типу `datetime`, оскільки вони не сумісні з JSON. -Отже, об'єкт типу `datetime` потрібно перетворити в рядок `str`, який містить дані в <a href="https://en.wikipedia.org/wiki/ISO_8601" class="external-link" target="_blank">ISO форматі</a>. +Отже, об'єкт типу `datetime` потрібно перетворити на `str`, який містить дані в <a href="https://en.wikipedia.org/wiki/ISO_8601" class="external-link" target="_blank">форматі ISO</a>. -Тим самим способом ця база даних не прийматиме об'єкт типу Pydantic model (об'єкт з атрибутами), а лише `dict`. +Так само ця база даних не прийматиме модель Pydantic (об'єкт з атрибутами), а лише `dict`. Ви можете використовувати `jsonable_encoder` для цього. -Вона приймає об'єкт, такий як Pydantic model, і повертає його версію, сумісну з JSON: +Вона приймає об'єкт, такий як модель Pydantic, і повертає його версію, сумісну з JSON: {* ../../docs_src/encoder/tutorial001_py310.py hl[4,21] *} -У цьому прикладі вона конвертує Pydantic model у `dict`, а `datetime` у `str`. +У цьому прикладі вона конвертує модель Pydantic у `dict`, а `datetime` у `str`. -Результат виклику цієї функції - це щось, що можна кодувати з використанням стандарту Python <a href="https://docs.python.org/3/library/json.html#json.dumps" class="external-link" target="_blank">`json.dumps()`</a>. +Результат виклику цієї функції — це щось, що можна кодувати з використанням стандарту Python <a href="https://docs.python.org/3/library/json.html#json.dumps" class="external-link" target="_blank">`json.dumps()`</a>. -Вона не повертає велику строку `str`, яка містить дані у форматі JSON (як строка). Вона повертає стандартну структуру даних Python (наприклад `dict`) із значеннями та підзначеннями, які є сумісними з JSON. +Вона не повертає великий `str`, який містить дані у форматі JSON (як рядок). Вона повертає стандартну структуру даних Python (наприклад, `dict`) зі значеннями та підзначеннями, які є сумісними з JSON. /// note | Примітка diff --git a/docs/uk/docs/tutorial/extra-data-types.md b/docs/uk/docs/tutorial/extra-data-types.md index 5da942b6e..a3545e074 100644 --- a/docs/uk/docs/tutorial/extra-data-types.md +++ b/docs/uk/docs/tutorial/extra-data-types.md @@ -1,13 +1,13 @@ -# Додаткові типи даних +# Додаткові типи даних { #extra-data-types } -До цього часу, ви використовували загальнопоширені типи даних, такі як: +До цього часу ви використовували загальнопоширені типи даних, такі як: * `int` * `float` * `str` * `bool` -Але можна також використовувати більш складні типи даних. +Але ви також можете використовувати більш складні типи даних. І ви все ще матимете ті ж можливості, які були показані до цього: @@ -17,30 +17,30 @@ * Валідація даних. * Автоматична анотація та документація. -## Інші типи даних +## Інші типи даних { #other-data-types } Ось додаткові типи даних для використання: * `UUID`: - * Стандартний "Універсальний Унікальний Ідентифікатор", який часто використовується як ідентифікатор у багатьох базах даних та системах. + * Стандартний "Універсальний унікальний ідентифікатор", який часто використовується як ID у багатьох базах даних та системах. * У запитах та відповідях буде представлений як `str`. * `datetime.datetime`: * Пайтонівський `datetime.datetime`. - * У запитах та відповідях буде представлений як `str` в форматі ISO 8601, як: `2008-09-15T15:53:00+05:00`. + * У запитах та відповідях буде представлений як `str` у форматі ISO 8601, як: `2008-09-15T15:53:00+05:00`. * `datetime.date`: * Пайтонівський `datetime.date`. - * У запитах та відповідях буде представлений як `str` в форматі ISO 8601, як: `2008-09-15`. + * У запитах та відповідях буде представлений як `str` у форматі ISO 8601, як: `2008-09-15`. * `datetime.time`: * Пайтонівський `datetime.time`. - * У запитах та відповідях буде представлений як `str` в форматі ISO 8601, як: `14:23:55.003`. + * У запитах та відповідях буде представлений як `str` у форматі ISO 8601, як: `14:23:55.003`. * `datetime.timedelta`: * Пайтонівський `datetime.timedelta`. * У запитах та відповідях буде представлений як `float` загальної кількості секунд. - * Pydantic також дозволяє представляти це як "ISO 8601 time diff encoding", <a href="https://docs.pydantic.dev/latest/concepts/serialization/#json_encoders" class="external-link" target="_blank">більше інформації дивись у документації</a>. + * Pydantic також дозволяє представляти це як "ISO 8601 time diff encoding", <a href="https://docs.pydantic.dev/latest/concepts/serialization/#custom-serializers" class="external-link" target="_blank">дивіться документацію для отримання додаткової інформації</a>. * `frozenset`: * У запитах і відповідях це буде оброблено так само, як і `set`: - * У запитах список буде зчитано, дублікати будуть видалені та він буде перетворений на `set`. - * У відповідях, `set` буде перетворений на `list`. + * У запитах список буде зчитано, дублікати буде видалено, і його буде перетворено на `set`. + * У відповідях `set` буде перетворено на `list`. * Згенерована схема буде вказувати, що значення `set` є унікальними (з використанням JSON Schema's `uniqueItems`). * `bytes`: * Стандартний Пайтонівський `bytes`. @@ -49,11 +49,11 @@ * `Decimal`: * Стандартний Пайтонівський `Decimal`. * У запитах і відповідях це буде оброблено так само, як і `float`. -* Ви можете перевірити всі дійсні типи даних Pydantic тут: <a href="https://docs.pydantic.dev/latest/concepts/types/" class="external-link" target="_blank">типи даних Pydantic</a>. +* Ви можете перевірити всі дійсні типи даних Pydantic тут: <a href="https://docs.pydantic.dev/latest/usage/types/types/" class="external-link" target="_blank">типи даних Pydantic</a>. -## Приклад +## Приклад { #example } -Ось приклад *path operation* з параметрами, використовуючи деякі з вищезазначених типів. +Ось приклад *операції шляху* з параметрами, використовуючи деякі з вищезазначених типів. {* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[1,3,12:16] *} diff --git a/docs/uk/docs/tutorial/first-steps.md b/docs/uk/docs/tutorial/first-steps.md index 3f861cb48..5f3750010 100644 --- a/docs/uk/docs/tutorial/first-steps.md +++ b/docs/uk/docs/tutorial/first-steps.md @@ -1,126 +1,118 @@ -# Перші кроки +# Перші кроки { #first-steps } Найпростіший файл FastAPI може виглядати так: -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} Скопіюйте це до файлу `main.py`. -Запустіть сервер: +Запустіть live-сервер: <div class="termy"> ```console -$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:single">main.py</u> -<font color="#3465A4">INFO </font> Using path <font color="#3465A4">main.py</font> -<font color="#3465A4">INFO </font> Resolved absolute path <font color="#75507B">/home/user/code/awesomeapp/</font><font color="#AD7FA8">main.py</font> -<font color="#3465A4">INFO </font> Searching for package file structure from directories with <font color="#3465A4">__init__.py</font> files -<font color="#3465A4">INFO </font> Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> +$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:solid">main.py</u> - ╭─ <font color="#8AE234"><b>Python module file</b></font> ─╮ - │ │ - │ 🐍 main.py │ - │ │ - ╰──────────────────────╯ + <span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting development server 🚀 -<font color="#3465A4">INFO </font> Importing module <font color="#4E9A06">main</font> -<font color="#3465A4">INFO </font> Found importable FastAPI app + Searching for package file structure from directories + with <font color="#3465A4">__init__.py</font> files + Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> - ╭─ <font color="#8AE234"><b>Importable FastAPI app</b></font> ─╮ - │ │ - │ <span style="background-color:#272822"><font color="#FF4689">from</font></span><span style="background-color:#272822"><font color="#F8F8F2"> main </font></span><span style="background-color:#272822"><font color="#FF4689">import</font></span><span style="background-color:#272822"><font color="#F8F8F2"> app</font></span><span style="background-color:#272822"> </span> │ - │ │ - ╰──────────────────────────╯ + <span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py -<font color="#3465A4">INFO </font> Using import string <font color="#8AE234"><b>main:app</b></font> + <span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with + the following code: - <span style="background-color:#C4A000"><font color="#2E3436">╭────────── FastAPI CLI - Development mode ───────────╮</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ Serving at: http://127.0.0.1:8000 │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ API docs: http://127.0.0.1:8000/docs │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ Running in development mode, for production use: │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ </font></span><span style="background-color:#C4A000"><font color="#555753"><b>fastapi run</b></font></span><span style="background-color:#C4A000"><font color="#2E3436"> │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">╰─────────────────────────────────────────────────────╯</font></span> + <u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> -<font color="#4E9A06">INFO</font>: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] -<font color="#4E9A06">INFO</font>: Uvicorn running on <b>http://127.0.0.1:8000</b> (Press CTRL+C to quit) -<font color="#4E9A06">INFO</font>: Started reloader process [<font color="#34E2E2"><b>2265862</b></font>] using <font color="#34E2E2"><b>WatchFiles</b></font> -<font color="#4E9A06">INFO</font>: Started server process [<font color="#06989A">2265873</font>] -<font color="#4E9A06">INFO</font>: Waiting for application startup. -<font color="#4E9A06">INFO</font>: Application startup complete. + <span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000/docs</u></font> + + <span style="background-color:#007166"><font color="#D3D7CF"> tip </font></span> Running in development mode, for production use: + <b>fastapi run</b> + + Logs: + + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Will watch for changes in these directories: + <b>[</b><font color="#4E9A06">'/home/user/code/awesomeapp'</font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> <b>(</b>Press CTRL+C + to quit<b>)</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started reloader process <b>[</b><font color="#34E2E2"><b>383138</b></font><b>]</b> using WatchFiles + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>383153</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. ``` </div> -У консолі буде рядок приблизно такого змісту: +У виводі є рядок приблизно такого змісту: ```hl_lines="4" INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` -Цей рядок показує URL, за яким додаток запускається на вашій локальній машині. +Цей рядок показує URL, за яким ваш застосунок обслуговується на вашій локальній машині. -### Перевірте +### Перевірте { #check-it } Відкрийте браузер та введіть адресу <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>. -Ви побачите у відповідь таке повідомлення у форматі JSON: +Ви побачите JSON-відповідь: ```JSON {"message": "Hello World"} ``` -### Інтерактивна API документація +### Інтерактивна API документація { #interactive-api-docs } -Перейдемо сюди <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. +Тепер перейдіть сюди <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. -Ви побачите автоматичну інтерактивну API документацію (створену завдяки <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>): +Ви побачите автоматичну інтерактивну API документацію (надається <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Альтернативна API документація +### Альтернативна API документація { #alternative-api-docs } -Тепер перейдемо сюди <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. +А тепер перейдіть сюди <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. -Ви побачите альтернативну автоматичну документацію (створену завдяки <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>): +Ви побачите альтернативну автоматичну документацію (надається <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -### OpenAPI +### OpenAPI { #openapi } -**FastAPI** генерує "схему" з усім вашим API, використовуючи стандарт **OpenAPI** для визначення API. +**FastAPI** генерує «схему» з усім вашим API, використовуючи стандарт **OpenAPI** для визначення API. -#### "Схема" +#### «Схема» { #schema } -"Схема" - це визначення або опис чогось. Це не код, який його реалізує, а просто абстрактний опис. +«Схема» — це визначення або опис чогось. Це не код, який його реалізує, а просто абстрактний опис. -#### API "схема" +#### API «схема» { #api-schema } У цьому випадку, <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> є специфікацією, яка визначає, як описати схему вашого API. -Це визначення схеми включає шляхи (paths) вашого API, можливі параметри, які вони приймають тощо. +Це визначення схеми включає шляхи (paths) вашого API, можливі параметри, які вони приймають, тощо. -#### "Схема" даних +#### «Схема» даних { #data-schema } -Термін "схема" також може відноситися до структури даних, наприклад, JSON. +Термін «схема» також може відноситися до форми деяких даних, наприклад, вмісту JSON. -У цьому випадку це означає - атрибути JSON і типи даних, які вони мають тощо. +У цьому випадку це означає атрибути JSON і типи даних, які вони мають, тощо. -#### OpenAPI і JSON Schema +#### OpenAPI і JSON Schema { #openapi-and-json-schema } -OpenAPI описує схему для вашого API. І ця схема включає визначення (або "схеми") даних, що надсилаються та отримуються вашим API за допомогою **JSON Schema**, стандарту для схем даних JSON. +OpenAPI описує схему API для вашого API. І ця схема включає визначення (або «схеми») даних, що надсилаються та отримуються вашим API, за допомогою **JSON Schema**, стандарту для схем даних JSON. -#### Розглянемо `openapi.json` +#### Перевірте `openapi.json` { #check-the-openapi-json } -Якщо вас цікавить, як виглядає вихідна схема OpenAPI, то FastAPI автоматично генерує JSON-схему з усіма описами API. +Якщо вас цікавить, як виглядає «сирий» OpenAPI schema, FastAPI автоматично генерує JSON (schema) з описами всього вашого API. -Ознайомитися можна за посиланням: <a href="http://127.0.0.1:8000/openapi.json" class="external-link" target="_blank">http://127.0.0.1:8000/openapi.json</a>. +Ви можете побачити це напряму тут: <a href="http://127.0.0.1:8000/openapi.json" class="external-link" target="_blank">http://127.0.0.1:8000/openapi.json</a>. -Ви побачите приблизно такий JSON: +Ви побачите JSON, що починається приблизно так: ```JSON { @@ -143,42 +135,79 @@ OpenAPI описує схему для вашого API. І ця схема вк ... ``` -#### Для чого потрібний OpenAPI +#### Для чого потрібний OpenAPI { #what-is-openapi-for } -Схема OpenAPI є основою для обох систем інтерактивної документації. +OpenAPI schema — це те, на чому працюють дві включені системи інтерактивної документації. -Існують десятки альтернативних інструментів, заснованих на OpenAPI. Ви можете легко додати будь-який з них до **FastAPI** додатку. +Також існують десятки альтернатив, і всі вони засновані на OpenAPI. Ви можете легко додати будь-яку з цих альтернатив до вашого застосунку, створеного з **FastAPI**. -Ви також можете використовувати OpenAPI для автоматичної генерації коду для клієнтів, які взаємодіють з API. Наприклад, для фронтенд-, мобільних або IoT-додатків +Ви також можете використовувати його для автоматичної генерації коду для клієнтів, які взаємодіють з вашим API. Наприклад, для фронтенд-, мобільних або IoT-застосунків. -## А тепер крок за кроком +### Розгорніть ваш застосунок (необовʼязково) { #deploy-your-app-optional } -### Крок 1: імпортуємо `FastAPI` +За бажанням ви можете розгорнути ваш FastAPI-застосунок у <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, перейдіть і приєднайтеся до списку очікування, якщо ви цього ще не зробили. 🚀 -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +Якщо у вас вже є обліковий запис **FastAPI Cloud** (ми запросили вас зі списку очікування 😉), ви можете розгорнути ваш застосунок однією командою. -`FastAPI` це клас у Python, який надає всю функціональність для API. +Перед розгортанням переконайтеся, що ви увійшли: + +<div class="termy"> + +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +</div> + +Потім розгорніть ваш застосунок: + +<div class="termy"> + +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +</div> + +Ось і все! Тепер ви можете отримати доступ до вашого застосунку за цим URL. ✨ + +## Підібʼємо підсумки, крок за кроком { #recap-step-by-step } + +### Крок 1: імпортуємо `FastAPI` { #step-1-import-fastapi } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} + +`FastAPI` — це клас у Python, який надає всю функціональність для вашого API. /// note | Технічні деталі -`FastAPI` це клас, який успадковується безпосередньо від `Starlette`. +`FastAPI` — це клас, який успадковується безпосередньо від `Starlette`. Ви також можете використовувати всю функціональність <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a> у `FastAPI`. /// -### Крок 2: створюємо екземпляр `FastAPI` +### Крок 2: створюємо «екземпляр» `FastAPI` { #step-2-create-a-fastapi-instance } -{* ../../docs_src/first_steps/tutorial001.py hl[3] *} -Змінна `app` є екземпляром класу `FastAPI`. +{* ../../docs_src/first_steps/tutorial001_py39.py hl[3] *} -Це буде головна точка для створення і взаємодії з API. +Тут змінна `app` буде «екземпляром» класу `FastAPI`. -### Крок 3: визначте операцію шляху (path operation) +Це буде головна точка взаємодії для створення всього вашого API. -#### Шлях (path) +### Крок 3: створіть *операцію шляху* { #step-3-create-a-path-operation } -"Шлях" це частина URL, яка йде одразу після символу `/`. +#### Шлях { #path } + +«Шлях» тут означає останню частину URL, починаючи з першого `/`. Отже, у такому URL, як: @@ -192,16 +221,17 @@ https://example.com/items/foo /items/foo ``` -/// info | Додаткова інформація +/// info | Інформація -"Шлях" (path) також зазвичай називають "ендпоінтом" (endpoint) або "маршрутом" (route). +«Шлях» також зазвичай називають «ендпоінтом» або «маршрутом». /// -При створенні API, "шлях" є основним способом розділення "завдань" і "ресурсів". -#### Operation +Під час створення API «шлях» є основним способом розділити «завдання» і «ресурси». -"Операція" (operation) тут означає один з "методів" HTTP. +#### Операція { #operation } + +«Операція» тут означає один з HTTP «методів». Один з: @@ -217,46 +247,47 @@ https://example.com/items/foo * `PATCH` * `TRACE` -У HTTP-протоколі можна спілкуватися з кожним шляхом, використовуючи один (або кілька) з цих "методів". +У протоколі HTTP ви можете спілкуватися з кожним шляхом, використовуючи один (або кілька) з цих «методів». --- -При створенні API зазвичай використовуються конкретні методи HTTP для виконання певних дій. +Під час створення API зазвичай використовують ці конкретні HTTP методи, щоб виконати певну дію. -Як правило, використовують: +Зазвичай використовують: -* `POST`: для створення даних. -* `GET`: для читання даних. -* `PUT`: для оновлення даних. -* `DELETE`: для видалення даних. +* `POST`: щоб створити дані. +* `GET`: щоб читати дані. +* `PUT`: щоб оновити дані. +* `DELETE`: щоб видалити дані. -В OpenAPI кожен HTTP метод називається "операція". +Отже, в OpenAPI кожен з HTTP методів називається «операцією». -Ми також будемо дотримуватися цього терміна. +Ми також будемо називати їх «**операціями**». -#### Визначте декоратор операції шляху (path operation decorator) +#### Визначте *декоратор операції шляху* { #define-a-path-operation-decorator } -{* ../../docs_src/first_steps/tutorial001.py hl[6] *} -Декоратор `@app.get("/")` вказує **FastAPI**, що функція нижче, відповідає за обробку запитів, які надходять до неї: +{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *} -* шлях `/` +Декоратор `@app.get("/")` повідомляє **FastAPI**, що функція одразу нижче відповідає за обробку запитів, які надходять до: + +* шляху `/` * використовуючи <abbr title="an HTTP GET method"><code>get</code> операцію</abbr> -/// info | `@decorator` Додаткова інформація +/// info | `@decorator` Інформація -Синтаксис `@something` у Python називається "декоратором". +Синтаксис `@something` у Python називається «декоратором». Ви розташовуєте його над функцією. Як гарний декоративний капелюх (мабуть, звідти походить термін). -"Декоратор" приймає функцію нижче і виконує з нею якусь дію. +«Декоратор» бере функцію нижче і виконує з нею якусь дію. У нашому випадку, цей декоратор повідомляє **FastAPI**, що функція нижче відповідає **шляху** `/` і **операції** `get`. -Це і є "декоратор операції шляху (path operation decorator)". +Це і є «**декоратор операції шляху**». /// -Можна також використовувати операції: +Можна також використовувати інші операції: * `@app.post()` * `@app.put()` @@ -271,58 +302,79 @@ https://example.com/items/foo /// tip | Порада -Ви можете використовувати кожну операцію (HTTP-метод) на свій розсуд. +Ви можете використовувати кожну операцію (HTTP-метод) як забажаєте. -**FastAPI** не нав'язує жодного певного значення для кожного методу. +**FastAPI** не навʼязує жодного конкретного значення. -Наведена тут інформація є рекомендацією, а не обов'язковою вимогою. +Наведена тут інформація подана як настанова, а не вимога. -Наприклад, під час використання GraphQL зазвичай усі дії виконуються тільки за допомогою `POST` операцій. +Наприклад, під час використання GraphQL ви зазвичай виконуєте всі дії, використовуючи лише `POST` операції. /// -### Крок 4: визначте **функцію операції шляху (path operation function)** +### Крок 4: визначте **функцію операції шляху** { #step-4-define-the-path-operation-function } -Ось "**функція операції шляху**": +Ось наша «**функція операції шляху**»: * **шлях**: це `/`. * **операція**: це `get`. -* **функція**: це функція, яка знаходиться нижче "декоратора" (нижче `@app.get("/")`). +* **функція**: це функція нижче «декоратора» (нижче `@app.get("/")`). -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} -Це звичайна функція Python. +Це функція Python. -FastAPI викликатиме її щоразу, коли отримає запит до URL із шляхом "/", використовуючи операцію `GET`. +**FastAPI** викликатиме її щоразу, коли отримає запит до URL «`/`», використовуючи операцію `GET`. -У даному випадку це асинхронна функція. +У цьому випадку це `async` функція. --- Ви також можете визначити її як звичайну функцію замість `async def`: -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note | Примітка -Якщо не знаєте в чому різниця, подивіться [Конкурентність: *"Поспішаєш?"*](../async.md#in-a-hurry){.internal-link target=_blank}. +Якщо ви не знаєте різницю, подивіться [Асинхронність: *«Поспішаєте?»*](../async.md#in-a-hurry){.internal-link target=_blank}. /// -### Крок 5: поверніть результат +### Крок 5: поверніть вміст { #step-5-return-the-content } -{* ../../docs_src/first_steps/tutorial001.py hl[8] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *} -Ви можете повернути `dict`, `list`, а також окремі значення `str`, `int`, ітд. +Ви можете повернути `dict`, `list`, а також окремі значення `str`, `int` тощо. Також можна повернути моделі Pydantic (про це ви дізнаєтесь пізніше). -Існує багато інших об'єктів і моделей, які будуть автоматично конвертовані в JSON (зокрема ORM тощо). Спробуйте використати свої улюблені, велика ймовірність, що вони вже підтримуються. +Існує багато інших обʼєктів і моделей, які будуть автоматично конвертовані в JSON (зокрема ORM тощо). Спробуйте використати свої улюблені — велика ймовірність, що вони вже підтримуються. -## Підіб'ємо підсумки +### Крок 6: розгорніть його { #step-6-deploy-it } -* Імпортуємо `FastAPI`. -* Створюємо екземпляр `app`. -* Пишемо **декоратор операції шляху** як `@app.get("/")`. -* Пишемо **функцію операції шляху**; наприклад, `def root(): ...`. -* Запускаємо сервер у режимі розробки `fastapi dev`. +Розгорніть ваш застосунок у **<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** однією командою: `fastapi deploy`. 🎉 + +#### Про FastAPI Cloud { #about-fastapi-cloud } + +**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** створено тим самим автором і командою, які стоять за **FastAPI**. + +Він спрощує процес **створення**, **розгортання** та **доступу** до API з мінімальними зусиллями. + +Він переносить той самий **досвід розробника** зі створення застосунків на FastAPI на **розгортання** їх у хмарі. 🎉 + +FastAPI Cloud — основний спонсор і джерело фінансування для open source проєктів *FastAPI and friends*. ✨ + +#### Розгортання в інших хмарних провайдерах { #deploy-to-other-cloud-providers } + +FastAPI — це open source і базується на стандартах. Ви можете розгортати FastAPI-застосунки у будь-якого хмарного провайдера на ваш вибір. + +Дотримуйтеся інструкцій вашого хмарного провайдера, щоб розгорнути FastAPI-застосунки з їхньою допомогою. 🤓 + +## Підібʼємо підсумки { #recap } + +* Імпортуйте `FastAPI`. +* Створіть екземпляр `app`. +* Напишіть **декоратор операції шляху**, використовуючи декоратори на кшталт `@app.get("/")`. +* Визначте **функцію операції шляху**; наприклад, `def root(): ...`. +* Запустіть сервер розробки командою `fastapi dev`. +* За бажанням розгорніть ваш застосунок за допомогою `fastapi deploy`. diff --git a/docs/uk/docs/tutorial/handling-errors.md b/docs/uk/docs/tutorial/handling-errors.md index 32de73b2a..53b8b12f6 100644 --- a/docs/uk/docs/tutorial/handling-errors.md +++ b/docs/uk/docs/tutorial/handling-errors.md @@ -1,10 +1,10 @@ -# Обробка Помилок +# Обробка помилок { #handling-errors } -Є багато ситуацій, коли потрібно повідомити клієнта, який використовує Ваш API, про помилку. +Є багато ситуацій, коли вам потрібно повідомити про помилку клієнта, який використовує ваш API. Цим клієнтом може бути браузер із фронтендом, код іншого розробника, IoT-пристрій тощо. -Можливо, Вам потрібно повідомити клієнта, що: +Можливо, вам потрібно повідомити клієнта, що: * У нього недостатньо прав для виконання цієї операції. * Він не має доступу до цього ресурсу. @@ -13,37 +13,37 @@ У таких випадках зазвичай повертається **HTTP статус-код** в діапазоні **400** (від 400 до 499). -Це схоже на HTTP статус-коди 200 (від 200 до 299). Ці "200" статус-коди означають, що запит пройшов успішно. +Це схоже на HTTP статус-коди 200 (від 200 до 299). Ці «200» статус-коди означають, що якимось чином запит був «успішним». Статус-коди в діапазоні 400 означають, що сталася помилка з боку клієнта. -Пам'ятаєте всі ці помилки **404 Not Found** (і жарти про них)? +Пам'ятаєте всі ці помилки **«404 Not Found»** (і жарти про них)? -## Використання `HTTPException` +## Використання `HTTPException` { #use-httpexception } Щоб повернути HTTP-відповіді з помилками клієнту, використовуйте `HTTPException`. -### Імпорт `HTTPException` +### Імпорт `HTTPException` { #import-httpexception } -{* ../../docs_src/handling_errors/tutorial001.py hl[1] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[1] *} -### Використання `HTTPException` у коді +### Згенеруйте `HTTPException` у своєму коді { #raise-an-httpexception-in-your-code } `HTTPException` — це звичайна помилка Python із додатковими даними, які стосуються API. -Оскільки це помилка Python, Ви не `повертаєте` його, а `генеруєте` (генеруєте помилку). +Оскільки це помилка Python, ви не `return` її, а `raise` її. -Це також означає, що якщо Ви перебуваєте всередині допоміжної функції, яку викликаєте всередині своєї *функції операції шляху*, і там генеруєте `HTTPException`, всередині цієї допоміжної функції, то решта коду в *функції операції шляху* не буде виконана. Запит одразу завершиться, і HTTP-помилка з `HTTPException` буде надіслана клієнту. +Це також означає, що якщо ви перебуваєте всередині допоміжної функції, яку викликаєте всередині своєї *функції операції шляху*, і там згенеруєте `HTTPException` всередині цієї допоміжної функції, то решта коду в *функції операції шляху* не буде виконана. Запит одразу завершиться, і HTTP-помилка з `HTTPException` буде надіслана клієнту. -Перевага використання `генерації` (raise) помилки замість `повернення` значення (return) стане більш очевидним в розділі про Залежності та Безпеку. +Перевага генерації виключення замість повернення значення стане більш очевидною в розділі про залежності та безпеку. -У цьому прикладі, якщо клієнт запитує елемент за ID, якого не існує, буде згенеровано помилку зі статус-кодом `404`: +У цьому прикладі, коли клієнт запитує елемент за ID, якого не існує, згенеруйте виключення зі статус-кодом `404`: -{* ../../docs_src/handling_errors/tutorial001.py hl[11] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[11] *} -### Отримана відповідь +### Отримана відповідь { #the-resulting-response } -Якщо клієнт робить запит за шляхом `http://example.com/items/foo` (де `item_id` `"foo"`), він отримає статус-код 200 і JSON відповідь: +Якщо клієнт робить запит за шляхом `http://example.com/items/foo` (де `item_id` `"foo"`), він отримає HTTP статус-код 200 і JSON відповідь: ```JSON { @@ -51,7 +51,7 @@ } ``` -Але якщо клієнт робить запит на `http://example.com/items/bar` (де `item_id` має не існуюче значення `"bar"`), то отримає статус-код 404 (помилка "не знайдено") та відповідь: +Але якщо клієнт робить запит на `http://example.com/items/bar` (де `item_id` має не існуюче значення `"bar"`), то отримає HTTP статус-код 404 (помилка «не знайдено») та JSON відповідь: ```JSON { @@ -61,7 +61,7 @@ /// tip | Порада -Під час виклику `HTTPException` Ви можете передати будь-яке значення, яке може бути перетворене в JSON, як параметр `detail`, а не лише рядок (`str`). +Під час генерації `HTTPException` ви можете передати будь-яке значення, яке може бути перетворене в JSON, як параметр `detail`, а не лише `str`. Ви можете передати `dict`, `list` тощо. @@ -69,33 +69,33 @@ /// -## Додавання власних заголовків +## Додавання власних заголовків { #add-custom-headers } -Іноді потрібно додати власні заголовки до HTTP-помилки, наприклад, для певних типів безпеки. +Є деякі ситуації, коли корисно мати можливість додавати власні заголовки до HTTP-помилки. Наприклад, для деяких типів безпеки. -Ймовірно, Вам не доведеться використовувати це безпосередньо у своєму коді. +Ймовірно, вам не доведеться використовувати це безпосередньо у своєму коді. -Але якщо Вам знадобиться це для складного сценарію, Ви можете додати власні заголовки: +Але якщо вам знадобиться це для складного сценарію, ви можете додати власні заголовки: -{* ../../docs_src/handling_errors/tutorial002.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *} -## Встановлення власних обробників помилок +## Встановлення власних обробників виключень { #install-custom-exception-handlers } -Ви можете додати власні обробники помилок за допомогою <a href="https://www.starlette.dev/exceptions/" class="external-link" target="_blank">тих самих утиліт обробки помилок зі Starlette</a>. +Ви можете додати власні обробники виключень за допомогою <a href="https://www.starlette.dev/exceptions/" class="external-link" target="_blank">тих самих утиліт для виключень зі Starlette</a>. -Припустимо, у Вас є власний обʼєкт помилки `UnicornException`, яке Ви (або бібліотека, яку Ви використовуєте) може `згенерувати` (`raise`). +Припустімо, у вас є власне виключення `UnicornException`, яке ви (або бібліотека, яку ви використовуєте) можете `raise`. -І Ви хочете обробляти це виключення глобально за допомогою FastAPI. +І ви хочете обробляти це виключення глобально за допомогою FastAPI. Ви можете додати власний обробник виключень за допомогою `@app.exception_handler()`: -{* ../../docs_src/handling_errors/tutorial003.py hl[5:7,13:18,24] *} +{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *} -Тут, якщо Ви звернетеся до `/unicorns/yolo`, то згенерується помилка `UnicornException`. +Тут, якщо ви звернетеся до `/unicorns/yolo`, *операція шляху* згенерує (`raise`) `UnicornException`. Але вона буде оброблена функцією-обробником `unicorn_exception_handler`. -Отже, Ви отримаєте зрозумілу помилку зі HTTP-статусом `418` і JSON-відповіддю: +Отже, ви отримаєте зрозумілу помилку зі HTTP-статусом `418` і JSON-вмістом: ```JSON {"message": "Oops! yolo did something. There goes a rainbow..."} @@ -105,31 +105,31 @@ Ви також можете використовувати `from starlette.requests import Request` і `from starlette.responses import JSONResponse`. -**FastAPI** надає ті самі `starlette.responses`, що й `fastapi.responses`, просто для зручності розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette. Те ж саме стосується і `Request`. +**FastAPI** надає ті самі `starlette.responses`, що й `fastapi.responses`, просто для зручності для вас, розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette. Те ж саме з `Request`. /// -## Перевизначення обробників помилок за замовчуванням +## Перевизначення обробників виключень за замовчуванням { #override-the-default-exception-handlers } -**FastAPI** має кілька обробників помилок за замовчуванням. +**FastAPI** має кілька обробників виключень за замовчуванням. -Ці обробники відповідають за повернення стандартних JSON-відповідей, коли Ви `генеруєте` (`raise`) `HTTPException`, а також коли запит містить некоректні дані. +Ці обробники відповідають за повернення стандартних JSON-відповідей, коли ви `raise` `HTTPException`, а також коли запит містить некоректні дані. -Ви можете перевизначити ці обробники, створивши власні. +Ви можете перевизначити ці обробники виключень власними. -### Перевизначення помилок валідації запиту +### Перевизначення виключень валідації запиту { #override-request-validation-exceptions } -Коли запит містить некоректні дані, **FastAPI** генерує `RequestValidationError`. +Коли запит містить некоректні дані, **FastAPI** внутрішньо генерує `RequestValidationError`. -І також включає обробник помилок за замовчуванням для нього. +І також включає обробник виключень за замовчуванням для нього. -Щоб перевизначити його, імпортуйте `RequestValidationError` і використовуйте його з `@app.exception_handler(RequestValidationError)` для декорування обробника помилок. +Щоб перевизначити його, імпортуйте `RequestValidationError` і використовуйте його з `@app.exception_handler(RequestValidationError)` для декорування обробника виключень. -Обробник помилок отримує `Request` і саму помилку. +Обробник виключень отримає `Request` і саме виключення. -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *} -Тепер, якщо Ви перейдете за посиланням `/items/foo`, замість того, щоб отримати стандартну JSON-помилку: +Тепер, якщо ви перейдете за посиланням `/items/foo`, замість того, щоб отримати стандартну JSON-помилку: ```JSON { @@ -146,55 +146,44 @@ } ``` -Ви отримаєте текстову версію: +ви отримаєте текстову версію: ``` -1 validation error -path -> item_id - value is not a valid integer (type=type_error.integer) +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer ``` -#### `RequestValidationError` проти `ValidationError` +### Перевизначення обробника помилок `HTTPException` { #override-the-httpexception-error-handler } -/// warning | Увага +Аналогічно, ви можете перевизначити обробник `HTTPException`. -Це технічні деталі, які Ви можете пропустити, якщо вони зараз не важливі для Вас. +Наприклад, ви можете захотіти повернути відповідь у вигляді простого тексту замість JSON для цих помилок: -/// - -`RequestValidationError` є підкласом Pydantic <a href="https://docs.pydantic.dev/latest/concepts/models/#error-handling" class="external-link" target="_blank">`ValidationError`</a>. - -**FastAPI** використовує його для того, якщо Ви використовуєте модель Pydantic у `response_model` і у ваших даних є помилка, Ви побачили помилку у своєму журналі. - -Але клієнт/користувач не побачить її. Натомість клієнт отримає "Internal Server Error" зі статусом HTTP `500`. - -Так має бути, якщо у Вас виникла `ValidationError` Pydantic у *відповіді* або деінде у вашому коді (не у *запиті* клієнта), це насправді є помилкою у Вашому коді. - -І поки Ви її виправляєте, клієнти/користувачі не повинні мати доступу до внутрішньої інформації про помилку, оскільки це може призвести до вразливості безпеки. - -### Перевизначення обробника помилок `HTTPException` - -Аналогічно, Ви можете перевизначити обробник `HTTPException`. - -Наприклад, Ви можете захотіти повернути текстову відповідь замість JSON для цих помилок: - -{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *} /// note | Технічні деталі Ви також можете використовувати `from starlette.responses import PlainTextResponse`. -**FastAPI** надає ті самі `starlette.responses`, що й `fastapi.responses`, просто для зручності розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette. +**FastAPI** надає ті самі `starlette.responses`, що й `fastapi.responses`, просто для зручності для вас, розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette. /// -### Використання тіла `RequestValidationError` +/// warning | Попередження + +Пам’ятайте, що `RequestValidationError` містить інформацію про назву файлу та рядок, де сталася помилка валідації, щоб за потреби ви могли показати це у своїх логах із відповідною інформацією. + +Але це означає, що якщо ви просто перетворите це на рядок і повернете цю інформацію напряму, ви можете розкрити трохи інформації про вашу систему, тому тут код витягає та показує кожну помилку незалежно. + +/// + +### Використання тіла `RequestValidationError` { #use-the-requestvalidationerror-body } `RequestValidationError` містить `body`, який він отримав із некоректними даними. Ви можете використовувати це під час розробки свого додатка, щоб логувати тіло запиту та налагоджувати його, повертати користувачеві тощо. -{* ../../docs_src/handling_errors/tutorial005.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *} Тепер спробуйте надіслати некоректний елемент, наприклад: @@ -204,8 +193,8 @@ path -> item_id "size": "XL" } ``` -Ви отримаєте відповідь, яка повідомить Вам, які саме дані є некоректні у вашому тілі запиту: +Ви отримаєте відповідь, яка повідомить вам, що дані є некоректними, і міститиме отримане тіло запиту: ```JSON hl_lines="12-15" { @@ -226,30 +215,30 @@ path -> item_id } ``` -#### `HTTPException` FastAPI проти `HTTPException` Starlette +#### `HTTPException` FastAPI проти `HTTPException` Starlette { #fastapis-httpexception-vs-starlettes-httpexception } **FastAPI** має власний `HTTPException`. -І клас помилки `HTTPException` в **FastAPI** успадковується від класу помилки `HTTPException` в Starlette. +І клас помилки `HTTPException` в **FastAPI** успадковується від класу помилки `HTTPException` у Starlette. Єдина різниця полягає в тому, що `HTTPException` в **FastAPI** приймає будь-які дані, які можна перетворити на JSON, для поля `detail`, тоді як `HTTPException` у Starlette приймає тільки рядки. -Отже, Ви можете продовжувати використовувати `HTTPException` в **FastAPI** як зазвичай у своєму коді. +Отже, ви можете продовжувати генерувати `HTTPException` **FastAPI** як зазвичай у своєму коді. -Але коли Ви реєструєте обробник виключень, слід реєструвати його для `HTTPException` зі Starlette. +Але коли ви реєструєте обробник виключень, слід реєструвати його для `HTTPException` зі Starlette. -Таким чином, якщо будь-яка частина внутрішнього коду Starlette або розширення чи плагін Starlette згенерує (raise) `HTTPException`, Ваш обробник зможе перехопити та обробити її. +Таким чином, якщо будь-яка частина внутрішнього коду Starlette або розширення чи плагін Starlette згенерує Starlette `HTTPException`, ваш обробник зможе перехопити та обробити її. -У цьому прикладі, щоб мати можливість використовувати обидва `HTTPException` в одному коді, помилка Starlette перейменовується на `StarletteHTTPException`: +У цьому прикладі, щоб мати можливість використовувати обидва `HTTPException` в одному коді, виключення Starlette перейменовується на `StarletteHTTPException`: ```Python from starlette.exceptions import HTTPException as StarletteHTTPException ``` -### Повторне використання обробників помилок **FastAPI** +### Повторне використання обробників виключень **FastAPI** { #reuse-fastapis-exception-handlers } -Якщо Ви хочете використовувати помилки разом із такими ж обробниками помилок за замовчуванням, як у **FastAPI**, Ви можете імпортувати та повторно використовувати їх із `fastapi.exception_handlers`: +Якщо ви хочете використовувати виключення разом із такими ж обробниками виключень за замовчуванням, як у **FastAPI**, ви можете імпортувати та повторно використати обробники виключень за замовчуванням із `fastapi.exception_handlers`: -{* ../../docs_src/handling_errors/tutorial006.py hl[2:5,15,21] *} +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} -У цьому прикладі Ви просто використовуєте `print` для виведення дуже інформативного повідомлення, але Ви зрозуміли основну ідею. Ви можете обробити помилку та повторно використовувати обробники помилок за замовчуванням. +У цьому прикладі ви просто друкуєте помилку з дуже виразним повідомленням, але ви зрозуміли основну ідею. Ви можете використовувати виключення, а потім просто повторно використати обробники виключень за замовчуванням. diff --git a/docs/uk/docs/tutorial/header-param-models.md b/docs/uk/docs/tutorial/header-param-models.md index 6f7b0bdae..c080c19f0 100644 --- a/docs/uk/docs/tutorial/header-param-models.md +++ b/docs/uk/docs/tutorial/header-param-models.md @@ -1,26 +1,24 @@ -# Моделі Параметрів Заголовків +# Моделі параметрів заголовків { #header-parameter-models } -Якщо у Вас є група пов’язаних параметрів заголовків, Ви можете створити **Pydantic модель** для їх оголошення. +Якщо у Вас є група пов’язаних **параметрів заголовків**, Ви можете створити **Pydantic модель** для їх оголошення. Це дозволить Вам повторно **використовувати модель** в **різних місцях**, а також оголосити валідації та метадані для всіх параметрів одночасно. 😎 -/// note | Нотатки +/// note | Примітка Ця можливість підтримується починаючи з версії FastAPI `0.115.0`. 🤓 /// -## Параметри Заголовків з Використанням Pydantic Model +## Параметри заголовків з використанням Pydantic моделі { #header-parameters-with-a-pydantic-model } Оголосіть потрібні **параметри заголовків** у **Pydantic моделі**, а потім оголосіть параметр як `Header`: {* ../../docs_src/header_param_models/tutorial001_an_py310.py hl[9:14,18] *} -FastAPI буде витягувати дані для кожного поля з заголовків у запиті та передавати їх у створену Вами Pydantic модель. - **FastAPI** буде **витягувати** дані для **кожного поля** з **заголовків** у запиті та передавати їх у створену Вами Pydantic модель. -## Перевірка в Документації +## Перевірка в документації { #check-the-docs } Ви можете побачити необхідні заголовки в інтерфейсі документації за адресою `/docs`: @@ -28,7 +26,7 @@ FastAPI буде витягувати дані для кожного поля з <img src="/img/tutorial/header-param-models/image01.png"> </div> -## Заборона Додаткових Заголовків +## Заборонити додаткові заголовки { #forbid-extra-headers } У деяких особливих випадках (ймовірно, не дуже поширених) Ви можете захотіти **обмежити** заголовки, які хочете отримати. @@ -38,7 +36,7 @@ FastAPI буде витягувати дані для кожного поля з Якщо клієнт спробує надіслати **додаткові заголовки**, він отримає **помилку** у відповіді. -Наприклад, якщо клієнт спробує надіслати заголовок `tool` зі значенням `plumbus`, він отримає **помилку** з повідомленням про те, що параметр заголовка `tool` не дозволений: +Наприклад, якщо клієнт спробує надіслати заголовок `tool` зі значенням `plumbus`, він отримає **помилку** у відповіді з повідомленням про те, що параметр заголовка `tool` не дозволений: ```json { @@ -53,6 +51,22 @@ FastAPI буде витягувати дані для кожного поля з } ``` -## Підсумок +## Вимкнути перетворення підкреслень { #disable-convert-underscores } + +Так само, як і зі звичайними параметрами заголовків, коли у назвах параметрів є символи підкреслення, вони **автоматично перетворюються на дефіси**. + +Наприклад, якщо у коді у Вас є параметр заголовка `save_data`, очікуваний HTTP-заголовок буде `save-data`, і він так само відображатиметься в документації. + +Якщо з якоїсь причини Вам потрібно вимкнути це автоматичне перетворення, Ви також можете зробити це для Pydantic моделей для параметрів заголовків. + +{* ../../docs_src/header_param_models/tutorial003_an_py310.py hl[19] *} + +/// warning | Попередження + +Перш ніж встановлювати `convert_underscores` у значення `False`, пам’ятайте, що деякі HTTP проксі та сервери забороняють використання заголовків із підкресленнями. + +/// + +## Підсумок { #summary } Ви можете використовувати **Pydantic моделі** для оголошення **заголовків** у **FastAPI**. 😎 diff --git a/docs/uk/docs/tutorial/header-params.md b/docs/uk/docs/tutorial/header-params.md index 09c70a4f6..f5a4ea18d 100644 --- a/docs/uk/docs/tutorial/header-params.md +++ b/docs/uk/docs/tutorial/header-params.md @@ -1,14 +1,14 @@ -# Header-параметри +# Параметри заголовків { #header-parameters } -Ви можете визначати параметри заголовків, так само як визначаєте `Query`, `Path` і `Cookie` параметри. +Ви можете визначати параметри заголовків так само, як визначаєте параметри `Query`, `Path` і `Cookie`. -## Імпорт `Header` +## Імпорт `Header` { #import-header } Спочатку імпортуйте `Header`: {* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *} -## Оголошення параметрів `Header` +## Оголошення параметрів `Header` { #declare-header-parameters } Потім оголосіть параметри заголовків, використовуючи ту ж структуру, що й для `Path`, `Query` та `Cookie`. @@ -18,9 +18,9 @@ /// note | Технічні деталі -`Header`є "сестринським" класом для `Path`, `Query` і `Cookie`. Він також успадковується від загального класу `Param`. +`Header` є «сестринським» класом для `Path`, `Query` і `Cookie`. Він також успадковується від того ж спільного класу `Param`. -Але пам’ятайте, що при імпорті `Query`, `Path`, `Header` та інших із `fastapi`, то насправді вони є функціями, які повертають спеціальні класи. +Але пам’ятайте, що коли ви імпортуєте `Query`, `Path`, `Header` та інші з `fastapi`, то насправді це функції, які повертають спеціальні класи. /// @@ -30,43 +30,43 @@ /// -## Автоматичне перетворення +## Автоматичне перетворення { #automatic-conversion } -`Header` має додатковий функціонал порівняно з `Path`, `Query` та `Cookie`. +`Header` має трохи додаткової функціональності порівняно з тим, що надають `Path`, `Query` та `Cookie`. -Більшість стандартних заголовків розділяються символом «дефіс», також відомим як «мінус» (`-`). +Більшість стандартних заголовків розділяються символом «дефіс», також відомим як «символ мінуса» (`-`). Але змінна, така як `user-agent`, є недійсною в Python. -Тому, за замовчуванням, `Header` автоматично перетворює символи підкреслення (`_`) на дефіси (`-`) для отримання та документування заголовків. +Тому, за замовчуванням, `Header` перетворюватиме символи підкреслення (`_`) у назвах параметрів на дефіси (`-`), щоб отримувати та документувати заголовки. -Оскільки заголовки HTTP не чутливі до регістру, Ви можете використовувати стандартний стиль Python ("snake_case"). +Також заголовки HTTP не чутливі до регістру, тож ви можете оголошувати їх у стандартному стилі Python (також відомому як «snake_case»). -Тому Ви можете використовувати `user_agent`, як зазвичай у коді Python, замість того щоб писати з великої літери, як `User_Agent` або щось подібне. +Тому ви можете використовувати `user_agent`, як зазвичай у коді Python, замість того щоб потрібно було писати з великої літери перші літери, як `User_Agent` або щось подібне. -Якщо Вам потрібно вимкнути автоматичне перетворення підкреслень у дефіси, встановіть `convert_underscores` в `Header` значення `False`: +Якщо з якоїсь причини вам потрібно вимкнути автоматичне перетворення підкреслень на дефіси, встановіть параметр `convert_underscores` у `Header` в `False`: {* ../../docs_src/header_params/tutorial002_an_py310.py hl[10] *} -/// warning | Увага +/// warning | Попередження -Перед тим як встановити значення `False` для `convert_underscores` пам’ятайте, що деякі HTTP-проксі та сервери не підтримують заголовки з підкресленнями. +Перед тим як встановити `convert_underscores` в `False`, пам’ятайте, що деякі HTTP-проксі та сервери забороняють використання заголовків із підкресленнями. /// -## Дубльовані заголовки +## Дубльовані заголовки { #duplicate-headers } -Можливо отримати дубльовані заголовки, тобто той самий заголовок із кількома значеннями. +Можливо отримати дубльовані заголовки. Тобто один і той самий заголовок із кількома значеннями. -Це можна визначити, використовуючи список у типізації параметра. +Ви можете визначити такі випадки, використовуючи список у типізації. -Ви отримаєте всі значення дубльованого заголовка у вигляді `list` у Python. +Ви отримаєте всі значення дубльованого заголовка у вигляді Python-`list`. -Наприклад, щоб оголосити заголовок `X-Token`, який може з’являтися більше ніж один раз: +Наприклад, щоб оголосити заголовок `X-Token`, який може з’являтися більше ніж один раз, ви можете написати: {* ../../docs_src/header_params/tutorial003_an_py310.py hl[9] *} -Якщо Ви взаємодієте з цією операцією шляху, надсилаючи два HTTP-заголовки, наприклад: +Якщо ви взаємодієте з цією *операцією шляху*, надсилаючи два HTTP-заголовки, наприклад: ``` X-Token: foo @@ -84,8 +84,8 @@ X-Token: bar } ``` -## Підсумок +## Підсумок { #recap } -Оголошуйте заголовки за допомогою `Header`, використовуючи той самий підхід, що й для `Query`, `Path` та `Cookie`. +Оголошуйте заголовки за допомогою `Header`, використовуючи той самий загальний підхід, що й для `Query`, `Path` та `Cookie`. -Не хвилюйтеся про підкреслення у змінних — **FastAPI** автоматично конвертує їх. +І не хвилюйтеся про підкреслення у ваших змінних — **FastAPI** подбає про їх перетворення. diff --git a/docs/uk/docs/tutorial/index.md b/docs/uk/docs/tutorial/index.md index 92c3e77a3..6848090ec 100644 --- a/docs/uk/docs/tutorial/index.md +++ b/docs/uk/docs/tutorial/index.md @@ -1,29 +1,53 @@ -# Туторіал - Посібник користувача +# Туторіал - Посібник користувача { #tutorial-user-guide } У цьому посібнику показано, як користуватися **FastAPI** з більшістю його функцій, крок за кроком. Кожен розділ поступово надбудовується на попередні, але він структурований на окремі теми, щоб ви могли перейти безпосередньо до будь-якої конкретної, щоб вирішити ваші конкретні потреби API. -Він також створений як довідник для роботи у майбутньому. +Також він створений як довідник для роботи у майбутньому, тож ви можете повернутися і побачити саме те, що вам потрібно. -Тож ви можете повернутися і побачити саме те, що вам потрібно. - -## Запустіть код +## Запустіть код { #run-the-code } Усі блоки коду можна скопіювати та використовувати безпосередньо (це фактично перевірені файли Python). -Щоб запустити будь-який із прикладів, скопіюйте код у файл `main.py` і запустіть `uvicorn` за допомогою: +Щоб запустити будь-який із прикладів, скопіюйте код у файл `main.py` і запустіть `fastapi dev` за допомогою: <div class="termy"> ```console -$ uvicorn main:app --reload +$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:solid">main.py</u> -<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -<span style="color: green;">INFO</span>: Started reloader process [28720] -<span style="color: green;">INFO</span>: Started server process [28722] -<span style="color: green;">INFO</span>: Waiting for application startup. -<span style="color: green;">INFO</span>: Application startup complete. + <span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting development server 🚀 + + Searching for package file structure from directories + with <font color="#3465A4">__init__.py</font> files + Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py + + <span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with + the following code: + + <u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> + + <span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000/docs</u></font> + + <span style="background-color:#007166"><font color="#D3D7CF"> tip </font></span> Running in development mode, for production use: + <b>fastapi run</b> + + Logs: + + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Will watch for changes in these directories: + <b>[</b><font color="#4E9A06">'/home/user/code/awesomeapp'</font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> <b>(</b>Press CTRL+C + to quit<b>)</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started reloader process <b>[</b><font color="#34E2E2"><b>383138</b></font><b>]</b> using WatchFiles + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>383153</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. ``` </div> @@ -34,45 +58,33 @@ $ uvicorn main:app --reload --- -## Встановлення FastAPI +## Встановлення FastAPI { #install-fastapi } Першим кроком є встановлення FastAPI. -Для туторіалу ви можете встановити його з усіма необов’язковими залежностями та функціями: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім **встановіть FastAPI**: <div class="termy"> ```console -$ pip install "fastapi[all]" +$ pip install "fastapi[standard]" ---> 100% ``` </div> -...який також включає `uvicorn`, який ви можете використовувати як сервер, який запускає ваш код. +/// note | Примітка -/// note +Коли ви встановлюєте через `pip install "fastapi[standard]"`, він постачається з деякими типовими необов’язковими стандартними залежностями, включно з `fastapi-cloud-cli`, який дозволяє розгортати в <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>. -Ви також можете встановити його частина за частиною. +Якщо ви не хочете мати ці необов’язкові залежності, натомість можете встановити `pip install fastapi`. -Це те, що ви, ймовірно, зробили б, коли захочете розгорнути свою програму у виробничому середовищі: - -``` -pip install fastapi -``` - -Також встановіть `uvicorn`, щоб він працював як сервер: - -``` -pip install "uvicorn[standard]" -``` - -І те саме для кожної з опціональних залежностей, які ви хочете використовувати. +Якщо ви хочете встановити стандартні залежності, але без `fastapi-cloud-cli`, ви можете встановити через `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. /// -## Розширений посібник користувача +## Розширений посібник користувача { #advanced-user-guide } Існує також **Розширений посібник користувача**, який ви зможете прочитати пізніше після цього **Туторіал - Посібник користувача**. @@ -80,4 +92,4 @@ pip install "uvicorn[standard]" Але вам слід спочатку прочитати **Туторіал - Посібник користувача** (те, що ви зараз читаєте). -Він розроблений таким чином, що ви можете створити повну програму лише за допомогою **Туторіал - Посібник користувача**, а потім розширити її різними способами, залежно від ваших потреб, використовуючи деякі з додаткових ідей з **Розширеного посібника користувача** . +Він розроблений таким чином, що ви можете створити повну програму лише за допомогою **Туторіал - Посібник користувача**, а потім розширити її різними способами, залежно від ваших потреб, використовуючи деякі з додаткових ідей з **Розширеного посібника користувача**. diff --git a/docs/uk/docs/tutorial/metadata.md b/docs/uk/docs/tutorial/metadata.md index 64e667ec6..cf1704562 100644 --- a/docs/uk/docs/tutorial/metadata.md +++ b/docs/uk/docs/tutorial/metadata.md @@ -1,26 +1,26 @@ -# Метадані та URL-адреси документації +# Метадані та URL-адреси документації { #metadata-and-docs-urls } Ви можете налаштувати кілька конфігурацій метаданих у Вашому додатку **FastAPI**. -## Метадані для API +## Метадані для API { #metadata-for-api } Ви можете встановити такі поля, які використовуються в специфікації OpenAPI та в автоматично згенерованих інтерфейсах документації API: | Параметр | Тип | Опис | |------------|------|-------------| | `title` | `str` | Назва API. | -| `summary` | `str` | Короткий опис API. <small>Доступно з OpenAPI 3.1.0, FastAPI 0.99.0.</small> | -| `description` | `str` | Більш детальний опис API. Може використовувати Markdown. | +| `summary` | `str` | Короткий підсумок API. <small>Доступно з OpenAPI 3.1.0, FastAPI 0.99.0.</small> | +| `description` | `str` | Короткий опис API. Може використовувати Markdown. | | `version` | `string` | Версія API. Це версія Вашого додатка, а не OpenAPI. Наприклад, `2.5.0`. | -| `terms_of_service` | `str` | URL до умов використання API. Якщо вказано, має бути у форматі URL. | -| `contact` | `dict` | Інформація для контакту з API. Може містити кілька полів. <details><summary><code>contact</code> поля</summary><table><thead><tr><th>Параметр</th><th>Тип</th><th>Опис</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td>Ім'я контактної особи або організації.</td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>URL з інформацією для контакту. Повинен бути у форматі URL.</td></tr><tr><td><code>email</code></td><td><code>str</code></td><td>Email контактної особи або організації. Повинен бути у форматі електронної пошти.</td></tr></tbody></table></details> | -| `license_info` | `dict` | Інформація про ліцензію для API. Може містити кілька полів. <details><summary><code>license_info</code> поля</summary><table><thead><tr><th>Параметр</th><th>Тип</th><th>Опис</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td><strong>ОБОВ'ЯЗКОВО</strong> (якщо встановлено <code>license_info</code>). Назва ліцензії для API.</td></tr><tr><td><code>identifier</code></td><td><code>str</code></td><td>Ліцензійний вираз за <a href="https://spdx.org/licenses/" class="external-link" target="_blank">SPDX</a> для API. Поле <code>identifier</code> взаємовиключне з полем <code>url</code>. <small>Доступно з OpenAPI 3.1.0, FastAPI 0.99.0.</small></td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>URL до ліцензії, яка використовується для API. Повинен бути у форматі URL.</td></tr></tbody></table></details> | +| `terms_of_service` | `str` | URL до умов використання API. Якщо вказано, має бути у форматі URL. | +| `contact` | `dict` | Інформація для контакту з опублікованим API. Може містити кілька полів. <details><summary><code>contact</code> поля</summary><table><thead><tr><th>Параметр</th><th>Тип</th><th>Опис</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td>Ідентифікаційне ім'я контактної особи або організації.</td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>URL, що вказує на контактну інформацію. <strong>МАЄ</strong> бути у форматі URL.</td></tr><tr><td><code>email</code></td><td><code>str</code></td><td>Адреса електронної пошти контактної особи або організації. <strong>МАЄ</strong> бути у форматі адреси електронної пошти.</td></tr></tbody></table></details> | +| `license_info` | `dict` | Інформація про ліцензію для опублікованого API. Може містити кілька полів. <details><summary><code>license_info</code> поля</summary><table><thead><tr><th>Параметр</th><th>Тип</th><th>Опис</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td><strong>ОБОВ'ЯЗКОВО</strong> (якщо встановлено <code>license_info</code>). Назва ліцензії для API.</td></tr><tr><td><code>identifier</code></td><td><code>str</code></td><td>Ліцензійний вираз за <a href="https://spdx.org/licenses/" class="external-link" target="_blank">SPDX</a> для API. Поле <code>identifier</code> взаємовиключне з полем <code>url</code>. <small>Доступно з OpenAPI 3.1.0, FastAPI 0.99.0.</small></td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>URL до ліцензії, яка використовується для API. <strong>МАЄ</strong> бути у форматі URL.</td></tr></tbody></table></details> | Ви можете налаштувати їх наступним чином: -{* ../../docs_src/metadata/tutorial001.py hl[3:16, 19:32] *} +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} -/// tip | Підказка +/// tip | Порада У полі `description` можна використовувати Markdown, і він буде відображатися у результаті. @@ -30,15 +30,15 @@ <img src="/img/tutorial/metadata/image01.png"> -## Ідентифікатор ліцензії +## Ідентифікатор ліцензії { #license-identifier } З початку використання OpenAPI 3.1.0 та FastAPI 0.99.0 Ви також можете налаштувати `license_info` за допомогою `identifier` замість `url`. Наприклад: -{* ../../docs_src/metadata/tutorial001_1.py hl[31] *} +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} -## Метадані для тегів +## Метадані для тегів { #metadata-for-tags } Ви також можете додати додаткові метадані для різних тегів, які використовуються для групування операцій шляхів, за допомогою параметра `openapi_tags`. @@ -46,53 +46,53 @@ Кожен словник може містити: -* `name` (**обов'язково**): `str` з тією ж назвою тегу, яку Ви використовуєте у параметрі `tags` у Ваших *операціях шляху* та `APIRouter`s. -* `description`: `str` з коротким описом тегу. Може містити Markdown і буде відображено в інтерфейсі документації. -* `externalDocs`: `dict` який описує зовнішню документацію з такими полями: +* `name` (**обов'язково**): `str` з тією ж назвою тегу, яку Ви використовуєте у параметрі `tags` у Ваших *операціях шляху* та `APIRouter`s. +* `description`: `str` з коротким описом тегу. Може містити Markdown і буде показано в інтерфейсі документації. +* `externalDocs`: `dict`, який описує зовнішню документацію з такими полями: * `description`: `str` з коротким описом зовнішньої документації. - * `url` (**обов'язково**): `str`з URL-адресою зовнішньої документації. + * `url` (**обов'язково**): `str` з URL-адресою зовнішньої документації. -### Створення метаданих для тегів +### Створення метаданих для тегів { #create-metadata-for-tags } Спробуймо це на прикладі з тегами для `users` та `items`. -Створіть метадані для своїх тегів і передайте їх у параметр `openapi_tags`: +Створіть метадані для своїх тегів і передайте їх у параметр `openapi_tags`: -{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *} Зверніть увагу, що в описах можна використовувати Markdown, наприклад, "login" буде показано жирним шрифтом (**login**), а "fancy" буде показано курсивом (_fancy_). /// tip | Порада -Не обов'язково додавати метадані для всіх тегів, які Ви використовуєте. +Вам не потрібно додавати метадані для всіх тегів, які Ви використовуєте. /// -### Використання тегів +### Використовуйте свої теги { #use-your-tags } -Використовуйте параметр `tags` зі своїми *операціями шляху* (і `APIRouter`) для призначення їх до різних тегів: +Використовуйте параметр `tags` зі своїми *операціями шляху* (і `APIRouter`s), щоб призначити їх до різних тегів: -{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} /// info | Інформація -Детальніше про теги читайте в розділі [Конфігурація шляхів операцій](path-operation-configuration.md#tags){.internal-link target=_blank}. +Детальніше про теги читайте в розділі [Конфігурація операції шляху](path-operation-configuration.md#tags){.internal-link target=_blank}. /// -### Перевірка документації +### Перевірте документацію { #check-the-docs } -Якщо Ви зараз перевірите документацію, вона покаже всі додаткові метадані: +Тепер, якщо Ви перевірите документацію, вона покаже всі додаткові метадані: <img src="/img/tutorial/metadata/image02.png"> -### Порядок тегів +### Порядок тегів { #order-of-tags } Порядок кожного словника метаданих тегу також визначає порядок відображення в інтерфейсі документації. -Наприклад, хоча `users` мав би йти після `items` в алфавітному порядку, він відображається перед ними, оскільки ми додали його метадані як перший словник у списку. +Наприклад, хоча `users` мав би йти після `items` в алфавітному порядку, він відображається перед ними, оскільки ми додали їхні метадані як перший словник у списку. -## URL для OpenAPI +## URL для OpenAPI { #openapi-url } За замовчуванням схема OpenAPI надається за адресою `/openapi.json`. @@ -100,11 +100,11 @@ Наприклад, щоб налаштувати його на `/api/v1/openapi.json`: -{* ../../docs_src/metadata/tutorial002.py hl[3] *} +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} Якщо Ви хочете повністю вимкнути схему OpenAPI, Ви можете встановити `openapi_url=None`, це також вимкне інтерфейси документації, які її використовують. -## URL-адреси документації +## URL-адреси документації { #docs-urls } Ви можете налаштувати два інтерфейси користувача для документації, які включені: @@ -117,4 +117,4 @@ Наприклад, щоб налаштувати Swagger UI на `/documentation` і вимкнути ReDoc: -{* ../../docs_src/metadata/tutorial003.py hl[3] *} +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/uk/docs/tutorial/middleware.md b/docs/uk/docs/tutorial/middleware.md index 13ce8573d..2d1580e49 100644 --- a/docs/uk/docs/tutorial/middleware.md +++ b/docs/uk/docs/tutorial/middleware.md @@ -1,45 +1,43 @@ -# Middleware (Проміжний шар) +# Middleware { #middleware } -У **FastAPI** можна додавати middleware (проміжний шар). +У **FastAPI** можна додавати middleware. -"Middleware" — це функція, яка працює з кожним **запитом** перед його обробкою будь-якою конкретною *операцією шляху* (*path operation*), а також з кожною **відповіддю** перед її поверненням. +«Middleware» — це функція, яка працює з кожним **запитом** перед його обробкою будь-якою конкретною *операцією шляху*. А також з кожною **відповіддю** перед її поверненням. -* Middleware отримує кожен **запит**, що надходить до Вашого застосунку. -* Може виконати певні дії із цим **запитом** або запустити необхідний код. -* Далі передає **запит** для обробки основним застосунком (*операцією шляху*). -* Отримує **відповідь**, сформовану застосунком (*операцією шляху*). -* Може змінити цю **відповідь** або виконати додатковий код. -* Повертає **відповідь** клієнту. +* Вона отримує кожен **запит**, що надходить до вашого застосунку. +* Потім вона може виконати певні дії із цим **запитом** або запустити необхідний код. +* Далі вона передає **запит** для обробки рештою застосунку (якоюсь *операцією шляху*). +* Потім вона отримує **відповідь**, сформовану застосунком (якоюсь *операцією шляху*). +* Вона може виконати певні дії із цією **відповіддю** або запустити необхідний код. +* Потім вона повертає **відповідь**. /// note | Технічні деталі -Якщо у Вас є залежності з `yield`, код виходу виконається *після* middleware. +Якщо у вас є залежності з `yield`, код виходу виконається *після* middleware. -Якщо були заплановані фонові задачі (background tasks - розглянуто далі), вони виконаються *після* всіх middleware. +Якщо були заплановані фонові задачі (розглянуто в розділі [Background Tasks](background-tasks.md){.internal-link target=_blank}, ви побачите це пізніше), вони виконаються *після* всіх middleware. /// -## Створення middleware +## Створення middleware { #create-a-middleware } -Щоб створити middleware, Ви використовуєте декоратор `@app.middleware("http")` на функції. +Щоб створити middleware, ви використовуєте декоратор `@app.middleware("http")` над функцією. Функція middleware отримує: -* `Запит`. -* Функцію `call_next`, яка приймає `запит` як параметр. - * Ця функція передає `запит` відповідній *операції шляху*. - * Потім вона повертає `відповідь`, згенеровану цією *операцією шляху*. +* `request`. +* Функцію `call_next`, яка отримає `request` як параметр. + * Ця функція передасть `request` відповідній *операції шляху*. + * Потім вона поверне `response`, згенеровану відповідною *операцією шляху*. +* Потім ви можете додатково змінити `response` перед тим, як повернути її. -* Ви можете ще змінити `відповідь` перед тим, як повернути її. - - -{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[8:9,11,14] *} /// tip | Порада -Не забувайте, що власні заголовки можна додавати, використовуючи <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">префікс 'X-'</a>. +Пам’ятайте, що власні пропрієтарні заголовки можна додавати <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">використовуючи префікс `X-`</a>. -Але якщо у Вас є власні заголовки, які Ви хочете, щоб браузерний клієнт міг побачити, потрібно додати їх до Вашої конфігурації CORS (див. [CORS (Обмін ресурсами між різними джерелами)](cors.md){.internal-link target=_blank} за допомогою параметра `expose_headers`, описаного в <a href="https://www.starlette.dev/middleware/#corsmiddleware" class="external-link" target="_blank">документації Starlette по CORS</a>. +Але якщо у вас є власні заголовки, які ви хочете, щоб клієнт у браузері міг побачити, потрібно додати їх до ваших конфігурацій CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) за допомогою параметра `expose_headers`, описаного в <a href="https://www.starlette.dev/middleware/#corsmiddleware" class="external-link" target="_blank">документації Starlette по CORS</a>. /// @@ -47,28 +45,50 @@ Ви також можете використати `from starlette.requests import Request`. -**FastAPI** надає це для Вашої зручності як розробника. Але він походить безпосередньо зі Starlette. +**FastAPI** надає це для вашої зручності як розробника. Але воно походить безпосередньо зі Starlette. /// -### До і після `response`(`відповіді`) +### До і після `response` { #before-and-after-the-response } -Ви можете додати код, який буде виконуватися з `запитом` (`request`), до того, як його обробить будь-яка *операція шляху* (*path operation*). +Ви можете додати код, який буде виконуватися з `request`, до того, як його отримає будь-яка *операція шляху*. -Також Ви можете додати код, який буде виконуватися після того, як `відповідь` (`response`) буде згенеровано, перед тим як його повернути. +Також ви можете додати код, який буде виконуватися після того, як `response` буде згенеровано, перед тим як її повернути. -Наприклад, Ви можете додати власний заголовок `X-Process-Time`, який міститиме час у секундах, який витратився на обробку запиту та генерацію відповіді: +Наприклад, ви можете додати власний заголовок `X-Process-Time`, який міститиме час у секундах, який витратився на обробку запиту та генерацію відповіді: -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} - -/// tip | Підказка +/// tip | Порада Тут ми використовуємо <a href="https://docs.python.org/3/library/time.html#time.perf_counter" class="external-link" target="_blank">`time.perf_counter()`</a> замість `time.time()` оскільки він може бути більш точним для таких випадків. 🤓 /// -## Інші middlewares +## Порядок виконання кількох middleware { #multiple-middleware-execution-order } + +Коли ви додаєте кілька middleware, використовуючи або декоратор `@app.middleware()` або метод `app.add_middleware()`, кожен новий middleware обгортає застосунок, утворюючи стек. Останній доданий middleware є *зовнішнім*, а перший — *внутрішнім*. + +На шляху запиту першим виконується *зовнішній* middleware. + +На шляху відповіді він виконується останнім. + +Наприклад: + +```Python +app.add_middleware(MiddlewareA) +app.add_middleware(MiddlewareB) +``` + +Це призводить до такого порядку виконання: + +* **Запит**: MiddlewareB → MiddlewareA → route + +* **Відповідь**: route → MiddlewareA → MiddlewareB + +Така поведінка стеку гарантує, що middleware виконуються у передбачуваному та керованому порядку. + +## Інші middlewares { #other-middlewares } Ви можете пізніше прочитати більше про інші middlewares в [Advanced User Guide: Advanced Middleware](../advanced/middleware.md){.internal-link target=_blank}. diff --git a/docs/uk/docs/tutorial/path-params-numeric-validations.md b/docs/uk/docs/tutorial/path-params-numeric-validations.md index 8ee4f480f..f6aa92019 100644 --- a/docs/uk/docs/tutorial/path-params-numeric-validations.md +++ b/docs/uk/docs/tutorial/path-params-numeric-validations.md @@ -1,8 +1,8 @@ -# Path Параметри та валідація числових даних +# Параметри шляху та валідація числових даних { #path-parameters-and-numeric-validations } -Так само як Ви можете оголошувати додаткові перевірки та метадані для query параметрів за допомогою `Query`, Ви можете оголошувати той самий тип перевірок і метаданих для параметрів шляху за допомогою `Path`. +Так само як ви можете оголошувати додаткові перевірки та метадані для query параметрів за допомогою `Query`, ви можете оголошувати той самий тип перевірок і метаданих для параметрів шляху за допомогою `Path`. -## Імпорт Path +## Імпорт `Path` { #import-path } Спочатку імпортуйте `Path` з `fastapi` і імпортуйте `Annotated`: @@ -10,70 +10,69 @@ /// info | Інформація -FastAPI додав підтримку `Annotated` (і почав рекомендувати його використання) у версії 0.95.0. +FastAPI додав підтримку `Annotated` (і почав рекомендувати його використання) у версії 0.95.0. -Якщо у Вас стара версія, при спробі використати `Annotated` можуть виникати помилки. +Якщо у вас стара версія, при спробі використати `Annotated` можуть виникати помилки. -Переконайтеся, що Ви [оновили версію FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} принаймні до версії 0.95.1 перед використанням `Annotated`. +Переконайтеся, що ви [оновили версію FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} принаймні до версії 0.95.1 перед використанням `Annotated`. /// -## Оголошення метаданих +## Оголошення метаданих { #declare-metadata } Ви можете оголошувати всі ті ж параметри, що і для `Query`. -Наприклад, щоб оголосити значення метаданих `title` для параметра шляху `item_id`, Ви можете написати: +Наприклад, щоб оголосити значення метаданих `title` для параметра шляху `item_id`, ви можете написати: {* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} /// note | Примітка -Параметр шляху завжди є обов’язковим, оскільки він має бути частиною шляху. Навіть якщо Ви оголосите його зі значенням `None` або встановите значення за замовчуванням — він все одно залишатиметься обов’язковим. +Параметр шляху завжди є обов’язковим, оскільки він має бути частиною шляху. Навіть якщо ви оголосите його зі значенням `None` або встановите значення за замовчуванням — це ні на що не вплине, він все одно завжди буде обов’язковим. /// -## Упорядковуйте параметри, як Вам потрібно +## Упорядковуйте параметри, як вам потрібно { #order-the-parameters-as-you-need } -/// tip | Підказка +/// tip | Порада -Це, мабуть, не настільки важливо або необхідно, якщо Ви використовуєте `Annotated`. +Це, мабуть, не настільки важливо або необхідно, якщо ви використовуєте `Annotated`. /// -Припустимо, Ви хочете оголосити параметр запиту `q` як обов’язковий `str`. +Припустимо, ви хочете оголосити параметр запиту `q` як обов’язковий `str`. -І Вам не потрібно оголошувати нічого іншого для цього параметра, тому немає потреби використовувати `Query`. +І вам не потрібно оголошувати нічого іншого для цього параметра, тому вам насправді не потрібно використовувати `Query`. -Але Вам все одно потрібно використовувати `Path` для параметра шляху `item_id`. І з певних причин Ви не хочете використовувати `Annotated`. +Але вам все одно потрібно використовувати `Path` для параметра шляху `item_id`. І з певних причин ви не хочете використовувати `Annotated`. Python видасть помилку, якщо розмістити значення з "default" перед значенням, яке не має "default". -Але Ви можете змінити порядок і розмістити значення без значення за замовчуванням (параметр запиту `q`) першим. +Але ви можете змінити порядок і розмістити значення без значення за замовчуванням (параметр запиту `q`) першим. +Для **FastAPI** порядок не має значення. Він визначає параметри за їхніми іменами, типами та оголошеннями за замовчуванням (`Query`, `Path` тощо) і не звертає уваги на порядок. -Для **FastAPI** порядок не має значення. Він визначає параметри за їх іменами, типами та значеннями за замовчуванням (`Query`, `Path` тощо) і не звертає уваги на порядок. +Тому ви можете оголосити вашу функцію так: -Тому Ви можете оголосити Вашу функцію так: +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} - -Але майте на увазі, що якщо Ви використовуєте `Annotated`, ця проблема не виникне, оскільки Ви не використовуєте значення за замовчуванням для параметрів `Query()` або `Path()`. +Але майте на увазі, що якщо ви використовуєте `Annotated`, цієї проблеми не буде, це не матиме значення, оскільки ви не використовуєте значення за замовчуванням параметрів функції для `Query()` або `Path()`. {* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} -## Упорядковуйте параметри за потребою, хитрощі +## Упорядковуйте параметри, як вам потрібно: хитрощі { #order-the-parameters-as-you-need-tricks } -/// tip | Підказка +/// tip | Порада -Це, мабуть, не настільки важливо або необхідно, якщо Ви використовуєте `Annotated`. +Це, мабуть, не настільки важливо або необхідно, якщо ви використовуєте `Annotated`. /// Ось **невелика хитрість**, яка може стати в пригоді, хоча вона рідко знадобиться. -Якщо Ви хочете: +Якщо ви хочете: -* оголосити параметр запиту `q` без використання `Query` або значення за замовчуванням +* оголосити параметр запиту `q` без `Query` і без жодного значення за замовчуванням * оголосити параметр шляху `item_id`, використовуючи `Path` * розмістити їх у різному порядку * не використовувати `Annotated` @@ -84,72 +83,72 @@ Python видасть помилку, якщо розмістити значен Python нічого не зробить із цією `*`, але розпізнає, що всі наступні параметри слід викликати як аргументи за ключовим словом (пари ключ-значення), також відомі як <abbr title="From: K-ey W-ord Arg-uments"><code>kwargs</code></abbr>. Навіть якщо вони не мають значення за замовчуванням. -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} -### Краще з `Annotated` +### Краще з `Annotated` { #better-with-annotated } -Майте на увазі, якщо Ви використовуєте `Annotated`, оскільки Ви не використовуєте значення за замовчуванням для параметрів функції, цієї проблеми не виникне, і, швидше за все, Вам не потрібно буде використовувати `*`. +Майте на увазі, що якщо ви використовуєте `Annotated`, оскільки ви не використовуєте значення за замовчуванням параметрів функції, цієї проблеми не буде, і, ймовірно, вам не потрібно буде використовувати `*`. {* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} -## Валідація числових даних: більше або дорівнює +## Валідація числових даних: більше або дорівнює { #number-validations-greater-than-or-equal } -За допомогою `Query` і `Path` (та інших, які Ви побачите пізніше) можна оголошувати числові обмеження. +За допомогою `Query` і `Path` (та інших, які ви побачите пізніше) можна оголошувати числові обмеження. Тут, завдяки `ge=1`, `item_id` має бути цілим числом, яке "`g`reater than or `e`qual" (більше або дорівнює) `1`. {* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} -## Валідація числових даних: більше ніж і менше або дорівнює +## Валідація числових даних: більше ніж і менше або дорівнює { #number-validations-greater-than-and-less-than-or-equal } Те саме застосовується до: -* `gt`: `g`reater `t`han (більше ніж) -* `le`: `l`ess than or `e`qual (менше або дорівнює) +* `gt`: `g`reater `t`han +* `le`: `l`ess than or `e`qual {* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *} -## Валідація числових даних: float, більше ніж і менше ніж +## Валідація числових даних: float, більше ніж і менше ніж { #number-validations-floats-greater-than-and-less-than } Валідація чисел також працює для значень типу `float`. -Ось де стає важливо мати можливість оголошувати <abbr title="greater than (більше ніж)"><code>gt</code></abbr>, а не тільки <abbr title="greater than or equal (більше або дорівнює)"><code>ge</code></abbr>. Це дозволяє, наприклад, вимагати, щоб значення було більше `0`, навіть якщо воно менше `1`. +Ось де стає важливо мати можливість оголошувати <abbr title="greater than"><code>gt</code></abbr>, а не тільки <abbr title="greater than or equal"><code>ge</code></abbr>. Це дозволяє, наприклад, вимагати, щоб значення було більше `0`, навіть якщо воно менше `1`. Таким чином, значення `0.5` буде допустимим. Але `0.0` або `0` — ні. -Те саме стосується <abbr title="less than (менше ніж)"><code>lt</code></abbr>. +Те саме стосується <abbr title="less than"><code>lt</code></abbr>. {* ../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py hl[13] *} -## Підсумок +## Підсумок { #recap } -За допомогою `Query`, `Path` (і інших параметрів, які Ви ще не бачили) можна оголошувати метадані та перевірки рядків, так само як у [Query параметри та валідація рядків](query-params-str-validations.md){.internal-link target=_blank}. +За допомогою `Query`, `Path` (і інших параметрів, які ви ще не бачили) можна оголошувати метадані та перевірки рядків так само як у [Query параметри та валідація рядків](query-params-str-validations.md){.internal-link target=_blank}. Також можна оголошувати числові перевірки: -* `gt`: `g`reater `t`han (більше ніж) -* `ge`: `g`reater than or `e`qual (більше або дорівнює) -* `lt`: `l`ess `t`han (менше ніж) -* `le`: `l`ess than or `e`qual (менше або дорівнює) +* `gt`: `g`reater `t`han +* `ge`: `g`reater than or `e`qual +* `lt`: `l`ess `t`han +* `le`: `l`ess than or `e`qual /// info | Інформація -`Query`, `Path` та інші класи, які Ви побачите пізніше, є підкласами спільного класу `Param`. +`Query`, `Path` та інші класи, які ви побачите пізніше, є підкласами спільного класу `Param`. -Всі вони мають однакові параметри для додаткових перевірок і метаданих, які Ви вже бачили. +Всі вони мають однакові параметри для додаткових перевірок і метаданих, які ви вже бачили. /// /// note | Технічні деталі -Коли Ви імпортуєте `Query`, `Path` та інші з `fastapi`, насправді це функції. +Коли ви імпортуєте `Query`, `Path` та інші з `fastapi`, насправді це функції. При виклику вони повертають екземпляри класів з такими ж іменами. -Тобто Ви імпортуєте `Query`, яка є функцією. А коли Ви її викликаєте, вона повертає екземпляр класу, який теж називається `Query`. +Тобто ви імпортуєте `Query`, яка є функцією. А коли ви її викликаєте, вона повертає екземпляр класу, який теж називається `Query`. -Ці функції створені таким чином (замість використання класів напряму), щоб Ваш редактор не відзначав їхні типи як помилки. +Ці функції створені таким чином (замість використання класів напряму), щоб ваш редактор не відзначав їхні типи як помилки. -Таким чином, Ви можете користуватися своїм звичайним редактором і інструментами для програмування без додаткових налаштувань для ігнорування таких помилок. +Таким чином, ви можете користуватися своїм звичайним редактором і інструментами для програмування без додаткових налаштувань для ігнорування таких помилок. /// diff --git a/docs/uk/docs/tutorial/path-params.md b/docs/uk/docs/tutorial/path-params.md index da4ff2f9e..059890549 100644 --- a/docs/uk/docs/tutorial/path-params.md +++ b/docs/uk/docs/tutorial/path-params.md @@ -1,34 +1,34 @@ -# Path Параметри +# Параметри шляху { #path-parameters } -Ви можете визначити "параметри" або "змінні" шляху, використовуючи синтаксис форматованих рядків: +Ви можете оголосити «параметри» або «змінні» шляху, використовуючи той самий синтаксис, що й у форматованих рядках Python: -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} -Значення параметра шляху `item_id` передається у функцію як аргумент `item_id`. +Значення параметра шляху `item_id` буде передано у вашу функцію як аргумент `item_id`. -Якщо запустити цей приклад та перейти за посиланням <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>, то отримаємо таку відповідь: +Отже, якщо ви запустите цей приклад і перейдете за посиланням <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>, то побачите відповідь: ```JSON {"item_id":"foo"} ``` -## Path параметри з типами +## Параметри шляху з типами { #path-parameters-with-types } -Ви можете визначити тип параметра шляху у функції, використовуючи стандартні анотації типів Python: +Ви можете оголосити тип параметра шляху у функції, використовуючи стандартні анотації типів Python: -{* ../../docs_src/path_params/tutorial002.py hl[7] *} +{* ../../docs_src/path_params/tutorial002_py39.py hl[7] *} -У такому випадку `item_id` визначається як `int`. +У цьому випадку `item_id` оголошено як `int`. /// check | Примітка -Це дасть можливість підтримки редактора всередині функції з перевірками помилок, автодоповнення тощо. +Це дасть вам підтримку редактора всередині функції з перевірками помилок, автодоповненням тощо. /// -## <abbr title="або: серіалізація, парсинг, маршалізація">Перетворення</abbr> даних +## <abbr title="also known as: serialization, parsing, marshalling – також відомо як: серіалізація, парсинг, маршалізація">Перетворення</abbr> даних { #data-conversion } -Якщо запустити цей приклад і перейти за посиланням <a href="http://127.0.0.1:8000/items/3" class="external-link" target="_blank">http://127.0.0.1:8000/items/3</a>, то отримаєте таку відповідь: +Якщо ви запустите цей приклад і відкриєте у браузері <a href="http://127.0.0.1:8000/items/3" class="external-link" target="_blank">http://127.0.0.1:8000/items/3</a>, то побачите відповідь: ```JSON {"item_id":3} @@ -36,15 +36,15 @@ /// check | Примітка -Зверніть увагу, що значення, яке отримала (і повернула) ваша функція, — це `3`. Це Python `int`, а не рядок `"3"`. +Зверніть увагу, що значення, яке отримала (і повернула) ваша функція, — це `3`, як Python `int`, а не рядок `"3"`. -Отже, з таким оголошенням типу **FastAPI** автоматично виконує <abbr title="перетворення рядка, що надходить із HTTP-запиту, у типи даних Python">"парсинг"</abbr> запитів. +Отже, з таким оголошенням типу **FastAPI** надає вам автоматичний <abbr title="converting the string that comes from an HTTP request into Python data – перетворення рядка, що надходить із HTTP-запиту, у дані Python">«parsing»</abbr> запиту. /// -## <abbr title="Або валідація">Перевірка</abbr> даних +## Валідація даних { #data-validation } -Якщо ж відкрити у браузері посилання <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>, то побачимо цікаву HTTP-помилку: +Але якщо ви перейдете у браузері за посиланням <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>, ви побачите гарну HTTP-помилку: ```JSON { @@ -61,144 +61,136 @@ ] } ``` -тому що параметр шляху має значення `"foo"`, яке не є типом `int`. -Таку саму помилку отримаємо, якщо передати `float` замість `int`, як бачимо, у цьому прикладі: <a href="http://127.0.0.1:8000/items/4.2" class="external-link" target="_blank">http://127.0.0.1:8000/items/4.2</a> +тому що параметр шляху `item_id` мав значення `"foo"`, яке не є `int`. + +Та сама помилка з’явиться, якщо ви передасте `float` замість `int`, як у: <a href="http://127.0.0.1:8000/items/4.2" class="external-link" target="_blank">http://127.0.0.1:8000/items/4.2</a> /// check | Примітка -Отже, **FastAPI** надає перевірку типів з таким самим оголошенням типу в Python. +Отже, з тим самим оголошенням типу в Python **FastAPI** надає вам валідацію даних. -Зверніть увагу, що помилка також чітко вказує саме на те місце, де валідація не пройшла. +Зверніть увагу, що помилка також чітко вказує саме місце, де валідація не пройшла. -Це неймовірно корисно під час розробки та дебагінгу коду, що взаємодіє з вашим API. +Це неймовірно корисно під час розробки та налагодження коду, що взаємодіє з вашим API. /// -## Документація +## Документація { #documentation } -Тепер коли відкриєте свій браузер за посиланням <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>, то побачите автоматично згенеровану, інтерактивну API-документацію: +А коли ви відкриєте у браузері <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>, ви побачите автоматичну, інтерактивну, API-документацію на кшталт: <img src="/img/tutorial/path-params/image01.png"> /// check | Примітка -Знову ж таки, лише з цим самим оголошенням типу в Python, FastAPI надає вам автоматичну, інтерактивну документацію (з інтеграцією Swagger UI). - -Зверніть увагу, що параметр шляху оголошений як ціле число. +Знову ж таки, лише з тим самим оголошенням типу в Python **FastAPI** надає вам автоматичну, інтерактивну документацію (з інтеграцією Swagger UI). +Зверніть увагу, що параметр шляху оголошено як ціле число. /// -## Переваги стандартизації, альтернативна документація +## Переваги стандартів, альтернативна документація { #standards-based-benefits-alternative-documentation } І оскільки згенерована схема відповідає стандарту <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md" class="external-link" target="_blank">OpenAPI</a>, існує багато сумісних інструментів. -З цієї причини FastAPI також надає альтернативну документацію API (використовуючи ReDoc), до якої можна отримати доступ за посиланням <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>: +Через це **FastAPI** також надає альтернативну API-документацію (використовуючи ReDoc), до якої ви можете отримати доступ за посиланням <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>: <img src="/img/tutorial/path-params/image02.png"> -Таким чином, існує багато сумісних інструментів, включаючи інструменти для генерації коду для багатьох мов. +Так само, існує багато сумісних інструментів. Зокрема інструменти генерації коду для багатьох мов. +## Pydantic { #pydantic } -## Pydantic +Уся валідація даних виконується за лаштунками за допомогою <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a>, тож ви отримуєте всі переваги від його використання. І ви знаєте, що ви в надійних руках. -Вся валідація даних виконується за лаштунками за допомогою <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a>, тому Ви отримуєте всі переваги від його використання. І можете бути впевнені, що все в надійних руках. +Ви можете використовувати ті самі оголошення типів з `str`, `float`, `bool` та багатьма іншими складними типами даних. -Ви можете використовувати ті самі оголошення типів з `str`, `float`, `bool` та багатьма іншими складними типами даних. +Декілька з них розглядаються в наступних розділах посібника. -Декілька з них будуть розглянуті в наступних розділах посібника. +## Порядок має значення { #order-matters } -## Порядок має значення +Під час створення *операцій шляху* можуть виникати ситуації, коли у вас є фіксований шлях. -При створенні *операцій шляху* можуть виникати ситуації, коли шлях фіксований. +Наприклад, `/users/me` — припустімо, це для отримання даних про поточного користувача. -Наприклад, `/users/me`. Припустимо, що це шлях для отримання даних про поточного користувача. +І тоді у вас також може бути шлях `/users/{user_id}` для отримання даних про конкретного користувача за його ID. -А також у вас може бути шлях `/users/{user_id}`, щоб отримати дані про конкретного користувача за його ID. +Оскільки *операції шляху* оцінюються по черзі, вам потрібно переконатися, що шлях для `/users/me` оголошено перед шляхом для `/users/{user_id}`: -Оскільки *операції шляху* оцінюються по черзі, Ви повинні переконатися, що шлях для `/users/me` оголошений перед шляхом для `/users/{user_id}`: +{* ../../docs_src/path_params/tutorial003_py39.py hl[6,11] *} -{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} +Інакше шлях для `/users/{user_id}` також відповідатиме `/users/me`, «вважаючи», що отримує параметр `user_id` зі значенням `"me"`. -Інакше шлях для `/users/{user_id}` також буде відповідати для `/users/me`, "вважаючи", що він отримує параметр `user_id` зі значенням `"me"`. +Так само ви не можете перевизначити операцію шляху: -Аналогічно, Ви не можете оголосити операцію шляху: +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} -{* ../../docs_src/path_params/tutorial003b.py hl[6,11] *} +Завжди використовуватиметься перша, оскільки шлях збігається першим. -Перша операція буде завжди використовуватися, оскільки шлях збігається першим. -## Попередньо визначені значення +## Попередньо визначені значення { #predefined-values } -Якщо у вас є *операція шляху*, яка приймає *параметр шляху*, але Ви хочете, щоб можливі допустимі значення *параметра шляху* були попередньо визначені, Ви можете використати стандартний Python <abbr title="перелічення">Enum</abbr>. +Якщо у вас є *операція шляху*, яка отримує *параметр шляху*, але ви хочете, щоб можливі коректні значення *параметра шляху* були попередньо визначені, ви можете використати стандартний Python <abbr title="Enumeration">`Enum`</abbr>. -### Створення класу `Enum` +### Створіть клас `Enum` { #create-an-enum-class } Імпортуйте `Enum` і створіть підклас, що наслідується від `str` та `Enum`. -Наслідуючи від `str`, документація API зможе визначити, що значення повинні бути типу `string`, і правильно їх відобразить. +Завдяки наслідуванню від `str` документація API зможе визначити, що значення повинні бути типу `string`, і зможе коректно їх відобразити. -Після цього створіть атрибути класу з фіксованими значеннями, які будуть доступними допустимими значеннями: +Після цього створіть атрибути класу з фіксованими значеннями, які будуть доступними коректними значеннями: -{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} - -/// info | Додаткова інформація - -<a href="https://docs.python.org/3/library/enum.html" class="external-link" target="_blank">Перелічення (або enums) доступні в Python</a> починаючи з версії 3.4. - -/// +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | Порада -Якщо вам цікаво, "AlexNet", "ResNet" та "LeNet" — це просто назви ML моделей <abbr title="Технічно, архітектури Deep Learning моделей">Machine Learning</abbr>. +Якщо вам цікаво, «AlexNet», «ResNet» та «LeNet» — це просто назви Machine Learning <abbr title="Technically, Deep Learning model architectures – технічно, архітектури моделей Deep Learning">models</abbr>. /// - -### Оголосіть *параметр шляху* +### Оголосіть *параметр шляху* { #declare-a-path-parameter } Потім створіть *параметр шляху* з анотацією типу, використовуючи створений вами клас enum (`ModelName`): -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} -### Перевірка документації +### Перевірте документацію { #check-the-docs } -Оскільки доступні значення для *параметра шляху* визначені заздалегідь, інтерактивна документація зможе красиво їх відобразити: +Оскільки доступні значення для *параметра шляху* визначені заздалегідь, інтерактивна документація може красиво їх показати: <img src="/img/tutorial/path-params/image03.png"> -### Робота з *перелічуваннями* у Python +### Робота з Python *переліченнями* { #working-with-python-enumerations } -Значення *параметра шляху* буде елементом *перелічування*. +Значення *параметра шляху* буде *елементом перелічування*. -#### Порівняння *елементів перелічування* +#### Порівняйте *елементи перелічування* { #compare-enumeration-members } -Ви можете порівнювати його з *елементами перелічування* у створеному вами enum `ModelName`: +Ви можете порівнювати його з *елементом перелічування* у створеному вами enum `ModelName`: -{* ../../docs_src/path_params/tutorial005.py hl[17] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *} -#### Отримання *значення перелічування* +#### Отримайте *значення перелічування* { #get-the-enumeration-value } Ви можете отримати фактичне значення (у цьому випадку це `str`), використовуючи `model_name.value`, або загалом `your_enum_member.value`: -{* ../../docs_src/path_params/tutorial005.py hl[20] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *} /// tip | Порада -Ви також можете отримати доступ до значення `"lenet"`, використовуючи `ModelName.lenet.value`. +Ви також можете отримати доступ до значення `"lenet"` через `ModelName.lenet.value`. /// +#### Поверніть *елементи перелічування* { #return-enumeration-members } -#### Повернення *елементів перелічування* - -Ви можете повертати *елементи перелічування* з вашої *операції шляху*, навіть вкладені у JSON-тіло (наприклад, `dict`). +Ви можете повертати *елементи enum* з вашої *операції шляху*, навіть вкладені у JSON-тіло (наприклад, `dict`). Вони будуть перетворені на відповідні значення (у цьому випадку рядки) перед поверненням клієнту: -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} -На стороні клієнта Ви отримаєте відповідь у форматі JSON, наприклад: +На стороні клієнта ви отримаєте відповідь у форматі JSON, наприклад: ```JSON { @@ -207,36 +199,35 @@ } ``` -## Path-параметри, що містять шляхи +## Параметри шляху, що містять шляхи { #path-parameters-containing-paths } -Припустимо, у вас є *операція шляху* з маршрутом `/files/{file_path}`. +Припустімо, у вас є *операція шляху* зі шляхом `/files/{file_path}`. -Але вам потрібно, щоб `file_path` містив *шлях*, наприклад `home/johndoe/myfile.txt`. +Але вам потрібно, щоб `file_path` сам містив *шлях*, наприклад `home/johndoe/myfile.txt`. -Отже, URL для цього файлу виглядатиме так: `/files/home/johndoe/myfile.txt`. +Отже, URL для цього файлу виглядатиме приблизно так: `/files/home/johndoe/myfile.txt`. +### Підтримка OpenAPI { #openapi-support } +OpenAPI не підтримує спосіб оголошення *параметра шляху*, який має містити всередині *шлях*, оскільки це може призвести до сценаріїв, які складно тестувати та визначати. -### Підтримка OpenAPI +Проте ви все одно можете зробити це в **FastAPI**, використовуючи один із внутрішніх інструментів Starlette. -OpenAPI не підтримує спосіб оголошення *параметра шляху*, що містить *шлях* всередині, оскільки це може призвести до сценаріїв, які складно тестувати та визначати. +І документація все ще працюватиме, хоча й не додаватиме жодної документації, яка б казала, що параметр має містити шлях. -Однак (одначе), Ви все одно можете зробити це в **FastAPI**, використовуючи один із внутрішніх інструментів Starlette. +### Конвертер шляху { #path-convertor } -Документація все ще працюватиме, хоча й не додаватиме опису про те, що параметр повинен містити шлях. - -### Конвертер шляху - -Використовуючи опцію безпосередньо зі Starlette, Ви можете оголосити *параметр шляху*, що містить *шлях*, використовуючи URL на кшталт: +Використовуючи опцію безпосередньо зі Starlette, ви можете оголосити *параметр шляху*, що містить *шлях*, використовуючи URL на кшталт: ``` /files/{file_path:path} ``` -У цьому випадку ім'я параметра — `file_path`, а остання частина `:path` вказує на те, що параметр повинен відповідати будь-якому *шляху*. -Отже, Ви можете використати його так: +У цьому випадку ім’я параметра — `file_path`, а остання частина `:path` вказує, що параметр має відповідати будь-якому *шляху*. -{* ../../docs_src/path_params/tutorial004.py hl[6] *} +Отже, ви можете використати його так: + +{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *} /// tip | Порада @@ -246,15 +237,15 @@ OpenAPI не підтримує спосіб оголошення *параме /// -## Підсумок +## Підсумок { #recap } -З **FastAPI**, використовуючи короткі, інтуїтивно зрозумілі та стандартні оголошення типів Python, Ви отримуєте: +З **FastAPI**, використовуючи короткі, інтуїтивно зрозумілі та стандартні оголошення типів Python, ви отримуєте: -* Підтримку в редакторі: перевірка помилок, автодоповнення тощо. -* "<abbr title="перетворення рядка, що надходить з HTTP-запиту, у типи даних Python">Парсинг</abbr>" даних +* Підтримку редактора: перевірка помилок, автодоповнення тощо. +* Перетворення даних «<abbr title="converting the string that comes from an HTTP request into Python data – перетворення рядка, що надходить з HTTP-запиту, у дані Python">parsing</abbr>» * Валідацію даних * Анотацію API та автоматичну документацію І вам потрібно оголосити їх лише один раз. -Це, ймовірно, основна видима перевага **FastAPI** порівняно з альтернативними фреймворками (окрім високої продуктивності). +Це, ймовірно, основна видима перевага **FastAPI** порівняно з альтернативними фреймворками (окрім сирої продуктивності). diff --git a/docs/uk/docs/tutorial/query-param-models.md b/docs/uk/docs/tutorial/query-param-models.md index 97eb82fa1..a28bf6c27 100644 --- a/docs/uk/docs/tutorial/query-param-models.md +++ b/docs/uk/docs/tutorial/query-param-models.md @@ -1,4 +1,4 @@ -# Моделі Query параметрів +# Моделі параметрів запиту { #query-parameter-models } Якщо у Вас є група **query параметрів**, які пов’язані між собою, Ви можете створити **Pydantic-модель** для їх оголошення. @@ -10,7 +10,7 @@ /// -## Query параметри з Pydantic-моделлю +## Query параметри з Pydantic-моделлю { #query-parameters-with-a-pydantic-model } Оголосіть **query параметри**, які Вам потрібні, у **Pydantic-моделі**, а потім оголосіть цей параметр як `Query`: @@ -18,7 +18,7 @@ **FastAPI** буде **витягувати** дані для **кожного поля** з **query параметрів** у запиті та передавати їх у визначену вами Pydantic-модель. -## Перевірте документацію +## Перевірте документацію { #check-the-docs } Ви можете побачити параметри запиту в UI документації за `/docs`: @@ -26,7 +26,7 @@ <img src="/img/tutorial/query-param-models/image01.png"> </div> -## Заборона зайвих Query параметрів +## Заборона зайвих Query параметрів { #forbid-extra-query-parameters } У деяких особливих випадках (ймовірно, не дуже поширених) Ви можете захотіти **обмежити** query параметри, які дозволено отримувати. @@ -34,7 +34,7 @@ {* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} -Якщо клієнт спробує надіслати **зайві** дані у **query параметрах**, він отримає **помилку**. +Якщо клієнт спробує надіслати **зайві** дані у **query параметрах**, він отримає **помилку** відповідь. Наприклад, якщо клієнт спробує надіслати query параметр `tool` зі значенням `plumbus`, як у цьому запиті: @@ -57,11 +57,11 @@ https://example.com/items/?limit=10&tool=plumbus } ``` -## Підсумок +## Підсумок { #summary } Ви можете використовувати **Pydantic-моделі** для оголошення **query параметрів** у **FastAPI**. 😎 -/// tip | Підказка +/// tip | Порада Спойлер: Ви також можете використовувати Pydantic-моделі для оголошення cookie та заголовків, але про це Ви дізнаєтеся пізніше в цьому посібнику. 🤫 diff --git a/docs/uk/docs/tutorial/query-params-str-validations.md b/docs/uk/docs/tutorial/query-params-str-validations.md index cd3f4ad93..414987880 100644 --- a/docs/uk/docs/tutorial/query-params-str-validations.md +++ b/docs/uk/docs/tutorial/query-params-str-validations.md @@ -1,26 +1,26 @@ -# Query параметри та валідація рядків +# Query параметри та валідація рядків { #query-parameters-and-string-validations } -**FastAPI** дозволяє оголошувати додаткову інформацію та виконувати валідацію для Ваших параметрів. +**FastAPI** дозволяє оголошувати додаткову інформацію та виконувати валідацію для ваших параметрів. Розглянемо цей додаток як приклад: {* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} -Query параметр `q` має тип `str | None`, що означає, що він може бути як `str`, так і `None`. За замовчуванням він має значення `None`, тому FastAPI розуміє, що цей параметр не є обов'язковим. +Query параметр `q` має тип `str | None`, що означає, що він має тип `str`, але також може бути `None`, і справді, значення за замовчуванням — `None`, тож FastAPI знатиме, що він не є обов'язковим. /// note | Примітка -FastAPI знає, що `q` не є обов’язковим, завдяки значенню за замовчуванням `= None`. +FastAPI знатиме, що значення `q` не є обов’язковим, завдяки значенню за замовчуванням `= None`. -Використання `str | None` дозволить Вашому редактору коду надавати кращу підтримку та виявляти помилки. +Використання `str | None` дозволить вашому редактору коду надавати кращу підтримку та виявляти помилки. /// -## Додаткова валідація +## Додаткова валідація { #additional-validation } -Ми хочемо, щоб навіть якщо `q` є необов’язковим, **його довжина не перевищувала 50 символів**, якщо він все ж буде переданий. +Ми хочемо, щоб навіть якщо `q` є необов’язковим, коли його передають, **його довжина не перевищувала 50 символів**. -### Імпорт `Query` та `Annotated` +### Імпорт `Query` та `Annotated` { #import-query-and-annotated } Щоб це зробити, спочатку імпортуємо: @@ -33,13 +33,13 @@ FastAPI знає, що `q` не є обов’язковим, завдяки з FastAPI додав підтримку `Annotated` (і почав рекомендувати його) у версії 0.95.0. -Якщо у Вас старіша версія, під час використання `Annotated` можуть виникати помилки. +Якщо у вас старіша версія, під час використання `Annotated` можуть виникати помилки. -Переконайтеся, що Ви [оновили версію FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} до принаймні 0.95.1, перш ніж використовувати `Annotated`. +Переконайтеся, що ви [оновили версію FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} до принаймні 0.95.1, перш ніж використовувати `Annotated`. /// -## Використання `Annotated` у типі параметра `q` +## Використання `Annotated` у типі параметра `q` { #use-annotated-in-the-type-for-the-q-parameter } Пам’ятаєте, як я раніше розповідав, що `Annotated` можна використовувати для додавання метаданих до параметрів у [Вступі до типів Python](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? @@ -55,7 +55,7 @@ q: str | None = None //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python q: Union[str, None] = None @@ -73,7 +73,7 @@ q: Annotated[str | None] = None //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python q: Annotated[Union[str, None]] = None @@ -85,33 +85,33 @@ q: Annotated[Union[str, None]] = None А тепер переходимо до цікавого! 🎉 -## Додавання `Query` до `Annotated` у параметр `q` +## Додавання `Query` до `Annotated` у параметр `q` { #add-query-to-annotated-in-the-q-parameter } -Тепер, коли у нас є `Annotated`, де ми можемо додавати додаткову інформацію (зокрема валідацію), додамо `Query` всередину `Annotated` і встановимо параметр `max_length` у `50`: +Тепер, коли у нас є `Annotated`, де ми можемо додавати додаткову інформацію (у цьому випадку — додаткову валідацію), додамо `Query` всередину `Annotated` і встановимо параметр `max_length` у `50`: {* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} Зверніть увагу, що значення за замовчуванням усе ще `None`, тому параметр залишається необов'язковим. -Але тепер, додавши `Query(max_length=50)` всередину `Annotated`, ми повідомляємо FastAPI, що хочемо **додаткову валідацію** для цього значення — воно має містити максимум 50 символів. 😎 +Але тепер, додавши `Query(max_length=50)` всередину `Annotated`, ми повідомляємо FastAPI, що хочемо **додаткову валідацію** для цього значення: ми хочемо, щоб воно мало максимум 50 символів. 😎 -/// tip | Підказка +/// tip | Порада -Ми використовуємо `Query()`, оскільки це **query параметр**. Далі ми розглянемо інші варіанти, як-от `Path()`, `Body()`, `Header()` та `Cookie()`, які приймають ті самі аргументи, що й `Query()`. +Тут ми використовуємо `Query()`, оскільки це **query параметр**. Далі ми розглянемо інші варіанти, як-от `Path()`, `Body()`, `Header()` та `Cookie()`, які приймають ті самі аргументи, що й `Query()`. /// Тепер FastAPI: -* **Перевірить** дані, щоб переконатися, що їхня довжина не перевищує 50 символів +* **Перевірить** дані, щоб переконатися, що їхня максимальна довжина — 50 символів * Покажe **чітку помилку** клієнту, якщо дані недійсні * **Задокументує** параметр в OpenAPI-схемі *операції шляху* (що відобразиться в **автоматично згенерованій документації**) -## Альтернативний (застарілий) метод: Query як значення за замовчуванням +## Альтернативний (застарілий) метод: `Query` як значення за замовчуванням { #alternative-old-query-as-the-default-value } -У попередніх версіях FastAPI (до <abbr title="до 2023-03">0.95.0</abbr>) `Query` використовувався як значення за замовчуванням для параметра, а не всередині `Annotated`. Ви, ймовірно, побачите код, який використовує цей підхід, тому варто розглянути його. +У попередніх версіях FastAPI (до <abbr title="before 2023-03 – до 2023-03">0.95.0</abbr>) потрібно було використовувати `Query` як значення за замовчуванням параметра, замість того, щоб додавати його в `Annotated`. Є висока ймовірність, що ви зустрінете код із таким підходом, тож я поясню його. -/// tip | Підказка +/// tip | Порада Для нового коду та коли це можливо, використовуйте `Annotated`, як показано вище. Це має багато переваг (пояснених нижче) і не має недоліків. 🍰 @@ -121,7 +121,7 @@ q: Annotated[Union[str, None]] = None {* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} -Оскільки в цьому випадку (без `Annotated`) нам потрібно замінити `None` у функції на `Query()`, тепер ми повинні явно встановити значення за замовчуванням через параметр `Query(default=None)`. Це виконує ту саму роль визначення значення за замовчуванням (принаймні для FastAPI). +Оскільки в цьому випадку (без `Annotated`) нам потрібно замінити значення за замовчуванням `None` у функції на `Query()`, тепер ми повинні встановити значення за замовчуванням через параметр `Query(default=None)`. Це виконує ту саму роль визначення значення за замовчуванням (принаймні для FastAPI). Таким чином: @@ -135,9 +135,10 @@ q: str | None = Query(default=None) ```Python q: str | None = None ``` -Але у версії з `Query` ми явно вказуємо, що це query параметр. -Далі ми можемо передавати `Query` додаткові параметри, зокрема `max_length`, який застосовується до рядків: +Але у версії з `Query` ми явно вказуємо, що це query параметр. + +Далі ми можемо передавати `Query` додаткові параметри. У цьому випадку — параметр `max_length`, який застосовується до рядків: ```Python q: str | None = Query(default=None, max_length=50) @@ -145,11 +146,11 @@ q: str | None = Query(default=None, max_length=50) Це забезпечить валідацію даних, виведе зрозумілу помилку у разі недійсних даних і задокументує параметр у схемі OpenAPI *операції шляху*. -### `Query` як значення за замовчуванням або всередині `Annotated` +### `Query` як значення за замовчуванням або всередині `Annotated` { #query-as-the-default-value-or-in-annotated } Важливо пам’ятати, якщо використовувати `Query` всередині `Annotated`, не можна задавати параметр `default` у `Query`. -Замість цього використовуйте значення за замовчуванням у самій функції. Інакше це буде нелогічно. +Замість цього використовуйте фактичне значення за замовчуванням параметра функції. Інакше це буде непослідовно. Наприклад, цей варіант є некоректним: @@ -159,39 +160,39 @@ q: Annotated[str, Query(default="rick")] = "morty" ...тому, що не зрозуміло, яке значення має бути значенням за замовчуванням: `"rick"` чи `"morty"`. -Коректні варіанти: +Тож ви будете використовувати (бажано): ```Python q: Annotated[str, Query()] = "rick" ``` -...або у старих кодових базах Ви знайдете: +...або у старих кодових базах ви знайдете: ```Python q: str = Query(default="rick") ``` -### Переваги використання `Annotated` +### Переваги використання `Annotated` { #advantages-of-annotated } **Використання `Annotated` є рекомендованим** замість задання значення за замовчуванням у параметрах функції, оскільки воно **краще** з кількох причин. 🤓 -Значення **за замовчуванням** параметра **функції** є його **фактичним значенням за замовчуванням**, що є більш інтуїтивним у Python загалом. 😌 +Значення **за замовчуванням** параметра **функції** є **фактичним значенням за замовчуванням**, що є більш інтуїтивним у Python загалом. 😌 -Ви можете **викликати** ту саму функцію **в інших місцях** без FastAPI, і вона **працюватиме очікувано**. Якщо параметр є **обов’язковим** (без значення за замовчуванням), Ваш **редактор** повідомить про помилку, а **Python** також видасть помилку, якщо Ви виконаєте функцію без передавання цього параметра. +Ви можете **викликати** ту саму функцію **в інших місцях** без FastAPI, і вона **працюватиме очікувано**. Якщо параметр є **обов’язковим** (без значення за замовчуванням), ваш **редактор** повідомить про помилку, а **Python** також видасть помилку, якщо ви виконаєте функцію без передавання цього параметра. -Якщо Ви не використовуєте `Annotated`, а використовуєте **(старий) стиль значень за замовчуванням**, то при виклику цієї функції без FastAPI **в інших місцях**, потрібно **не забути** передати їй аргументи, інакше значення будуть відрізнятися від очікуваних (наприклад, Ви отримаєте `QueryInfo` або подібне замість `str`). Ваш редактор не повідомить про помилку, і Python також не видасть помилку при запуску функції, поки не виникне помилка під час виконання операцій усередині. +Якщо ви не використовуєте `Annotated`, а використовуєте **(старий) стиль значень за замовчуванням**, то при виклику цієї функції без FastAPI **в інших місцях**, потрібно **пам’ятати** передати їй аргументи, щоб вона працювала коректно, інакше значення будуть відрізнятися від очікуваних (наприклад, ви отримаєте `QueryInfo` або щось подібне замість `str`). І ваш редактор не повідомить про помилку, і Python не скаржитиметься під час запуску цієї функції — лише коли операції всередині завершаться помилкою. -Оскільки `Annotated` може містити кілька анотацій метаданих, Ви навіть можете використовувати ту саму функцію з іншими інструментами, такими як <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">Typer</a>. 🚀 +Оскільки `Annotated` може містити кілька анотацій метаданих, тепер ви навіть можете використовувати ту саму функцію з іншими інструментами, такими як <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">Typer</a>. 🚀 -## Додавання додаткових валідацій +## Додавання додаткових валідацій { #add-more-validations } Ви також можете додати параметр `min_length`: {* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} -## Додавання регулярних виразів +## Додавання регулярних виразів { #add-regular-expressions } -Ви можете визначити <abbr title="Регулярний вираз (regex або regexp) — це послідовність символів, яка визначає шаблон для пошуку в рядках.">регулярний вираз</abbr> pattern, якому має відповідати параметр: +Ви можете визначити <abbr title="A regular expression, regex or regexp is a sequence of characters that define a search pattern for strings. – Регулярний вираз (regex або regexp) — це послідовність символів, яка визначає шаблон для пошуку в рядках.">regular expression</abbr> `pattern`, якому має відповідати параметр: {* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} @@ -201,41 +202,27 @@ q: str = Query(default="rick") * `fixedquery`: точно відповідає значенню `fixedquery`. * `$`: закінчується тут, після `fixedquery` немає жодних символів. -Якщо Ви почуваєтеся розгублено щодо **"регулярних виразів"**, не хвилюйтеся. Вони є складною темою для багатьох людей. Ви все одно можете зробити багато речей без їх використання. +Якщо ви почуваєтеся розгублено щодо **«regular expression»**, не хвилюйтеся. Це складна тема для багатьох людей. Ви все одно можете робити багато речей без використання регулярних виразів. -Але тепер Ви знаєте, що коли вони знадобляться, їх можна застосовувати у **FastAPI**. +Тепер ви знаєте, що коли вони знадобляться, їх можна застосовувати у **FastAPI**. -### Pydantic v1 `regex` замість `pattern` +## Значення за замовчуванням { #default-values } -До версії Pydantic 2 і FastAPI 0.100.0 параметр називався `regex` замість `pattern`, але тепер він застарів. +Ви можете, звісно, використовувати значення за замовчуванням, відмінні від `None`. -Ви все ще можете зустріти код, який використовує його: - -//// tab | Pydantic v1 - -{* ../../docs_src/query_params_str_validations/tutorial004_regex_an_py310.py hl[11] *} - -//// - -Але майте на увазі, що він є застарілим і його слід оновити до нового параметра `pattern`. 🤓 - -## Значення за замовчуванням - -Ви можете використовувати значення за замовчуванням, відмінні від `None`. - -Наприклад, якщо Ви хочете оголосити параметр запиту `q` з `min_length` `3` і значенням за замовчуванням `"fixedquery"`: +Припустімо, що ви хочете оголосити query параметр `q` з `min_length` `3` і значенням за замовчуванням `"fixedquery"`: {* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *} -/// note | Технічні деталі +/// note | Примітка Наявність значення за замовчуванням будь-якого типу, включаючи `None`, робить параметр необов’язковим (not required). /// -## Обов’язкові параметри +## Обов’язкові параметри { #required-parameters } -Якщо нам не потрібно вказувати додаткові перевірки або метадані, ми можемо зробити параметр `q` обов’язковим, просто не оголошуючи значення за замовчуванням, наприклад: +Якщо нам не потрібно оголошувати додаткові валідації або метадані, ми можемо зробити query параметр `q` обов’язковим, просто не вказуючи значення за замовчуванням, наприклад: ```Python q: str @@ -247,43 +234,39 @@ q: str q: str | None = None ``` -Але тепер ми оголошуємо його з `Query`, наприклад: - -//// tab | Annotated +Але тепер ми оголошуємо його з `Query`, наприклад так: ```Python q: Annotated[str | None, Query(min_length=3)] = None ``` -//// - -Тому, якщо Вам потрібно зробити значення обов’язковим, використовуючи `Query`, просто не вказуйте значення за замовчуванням: +Тому, якщо вам потрібно оголосити значення як обов’язкове під час використання `Query`, просто не вказуйте значення за замовчуванням: {* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -### Обов’язкове значення, яке може бути `None` +### Обов’язковий, може бути `None` { #required-can-be-none } -Ви можете вказати, що параметр може приймати `None`, але при цьому залишається обов’язковим. Це змусить клієнтів надіслати значення, навіть якщо воно дорівнює `None`. +Ви можете вказати, що параметр може приймати `None`, але при цьому залишається обов’язковим. Це змусить клієнтів надіслати значення, навіть якщо значення дорівнює `None`. -Щоб зробити це, оголосіть, що `None` є допустимим типом, але не вказуйте значення за замовчуванням: +Щоб зробити це, оголосіть, що `None` є допустимим типом, але просто не вказуйте значення за замовчуванням: {* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} -## Список параметрів запиту / кілька значень +## Список query параметрів / кілька значень { #query-parameter-list-multiple-values } -Якщо Ви визначаєте параметр запиту за допомогою `Query`, Ви також можете дозволити отримання списку значень, тобто дозволити отримання кількох значень. +Коли ви явно визначаєте query параметр за допомогою `Query`, ви також можете оголосити, що він має приймати список значень, або, іншими словами, кілька значень. -Наприклад, щоб дозволити параметру запиту `q` з'являтися кілька разів в URL, можна написати: +Наприклад, щоб оголосити query параметр `q`, який може з’являтися в URL кілька разів, можна написати: {* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *} -Тоді, у випадку запиту за URL: +Тоді, у випадку URL: ``` http://localhost:8000/items/?q=foo&q=bar ``` -Ви отримаєте кілька значень *query параметра* `q` (`foo` і `bar`) у вигляді списку `list` в Python у Вашій *функції обробки шляху*, у *параметрі функції* `q`. +ви отримаєте кілька значень `q` *query параметрів* (`foo` і `bar`) у вигляді Python `list` у вашій *функції операції шляху*, у *параметрі функції* `q`. Отже, відповідь на цей URL буде: @@ -296,9 +279,9 @@ http://localhost:8000/items/?q=foo&q=bar } ``` -/// tip | Підказка +/// tip | Порада -Щоб оголосити параметр запиту з типом `list`, як у наведеному вище прикладі, потрібно явно використовувати `Query`, інакше він буде інтерпретований як тіло запиту. +Щоб оголосити query параметр з типом `list`, як у наведеному вище прикладі, потрібно явно використовувати `Query`, інакше він буде інтерпретований як тіло запиту. /// @@ -306,19 +289,19 @@ http://localhost:8000/items/?q=foo&q=bar <img src="/img/tutorial/query-params-str-validations/image02.png"> -### Список параметрів запиту / кілька значень за замовчуванням +### Список query параметрів / кілька значень за замовчуванням { #query-parameter-list-multiple-values-with-defaults } -Ви також можете визначити значення за замовчуванням для `list`, якщо жодне значення не було передане: +Ви також можете визначити значення за замовчуванням `list`, якщо жодне значення не було передане: {* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} -Якщо Ви перейдете за посиланням: +Якщо ви перейдете за посиланням: ``` http://localhost:8000/items/ ``` -то значення `q` за замовчуванням буде: `["foo", "bar"]`, і Ваша відповідь виглядатиме так: +то значення `q` за замовчуванням буде: `["foo", "bar"]`, і ваша відповідь виглядатиме так: ```JSON { @@ -329,35 +312,35 @@ http://localhost:8000/items/ } ``` -#### Використання тільки `list` +#### Використання тільки `list` { #using-just-list } -Ви також можете використовувати `list` без уточнення типу, замість `list[str]`: +Ви також можете використовувати `list` напряму замість `list[str]`: {* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} -/// note | Технічні деталі +/// note | Примітка Майте на увазі, що в цьому випадку FastAPI не перевірятиме вміст списку. -Наприклад, `list[int]` перевірятиме (і документуватиме), що всі елементи списку є цілими числами. Але `list` без уточнення цього не робитиме. +Наприклад, `list[int]` перевірятиме (і документуватиме), що вміст списку — цілі числа. Але `list` без уточнення цього не робитиме. /// -## Додавання додаткових метаданих +## Оголосити більше метаданих { #declare-more-metadata } Ви можете додати більше інформації про параметр. -Ця інформація буде включена у згенерований OpenAPI та використана в інтерфейсах документації та зовнішніх інструментах. +Ця інформація буде включена у згенерований OpenAPI та використана інтерфейсами документації та зовнішніми інструментами. -/// note | Технічні деталі +/// note | Примітка Майте на увазі, що різні інструменти можуть мати різний рівень підтримки OpenAPI. -Деякі з них можуть ще не відображати всю додаткову інформацію, хоча в більшості випадків ця функція вже запланована для розробки. +Деякі з них можуть ще не відображати всю додаткову інформацію, хоча в більшості випадків відсутню функцію вже заплановано до реалізації. /// -Ви можете додати `title` : +Ви можете додати `title`: {* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *} @@ -365,9 +348,9 @@ http://localhost:8000/items/ {* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} -## Аліаси параметрів +## Аліаси параметрів { #alias-parameters } -Уявіть, що Ви хочете, щоб параметр називався `item-query`. +Уявіть, що ви хочете, щоб параметр називався `item-query`. Наприклад: @@ -379,19 +362,19 @@ http://127.0.0.1:8000/items/?item-query=foobaritems Найближчий допустимий варіант — `item_query`. -Проте Вам потрібно, щоб параметр залишався саме `item-query`... +Проте вам потрібно, щоб параметр залишався саме `item-query`... У такому випадку можна оголосити `alias`, і саме він буде використовуватися для отримання значення параметра: {* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} -## Виведення параметрів як застарілих +## Позначення параметрів як застарілих { #deprecating-parameters } -Припустимо, що Ви більше не хочете використовувати цей параметр. +Припустімо, що вам більше не подобається цей параметр. -Вам потрібно залишити його на деякий час, оскільки ним користуються клієнти, але Ви хочете, щоб документація чітко показувала, що він є <abbr title="застарілий, не рекомендується до використання">застарілим</abbr>. +Вам потрібно залишити його на деякий час, оскільки ним користуються клієнти, але ви хочете, щоб документація чітко показувала, що він є <abbr title="obsolete, recommended not to use it – застарілий, не рекомендується до використання">deprecated</abbr>. -Тоді Ви можете передати параметр `deprecated=True` до `Query`: +Тоді передайте параметр `deprecated=True` до `Query`: {* ../../docs_src/query_params_str_validations/tutorial010_an_py310.py hl[19] *} @@ -399,27 +382,27 @@ http://127.0.0.1:8000/items/?item-query=foobaritems <img src="/img/tutorial/query-params-str-validations/image01.png"> -## Виняток параметрів з OpenAPI +## Виняток параметрів з OpenAPI { #exclude-parameters-from-openapi } -Щоб виключити параметр запиту зі згенерованої схеми OpenAPI (і, таким чином, з автоматичних систем документації), встановіть параметр `include_in_schema` для `Query` в `False`: +Щоб виключити query параметр зі згенерованої схеми OpenAPI (і, таким чином, з автоматичних систем документації), встановіть параметр `include_in_schema` для `Query` в `False`: {* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} -## Кастомна валідація +## Кастомна валідація { #custom-validation } -Можуть бути випадки, коли Вам потрібно провести **кастомну валідацію**, яку не можна реалізувати за допомогою параметрів, показаних вище. +Можуть бути випадки, коли вам потрібно провести **кастомну валідацію**, яку не можна реалізувати за допомогою параметрів, показаних вище. -У таких випадках ви можете використати **кастомну функцію валідації**, яка буде застосована після звичайної валідації (наприклад, після перевірки, що значення є типом `str`). +У таких випадках ви можете використати **кастомну функцію-валідатор**, яка буде застосована після звичайної валідації (наприклад, після перевірки, що значення є типом `str`). Це можна досягти за допомогою <a href="https://docs.pydantic.dev/latest/concepts/validators/#field-after-validator" class="external-link" target="_blank">Pydantic's `AfterValidator`</a> в середині `Annotated`. -/// tip | Підказка +/// tip | Порада Pydantic також має <a href="https://docs.pydantic.dev/latest/concepts/validators/#field-before-validator" class="external-link" target="_blank">`BeforeValidator`</a> та інші. 🤓 /// -Наприклад, цей кастомний валідатор перевіряє, чи починається ID елемента з `isbn-` для номера книги <abbr title="ISBN означає Міжнародний стандартний номер книги">ISBN</abbr> або з `imdb-` для ID URL фільму на <abbr title="IMDB (Internet Movie Database) це вебсайт з інформацією про фільми">IMDB</abbr>: +Наприклад, цей кастомний валідатор перевіряє, чи починається ID елемента з `isbn-` для номера книги <abbr title="ISBN means International Standard Book Number – ISBN означає Міжнародний стандартний номер книги">ISBN</abbr> або з `imdb-` для ID URL фільму на <abbr title="IMDB (Internet Movie Database) is a website with information about movies – IMDB (Internet Movie Database) це вебсайт з інформацією про фільми">IMDB</abbr>: {* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} @@ -429,49 +412,49 @@ Pydantic також має <a href="https://docs.pydantic.dev/latest/concepts/va /// -/// tip | Підказка +/// tip | Порада -Якщо Вам потрібно виконати будь-яку валідацію, яка вимагає взаємодії з будь-яким **зовнішнім компонентом**, таким як база даних чи інший API, ви повинні замість цього використовувати **FastAPI Dependencies**. Ви дізнаєтесь про них пізніше. +Якщо вам потрібно виконати будь-яку валідацію, яка вимагає взаємодії з будь-яким **зовнішнім компонентом**, таким як база даних чи інший API, замість цього слід використовувати **FastAPI Dependencies** — ви дізнаєтесь про них пізніше. -Ці кастомні валідатори використовуються для речей, які можна перевірити лише з **тими даними**, що надані в запиті. +Ці кастомні валідатори використовуються для речей, які можна перевірити лише з **тіими самими даними**, що надані в запиті. /// -### Зрозумійте цей код +### Зрозумійте цей код { #understand-that-code } -Головний момент – це використання **`AfterValidator` з функцією всередині `Annotated`**. Можете пропустити цю частину, якщо хочете. 🤸 +Головний момент — це використання **`AfterValidator` з функцією всередині `Annotated`**. Можете пропустити цю частину, якщо хочете. 🤸 --- -Але якщо Вам цікаво розібратися в цьому конкретному прикладі коду і Вам ще не набридло, ось кілька додаткових деталей. +Але якщо вам цікаво розібратися в цьому конкретному прикладі коду і вам ще не набридло, ось кілька додаткових деталей. -#### Рядок із `value.startswith()` +#### Рядок із `value.startswith()` { #string-with-value-startswith } Звернули увагу? Рядок із `value.startswith()` може приймати кортеж, і тоді він перевірятиме кожне значення в кортежі: {* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *} -#### Випадковий елемент +#### Випадковий елемент { #a-random-item } -За допомогою `data.items()` ми отримуємо <abbr title="Об'єкт, який можна перебирати в циклі, як-от список чи множину.">ітерабельний об'єкт</abbr> із кортежами, що містять ключ і значення для кожного елемента словника. +За допомогою `data.items()` ми отримуємо <abbr title="Something we can iterate on with a for loop, like a list, set, etc. – Об'єкт, який можна перебирати в циклі, як-от список чи множину.">iterable object</abbr> із кортежами, що містять ключ і значення для кожного елемента словника. -Ми перетворюємо цей ітерабельний об'єкт у звичайний `list` за допомогою `list(data.items())`. +Ми перетворюємо цей iterable object у звичайний `list` за допомогою `list(data.items())`. -Потім, використовуючи `random.choice()`, ми можемо отримати випадкове значення зі списку, тобто отримуємо кортеж із `(id, name)`. Це може бути щось на зразок `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`. +Потім, використовуючи `random.choice()`, ми можемо отримати **випадкове значення** зі списку, тобто отримуємо кортеж із `(id, name)`. Це може бути щось на зразок `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`. Далі ми **присвоюємо ці два значення** кортежу змінним `id` і `name`. Тож, якщо користувач не вказав ID елемента, він все одно отримає випадкову рекомендацію. -...і все це реалізовано в **одному рядку коду**. 🤯 Хіба не прекрасний Python? 🐍 +...ми робимо все це в **одному простому рядку**. 🤯 Хіба ви не любите Python? 🐍 {* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} -## Підсумок +## Підсумок { #recap } -Ви можете оголошувати додаткові валідації та метаінформацію для своїх параметрів. +Ви можете оголошувати додаткові валідації та метадані для ваших параметрів. -Загальні валідації та метаінформація: +Загальні валідації та метадані: * `alias` * `title` @@ -486,6 +469,6 @@ Pydantic також має <a href="https://docs.pydantic.dev/latest/concepts/va Кастомні валідації за допомогою `AfterValidator`. -У цих прикладах Ви побачили, як оголошувати валідації для значень `str`. +У цих прикладах ви побачили, як оголошувати валідації для значень `str`. Дивіться наступні розділи, щоб дізнатися, як оголошувати валідації для інших типів, наприклад чисел. diff --git a/docs/uk/docs/tutorial/query-params.md b/docs/uk/docs/tutorial/query-params.md index 4b75ecab9..a9068aa8f 100644 --- a/docs/uk/docs/tutorial/query-params.md +++ b/docs/uk/docs/tutorial/query-params.md @@ -1,10 +1,10 @@ -# Query Параметри +# Query параметри { #query-parameters } -Коли Ви оголошуєте інші параметри функції, які не є частиною параметрів шляху, вони автоматично інтерпретуються як "query" параметри. +Коли ви оголошуєте інші параметри функції, які не є частиною параметрів шляху, вони автоматично інтерпретуються як параметри «query». -{* ../../docs_src/query_params/tutorial001.py hl[9] *} +{* ../../docs_src/query_params/tutorial001_py39.py hl[9] *} -Query параметри — це набір пар ключ-значення, що йдуть після символу `?` в URL, розділені символами `&`. +Query — це набір пар ключ-значення, що йдуть після символу `?` в URL, розділені символами `&`. Наприклад, в URL: @@ -12,41 +12,41 @@ Query параметри — це набір пар ключ-значення, http://127.0.0.1:8000/items/?skip=0&limit=10 ``` -...query параметрами є: +...параметрами query є: * `skip`: зі значенням `0` * `limit`: зі значенням `10` -Оскільки вони є частиною URL, вони "за замовчуванням" є рядками. +Оскільки вони є частиною URL, вони «природно» є рядками. -Але коли Ви оголошуєте їх із типами Python (у наведеному прикладі як `int`), вони перетворюються на цей тип і проходять перевірку відповідності. +Але коли ви оголошуєте їх із типами Python (у наведеному прикладі як `int`), вони перетворюються на цей тип і проходять перевірку відповідності. -Увесь той самий процес, який застосовується до параметрів шляху, також застосовується до query параметрів: +Увесь той самий процес, який застосовується до параметрів шляху, також застосовується до параметрів query: -* Підтримка в редакторі (автодоповнення, перевірка помилок) -* <abbr title="перетворення рядка, що надходить з HTTP-запиту, у типи даних Python">"Парсинг"</abbr> даних +* Підтримка в редакторі (очевидно) +* <abbr title="converting the string that comes from an HTTP request into Python data – перетворення рядка, що надходить з HTTP-запиту, у дані Python">«parsing»</abbr> даних * Валідація даних * Автоматична документація +## Значення за замовчуванням { #defaults } -## Значення за замовчуванням - -Оскільки query параметри не є фіксованою частиною шляху, вони можуть бути необов’язковими та мати значення за замовчуванням. +Оскільки параметри query не є фіксованою частиною шляху, вони можуть бути необов’язковими та мати значення за замовчуванням. У наведеному вище прикладі вони мають значення за замовчуванням: `skip=0` і `limit=10`. -Отже, результат переходу за URL: +Отже, перехід за URL: ``` http://127.0.0.1:8000/items/ ``` + буде таким самим, як і перехід за посиланням: ``` http://127.0.0.1:8000/items/?skip=0&limit=10 ``` -Але якщо Ви перейдете, наприклад, за посиланням: +Але якщо ви перейдете, наприклад, за посиланням: ``` http://127.0.0.1:8000/items/?skip=20 @@ -54,12 +54,12 @@ http://127.0.0.1:8000/items/?skip=20 Значення параметрів у вашій функції будуть такими: -* `skip=20`: оскільки Ви вказали його в URL +* `skip=20`: оскільки ви вказали його в URL * `limit=10`: оскільки це значення за замовчуванням -## Необов'язкові параметри +## Необов'язкові параметри { #optional-parameters } -Аналогічно, Ви можете оголосити необов’язкові query параметри, встановивши для них значення за замовчуванням `None`: +Так само ви можете оголосити необов’язкові параметри query, встановивши для них значення за замовчуванням `None`: {* ../../docs_src/query_params/tutorial002_py310.py hl[7] *} @@ -67,18 +67,17 @@ http://127.0.0.1:8000/items/?skip=20 /// check | Примітка -Також зверніть увагу, що **FastAPI** достатньо розумний, щоб визначити, що параметр шляху `item_id` є параметром шляху, а `q` — ні, отже, це query параметр. +Також зверніть увагу, що **FastAPI** достатньо розумний, щоб визначити, що параметр шляху `item_id` є параметром шляху, а `q` — ні, отже, це параметр query. /// -## Перетворення типу Query параметра +## Перетворення типу параметра query { #query-parameter-type-conversion } Ви також можете оголошувати параметри типу `bool`, і вони будуть автоматично конвертовані: {* ../../docs_src/query_params/tutorial003_py310.py hl[7] *} -У цьому випадку, якщо Ви звернетесь до: - +У цьому випадку, якщо ви перейдете за: ``` http://127.0.0.1:8000/items/foo?short=1 @@ -108,38 +107,38 @@ http://127.0.0.1:8000/items/foo?short=on http://127.0.0.1:8000/items/foo?short=yes ``` -або будь-який інший варіант написання (великі літери, перша літера велика тощо), ваша функція побачить параметр `short` зі значенням `True` з типом даних `bool`. В іншому випадку – `False`. - -## Кілька path і query параметрів - -Ви можете одночасно оголошувати кілька path і query параметрів, і **FastAPI** автоматично визначить, який з них до чого належить. +або будь-який інший варіант написання (великі літери, перша літера велика тощо), ваша функція побачить параметр `short` зі значенням `True` типу `bool`. В іншому випадку — `False`. -Не потрібно дотримуватись певного порядку їх оголошення. +## Кілька path і query параметрів { #multiple-path-and-query-parameters } + +Ви можете одночасно оголошувати кілька параметрів шляху та параметрів query, **FastAPI** знає, який з них який. + +І вам не потрібно оголошувати їх у якомусь конкретному порядку. Вони визначаються за назвою: {* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## Обов’язкові Query параметри +## Обов’язкові параметри query { #required-query-parameters } -Якщо Ви оголошуєте значення за замовчуванням для параметрів, які не є path-параметрами (у цьому розділі ми бачили поки що лише path параметри), тоді вони стають необов’язковими. +Коли ви оголошуєте значення за замовчуванням для не-path-параметрів (поки що ми бачили лише параметри query), тоді вони не є обов’язковими. -Якщо Ви не хочете вказувати конкретні значення, але хочете зробити параметр опціональним, задайте `None` як значення за замовчуванням. +Якщо ви не хочете задавати конкретне значення, а просто зробити параметр необов’язковим, задайте `None` як значення за замовчуванням. -Але якщо Ви хочете зробити query параметр обов’язковим, просто не вказуйте для нього значення за замовчуванням: +Але якщо ви хочете зробити параметр query обов’язковим, просто не вказуйте для нього значення за замовчуванням: -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} -Тут `needy` – обов’язковий query параметр типу `str`. +Тут параметр query `needy` — обов’язковий параметр query типу `str`. -Якщо Ви відкриєте у браузері URL-адресу: +Якщо ви відкриєте у браузері URL-адресу: ``` http://127.0.0.1:8000/items/foo-item ``` -...без додавання обов’язкового параметра `needy`, Ви побачите помилку: +...без додавання обов’язкового параметра `needy`, ви побачите помилку на кшталт: ```JSON { @@ -163,7 +162,7 @@ http://127.0.0.1:8000/items/foo-item http://127.0.0.1:8000/items/foo-item?needy=sooooneedy ``` -...цей запит поверне: +...це спрацює: ```JSON { @@ -172,20 +171,18 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy } ``` - -Звичайно, Ви можете визначити деякі параметри як обов’язкові, інші зі значенням за замовчуванням, а ще деякі — повністю опціональні: +І звісно, ви можете визначити деякі параметри як обов’язкові, деякі — зі значенням за замовчуванням, а деякі — повністю необов’язкові: {* ../../docs_src/query_params/tutorial006_py310.py hl[8] *} -У цьому випадку є 3 query параметри: +У цьому випадку є 3 параметри query: * `needy`, обов’язковий `str`. * `skip`, `int` зі значенням за замовчуванням `0`. -* `limit`, опціональний `int`. +* `limit`, необов’язковий `int`. +/// tip | Порада -/// tip | Підказка - -Ви також можете використовувати `Enum`-и, так само як і з [Path Parameters](path-params.md#predefined-values){.internal-link target=_blank}. +Ви також можете використовувати `Enum` так само, як і з [Path Parameters](path-params.md#predefined-values){.internal-link target=_blank}. /// diff --git a/docs/uk/docs/tutorial/request-files.md b/docs/uk/docs/tutorial/request-files.md index 18b7cc01c..a6ff70dc0 100644 --- a/docs/uk/docs/tutorial/request-files.md +++ b/docs/uk/docs/tutorial/request-files.md @@ -1,56 +1,56 @@ -# Запит файлів +# Запит файлів { #request-files } Ви можете визначити файли, які будуть завантажуватися клієнтом, використовуючи `File`. /// info | Інформація -Щоб отримувати завантажені файли, спочатку встановіть <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">python-multipart</a>. +Щоб отримувати завантажені файли, спочатку встановіть <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>. -Переконайтеся, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його та встановили пакет, наприклад: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили пакет, наприклад: ```console $ pip install python-multipart ``` -Це необхідно, оскільки завантажені файли передаються у вигляді "форматованих даних форми". +Це необхідно, оскільки завантажені файли передаються у вигляді «form data». /// -## Імпорт `File` +## Імпорт `File` { #import-file } Імпортуйте `File` та `UploadFile` з `fastapi`: {* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *} -## Визначення параметрів `File` +## Визначення параметрів `File` { #define-file-parameters } -Створіть параметри файлів так само як Ви б створювали `Body` або `Form`: +Створіть параметри файлів так само як ви б створювали `Body` або `Form`: {* ../../docs_src/request_files/tutorial001_an_py39.py hl[9] *} /// info | Інформація -`File` — це клас, який безпосередньо успадковує `Form`. +`File` — це клас, який безпосередньо успадковує `Form`. -Але пам’ятайте, що коли Ви імпортуєте `Query`, `Path`, `File` та інші з `fastapi`, це насправді функції, які повертають спеціальні класи. +Але пам’ятайте, що коли ви імпортуєте `Query`, `Path`, `File` та інші з `fastapi`, це насправді функції, які повертають спеціальні класи. /// -/// tip | Підказка +/// tip | Порада -Щоб оголосити тіла файлів, Вам потрібно використовувати `File`, тому що інакше параметри будуть інтерпретовані як параметри запиту або параметри тіла (JSON). +Щоб оголосити тіла файлів, вам потрібно використовувати `File`, тому що інакше параметри будуть інтерпретовані як параметри запиту або параметри тіла (JSON). /// -Файли будуть завантажені у вигляді "форматованих даних форми". +Файли будуть завантажені у вигляді «form data». -Якщо Ви оголосите тип параметра функції обробника маршруту як `bytes`, **FastAPI** прочитає файл за Вас, і Ви отримаєте його вміст у вигляді `bytes`. +Якщо ви оголосите тип параметра *функції операції шляху* як `bytes`, **FastAPI** прочитає файл за вас, і ви отримаєте його вміст у вигляді `bytes`. -Однак майте на увазі, що весь вміст буде збережено в пам'яті. Це працюватиме добре для малих файлів. +Майте на увазі, що це означає, що весь вміст буде збережено в пам'яті. Це працюватиме добре для малих файлів. -Але в деяких випадках Вам може знадобитися `UploadFile`. +Але є кілька випадків, у яких вам може бути корисно використовувати `UploadFile`. -## Параметри файлу з `UploadFile` +## Параметри файлу з `UploadFile` { #file-parameters-with-uploadfile } Визначте параметр файлу з типом `UploadFile`: @@ -59,38 +59,39 @@ $ pip install python-multipart Використання `UploadFile` має кілька переваг перед `bytes`: * Вам не потрібно використовувати `File()` у значенні за замовчуванням параметра. -* Використовується "буферизований" файл: - * Файл зберігається в пам'яті до досягнення певного обмеження, після чого він записується на диск. -* Це означає, що він добре працює для великих файлів, таких як зображення, відео, великі двійкові файли тощо, не споживаючи всю пам'ять. -Ви можете отримати метадані про завантажений файл. -* Він має <a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> `асинхронний файловий інтерфейс` interface. -* Він надає фактичний об'єкт Python <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a>, який можна передавати безпосередньо іншим бібліотекам. +* Використовується «spooled» файл: + * Файл зберігається в пам'яті до досягнення максимального обмеження розміру, після чого він буде збережений на диску. +* Це означає, що він добре працюватиме для великих файлів, таких як зображення, відео, великі двійкові файли тощо, не споживаючи всю пам'ять. +* Ви можете отримати метадані про завантажений файл. +* Він має <a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> `async` інтерфейс. +* Він надає фактичний об'єкт Python <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a>, який можна передавати безпосередньо іншим бібліотекам, що очікують file-like об'єкт. -### `UploadFile` +### `UploadFile` { #uploadfile } `UploadFile` має такі атрибути: * `filename`: Рядок `str` з оригінальною назвою файлу, який був завантажений (наприклад, `myimage.jpg`). -* `content_type`: Рядок `str` з MIME-типом (наприклад, `image/jpeg`). -* `file`: Об'єкт <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">SpooledTemporaryFile</a> (<a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">файлоподібний</a> об'єкт). Це фактичний файловий об'єкт Python, який можна безпосередньо передавати іншим функціям або бібліотекам, що очікують "файлоподібний" об'єкт. +* `content_type`: Рядок `str` з типом вмісту (MIME type / media type) (наприклад, `image/jpeg`). +* `file`: <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a> (<a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> об'єкт). Це фактичний файловий об'єкт Python, який ви можете передавати безпосередньо іншим функціям або бібліотекам, що очікують «file-like» об'єкт. -`UploadFile` має такі асинхронні `async` методи. Вони викликають відповідні методи файлу під капотом (використовуючи внутрішній `SpooledTemporaryFile`). +`UploadFile` має такі асинхронні `async` методи. Вони всі викликають відповідні методи файлу під капотом (використовуючи внутрішній `SpooledTemporaryFile`). * `write(data)`: Записує `data` (`str` або `bytes`) у файл. * `read(size)`: Читає `size` (`int`) байтів/символів з файлу. -* `seek(offset)`: Переміщується до позиції `offset` (`int`) у файлі. - * Наприклад, `await myfile.seek(0)` поверне курсор на початок файлу. - * This is especially useful if you run `await myfile.read()` once and then need to read the contents again. Це особливо корисно, якщо Ви виконуєте await `await myfile.read()` один раз, а потім потрібно знову прочитати вміст. +* `seek(offset)`: Переходить до байтової позиції `offset` (`int`) у файлі. + * Наприклад, `await myfile.seek(0)` перейде на початок файлу. + * Це особливо корисно, якщо ви виконаєте `await myfile.read()` один раз, а потім потрібно знову прочитати вміст. * `close()`: Закриває файл. -Оскільки всі ці методи є асинхронними `async`, Вам потрібно використовувати "await": +Оскільки всі ці методи є асинхронними `async` методами, вам потрібно їх «await»-ити. -Наприклад, всередині `async` *функції обробки шляху* Ви можете отримати вміст за допомогою: +Наприклад, всередині `async` *функції операції шляху* ви можете отримати вміст за допомогою: ```Python contents = await myfile.read() ``` -Якщо Ви знаходитесь у звичайній `def` *функції обробки шляху*, Ви можете отримати доступ до `UploadFile.file` безпосередньо, наприклад: + +Якщо ви знаходитесь у звичайній `def` *функції операції шляху*, ви можете отримати доступ до `UploadFile.file` безпосередньо, наприклад: ```Python contents = myfile.file.read() @@ -98,57 +99,57 @@ contents = myfile.file.read() /// note | Технічні деталі `async` -Коли Ви використовуєте `async` методи, **FastAPI** виконує файлові операції у пулі потоків та очікує їх завершення. +Коли ви використовуєте `async` методи, **FastAPI** виконує файлові методи у пулі потоків і очікує на них. /// /// note | Технічні деталі Starlette -`UploadFile` у **FastAPI** успадковується безпосередньо від `UploadFile` у **Starlette**, але додає деякі необхідні частини, щоб зробити його сумісним із **Pydantic** та іншими компонентами FastAPI. +`UploadFile` у **FastAPI** успадковується безпосередньо від `UploadFile` у **Starlette**, але додає деякі необхідні частини, щоб зробити його сумісним із **Pydantic** та іншими частинами FastAPI. /// -## Що таке "Form Data" +## Що таке «Form Data» { #what-is-form-data } -Спосіб, у який HTML-форми (`<form></form>`) надсилають дані на сервер, зазвичай використовує "спеціальне" кодування, відмінне від JSON. +Спосіб, у який HTML-форми (`<form></form>`) надсилають дані на сервер, зазвичай використовує «спеціальне» кодування для цих даних, відмінне від JSON. -**FastAPI** забезпечує правильне зчитування цих даних з відповідної частини запиту, а не з JSON. +**FastAPI** забезпечить зчитування цих даних з правильного місця, а не з JSON. /// note | Технічні деталі -Дані з форм зазвичай кодуються за допомогою "media type" `application/x-www-form-urlencoded`, якщо вони не містять файлів. +Дані з форм зазвичай кодуються за допомогою «media type» `application/x-www-form-urlencoded`, якщо вони не містять файлів. -Але якщо форма містить файли, вона кодується у форматі `multipart/form-data`. Якщо Ви використовуєте `File`, **FastAPI** визначить, що потрібно отримати файли з відповідної частини тіла запиту. +Але якщо форма містить файли, вона кодується як `multipart/form-data`. Якщо ви використовуєте `File`, **FastAPI** знатиме, що потрібно отримати файли з правильної частини тіла. -Щоб дізнатися більше про ці типи кодування та формові поля, ознайомтеся з <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">документацією MDN</abbr> щодо <code>POST</code></a>. +Якщо ви хочете дізнатися більше про ці типи кодування та формові поля, ознайомтеся з <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> web docs для <code>POST</code></a>. /// -/// warning | Увага +/// warning | Попередження -Ви можете оголосити кілька параметрів `File` і `Form` в *операції шляху*, але Ви не можете одночасно оголошувати поля `Body`, які мають надходити у форматі JSON, оскільки тіло запиту буде закодоване у форматі `multipart/form-data`, а не `application/json`. +Ви можете оголосити кілька параметрів `File` і `Form` в *операції шляху*, але ви не можете одночасно оголошувати поля `Body`, які ви очікуєте отримати як JSON, оскільки запит матиме тіло, закодоване як `multipart/form-data`, а не `application/json`. -Це не обмеження **FastAPI**, а особливість протоколу HTTP. +Це не обмеження **FastAPI**, а частина протоколу HTTP. /// -## Опціональне Завантаження Файлів +## Необов’язкове завантаження файлу { #optional-file-upload } -Файл можна зробити необов’язковим, використовуючи стандартні анотації типів і встановлюючи значення за замовчуванням `None`: +Ви можете зробити файл необов’язковим, використовуючи стандартні анотації типів і встановивши значення за замовчуванням `None`: {* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} -## `UploadFile` із Додатковими Мета Даними +## `UploadFile` із додатковими метаданими { #uploadfile-with-additional-metadata } -Ви також можете використовувати `File()` разом із `UploadFile`, наприклад, для встановлення додаткових метаданих: +Ви також можете використовувати `File()` разом із `UploadFile`, наприклад, щоб встановити додаткові метадані: {* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} -## Завантаження Кількох Файлів +## Завантаження кількох файлів { #multiple-file-uploads } Можна завантажувати кілька файлів одночасно. -Вони будуть пов’язані з одним і тим самим "form field", який передається у вигляді "form data". +Вони будуть пов’язані з одним і тим самим «form field», який передається у вигляді «form data». Щоб це реалізувати, потрібно оголосити список `bytes` або `UploadFile`: @@ -160,16 +161,16 @@ contents = myfile.file.read() Ви також можете використати `from starlette.responses import HTMLResponse`. -**FastAPI** надає ті ж самі `starlette.responses`, що й `fastapi.responses`, для зручності розробників. Однак більшість доступних відповідей надходять безпосередньо від Starlette. +**FastAPI** надає ті ж самі `starlette.responses`, що й `fastapi.responses`, просто для зручності для вас, розробника. Але більшість доступних відповідей надходять безпосередньо від Starlette. /// -### Завантаження декількох файлів із додатковими метаданими +### Завантаження кількох файлів із додатковими метаданими { #multiple-file-uploads-with-additional-metadata } -Так само як і раніше, Ви можете використовувати `File()`, щоб встановити додаткові параметри навіть для `UploadFile`: +Так само як і раніше, ви можете використовувати `File()`, щоб встановити додаткові параметри навіть для `UploadFile`: {* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} -## Підсумок +## Підсумок { #recap } -Використовуйте `File`, `bytes`та `UploadFile`, щоб оголошувати файли для завантаження у запитах, які надсилаються у вигляді form data. +Використовуйте `File`, `bytes` та `UploadFile`, щоб оголошувати файли для завантаження в запиті, надіслані у вигляді form data. diff --git a/docs/uk/docs/tutorial/request-form-models.md b/docs/uk/docs/tutorial/request-form-models.md index 7f5759e79..1bfd368d6 100644 --- a/docs/uk/docs/tutorial/request-form-models.md +++ b/docs/uk/docs/tutorial/request-form-models.md @@ -1,12 +1,12 @@ -# Моделі форм (Form Models) +# Моделі форм { #form-models } -У FastAPI Ви можете використовувати **Pydantic-моделі** для оголошення **полів форми**. +У FastAPI ви можете використовувати **Pydantic-моделі** для оголошення **полів форми**. /// info | Інформація -Щоб використовувати форми, спочатку встановіть <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">python-multipart</a>. +Щоб використовувати форми, спочатку встановіть <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>. -Переконайтеся, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили бібліотеку, наприклад: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили його, наприклад: ```console $ pip install python-multipart @@ -14,21 +14,21 @@ $ pip install python-multipart /// -/// note | Підказка +/// note | Примітка -Ця функція підтримується, починаючи з FastAPI версії `0.113.0`. 🤓 +Це підтримується, починаючи з FastAPI версії `0.113.0`. 🤓 /// -## Використання Pydantic-моделей для форм +## Pydantic-моделі для форм { #pydantic-models-for-forms } -Вам просто потрібно оголосити **Pydantic-модель** з полями, які Ви хочете отримати як **поля форми**, а потім оголосити параметр як `Form`: +Вам просто потрібно оголосити **Pydantic-модель** з полями, які ви хочете отримати як **поля форми**, а потім оголосити параметр як `Form`: {* ../../docs_src/request_form_models/tutorial001_an_py39.py hl[9:11,15] *} -**FastAPI** **витягне** дані для **кожного поля** з **формових даних** у запиті та надасть вам Pydantic-модель, яку Ви визначили. +**FastAPI** **витягне** дані для **кожного поля** з **формових даних** у запиті та надасть вам Pydantic-модель, яку ви визначили. -## Перевірка документації +## Перевірте документацію { #check-the-docs } Ви можете перевірити це в UI документації за `/docs`: @@ -36,13 +36,13 @@ $ pip install python-multipart <img src="/img/tutorial/request-form-models/image01.png"> </div> -## Заборона додаткових полів форми +## Забороніть додаткові поля форми { #forbid-extra-form-fields } -У деяких особливих випадках (ймовірно, рідко) Ви можете **обмежити** форму лише тими полями, які були оголошені в Pydantic-моделі, і **заборонити** будь-які **додаткові** поля. +У деяких особливих випадках (ймовірно, не дуже поширених) ви можете **обмежити** поля форми лише тими, які були оголошені в Pydantic-моделі. І **заборонити** будь-які **додаткові** поля. -/// note | Підказка +/// note | Примітка -Ця функція підтримується, починаючи з FastAPI версії `0.114.0`. 🤓 +Це підтримується, починаючи з FastAPI версії `0.114.0`. 🤓 /// @@ -52,7 +52,7 @@ $ pip install python-multipart Якщо клієнт спробує надіслати додаткові дані, він отримає **відповідь з помилкою**. -Наприклад, якщо клієнт спробує надіслати наступні поля форми: +Наприклад, якщо клієнт спробує надіслати поля форми: * `username`: `Rick` * `password`: `Portal Gun` @@ -73,6 +73,6 @@ $ pip install python-multipart } ``` -## Підсумок +## Підсумок { #summary } -Ви можете використовувати Pydantic-моделі для оголошення полів форми у FastAPI. 😎 +У FastAPI ви можете використовувати Pydantic-моделі для оголошення полів форми. 😎 diff --git a/docs/uk/docs/tutorial/request-forms-and-files.md b/docs/uk/docs/tutorial/request-forms-and-files.md index a089ef945..e809bee22 100644 --- a/docs/uk/docs/tutorial/request-forms-and-files.md +++ b/docs/uk/docs/tutorial/request-forms-and-files.md @@ -1,10 +1,10 @@ -# Запити з формами та файлами +# Запити з формами та файлами { #request-forms-and-files } -У FastAPI Ви можете одночасно отримувати файли та поля форми, використовуючи `File` і `Form`. +Ви можете одночасно визначати файли та поля форми, використовуючи `File` і `Form`. /// info | Інформація -Щоб отримувати завантажені файли та/або дані форми, спочатку встановіть <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">python-multipart</a>. +Щоб отримувати завантажені файли та/або дані форми, спочатку встановіть <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>. Переконайтеся, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили бібліотеку, наприклад: @@ -14,21 +14,21 @@ $ pip install python-multipart /// -## Імпорт `File` та `Form` +## Імпорт `File` та `Form` { #import-file-and-form } {* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[3] *} -## Оголошення параметрів `File` та `Form` +## Оголошення параметрів `File` та `Form` { #define-file-and-form-parameters } Створіть параметри файлів та форми так само як і для `Body` або `Query`: {* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[10:12] *} -Файли та поля форми будуть завантажені як формові дані, і Ви отримаєте як файли, так і введені користувачем поля. +Файли та поля форми будуть завантажені як формові дані, і Ви отримаєте файли та поля форми. Ви також можете оголосити деякі файли як `bytes`, а деякі як `UploadFile`. -/// warning | Увага +/// warning | Попередження Ви можете оголосити кілька параметрів `File` і `Form` в операції *шляху*, але не можете одночасно оголошувати `Body`-поля, які очікуєте отримати у форматі JSON, оскільки запит матиме тіло, закодоване за допомогою `multipart/form-data`, а не `application/json`. @@ -36,6 +36,6 @@ $ pip install python-multipart /// -## Підсумок +## Підсумок { #recap } -Використовуйте `File` та `Form` разом, коли вам потрібно отримувати дані форми та файли в одному запиті. +Використовуйте `File` та `Form` разом, коли вам потрібно отримувати дані та файли в одному запиті. diff --git a/docs/uk/docs/tutorial/request-forms.md b/docs/uk/docs/tutorial/request-forms.md index 10c58a73e..2a22ad922 100644 --- a/docs/uk/docs/tutorial/request-forms.md +++ b/docs/uk/docs/tutorial/request-forms.md @@ -1,12 +1,12 @@ -# Дані форми +# Дані форми { #form-data } -Якщо Вам потрібно отримувати поля форми замість JSON, Ви можете використовувати `Form`. +Якщо вам потрібно отримувати поля форми замість JSON, ви можете використовувати `Form`. /// info | Інформація Щоб використовувати форми, спочатку встановіть <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>. -Переконайтеся, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, і потім встановили бібліотеку, наприклад: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, і потім встановили бібліотеку, наприклад: ```console $ pip install python-multipart @@ -14,23 +14,23 @@ $ pip install python-multipart /// -## Імпорт `Form` +## Імпорт `Form` { #import-form } Імпортуйте `Form` з `fastapi`: {* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} -## Оголошення параметрів `Form` +## Оголошення параметрів `Form` { #define-form-parameters } -Створюйте параметри форми так само як Ви б створювали `Body` або `Query`: +Створюйте параметри форми так само як ви б створювали `Body` або `Query`: {* ../../docs_src/request_forms/tutorial001_an_py39.py hl[9] *} Наприклад, один зі способів використання специфікації OAuth2 (так званий "password flow") вимагає надсилати `username` та `password` як поля форми. -<abbr title="Специфікація">spec</abbr> вимагає, щоб ці поля мали точні назви `username` і `password` та надсилалися у вигляді полів форми, а не JSON. +<abbr title="specification">spec</abbr> вимагає, щоб ці поля мали точні назви `username` і `password` та надсилалися у вигляді полів форми, а не JSON. -З `Form` Ви можете оголошувати ті ж конфігурації, що і з `Body` (та `Query`, `Path`, `Cookie`), включаючи валідацію, приклади, псевдоніми (наприклад, `user-name` замість `username`) тощо. +З `Form` ви можете оголошувати ті ж конфігурації, що і з `Body` (та `Query`, `Path`, `Cookie`), включаючи валідацію, приклади, псевдоніми (наприклад, `user-name` замість `username`) тощо. /// info | Інформація @@ -44,7 +44,7 @@ $ pip install python-multipart /// -## Про "поля форми" +## Про "поля форми" { #about-form-fields } HTML-форми (`<form></form>`) надсилають дані на сервер у "спеціальному" кодуванні, яке відрізняється від JSON. @@ -56,18 +56,18 @@ HTML-форми (`<form></form>`) надсилають дані на серве Але якщо форма містить файли, вона кодується як `multipart/form-data`. Ви дізнаєтеся про обробку файлів у наступному розділі. -Якщо Ви хочете дізнатися більше про ці кодування та поля форм, зверніться до <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> вебдокументації для <code>POST</code></a>. +Якщо ви хочете дізнатися більше про ці кодування та поля форм, зверніться до <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> вебдокументації для <code>POST</code></a>. /// /// warning | Попередження -Ви можете оголосити кілька параметрів `Form` в *операції шляху*, але не можете одночасно оголосити поля `Body`, які Ви очікуєте отримати у форматі JSON, оскільки тіло запиту буде закодовано у форматі `application/x-www-form-urlencoded`, а не `application/json`. +Ви можете оголосити кілька параметрів `Form` в *операції шляху*, але не можете одночасно оголосити поля `Body`, які ви очікуєте отримати у форматі JSON, оскільки запит матиме тіло, закодоване як `application/x-www-form-urlencoded`, а не `application/json`. Це не обмеження **FastAPI**, а частина HTTP-протоколу. /// -## Підсумок +## Підсумок { #recap } Використовуйте `Form` для оголошення вхідних параметрів у вигляді даних форми. diff --git a/docs/uk/docs/tutorial/response-model.md b/docs/uk/docs/tutorial/response-model.md index def1f8a2d..2fcad9438 100644 --- a/docs/uk/docs/tutorial/response-model.md +++ b/docs/uk/docs/tutorial/response-model.md @@ -1,36 +1,35 @@ -# Модель відповіді — Тип, що повертається +# Модель відповіді — Тип, що повертається { #response-model-return-type } -Ви можете оголосити тип, який використовуватиметься у відповіді, за допомогою *анотації типу, що повертається* *функцією операцією шляху* (path operation) +Ви можете оголосити тип, який використовуватиметься у відповіді, анотувавши **тип повернення** *функції операції шляху*. -**Анотацію типу** можна вказати так само як і для вхідних **параметрів** функції: це можуть бути моделі Pydantic, списки (lists), словники (dictionaries), скалярні значення, як-от цілі числа (integers), булеві значення (booleans) тощо. +**Анотації типів** можна використовувати так само, як і для вхідних даних у **параметрах** функції: можна використовувати моделі Pydantic, списки, словники, скалярні значення, як-от цілі числа, булеві значення тощо. {* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} -FastAPI використовуватиме цей тип, щоб: +FastAPI використовуватиме цей тип повернення, щоб: * **Перевірити правильність** повернених даних. - * Якщо дані не валідні (наприклад, відсутнє поле), це означає, що Ваш код додатку працює некоректно і не повертає те, що повинен. У такому випадку FastAPI поверне помилку сервера, замість того щоб віддати недопустимі дані. Так Ви та Ваші клієнти будете впевнені, що отримуєте очікувані дані у правильному форматі. - -* Додати **JSON Schema** відповіді до специфікації OpenAPI в *операціях шляху*. + * Якщо дані не валідні (наприклад, відсутнє поле), це означає, що *ваш* код застосунку зламаний, не повертає те, що повинен, і буде повернуто помилку сервера замість некоректних даних. Так ви та ваші клієнти можете бути впевнені, що отримаєте дані й очікувану структуру даних. +* Додати **JSON Schema** для відповіді в OpenAPI *операції шляху*. * Це буде використано в **автоматичній документації**. - * А також інструментами, які автоматично генерують клієнтський код. + * Це також буде використано інструментами, які автоматично генерують клієнтський код. Але найголовніше: -* FastAPI **обмежить та відфільтрує** вихідні дані відповідно до типу, вказаного у відповіді. - * Це особливо важливо для **безпеки**. Деталі нижче. +* Це **обмежить та відфільтрує** вихідні дані до того, що визначено в типі повернення. + * Це особливо важливо для **безпеки**, нижче ми побачимо про це більше. -## Параметр `response_model` +## Параметр `response_model` { #response-model-parameter } -Іноді Вам потрібно або зручно повертати інші типи даних, ніж ті, що зазначені як тип відповіді. +Є випадки, коли вам потрібно або ви хочете повертати дані, які не зовсім відповідають тому, що оголошено типом. -Наприклад, Ви можете **повертати словник** або об’єкт бази даних, але **оголосити модель Pydantic** як модель відповіді. Тоді модель Pydantic автоматично оброблятиме валідацію, документацію тощо. +Наприклад, ви можете захотіти **повертати словник** або об’єкт бази даних, але **оголосити його як модель Pydantic**. Таким чином модель Pydantic виконуватиме всю документацію даних, валідацію тощо для об’єкта, який ви повернули (наприклад, словника або об’єкта бази даних). -Якщо Ви додасте анотацію типу для повернення, редактор коду або mypy можуть поскаржитися, що функція повертає інший тип (наприклад, dict замість Item). +Якщо ви додали анотацію типу повернення, інструменти та редактори скаржитимуться (коректною) помилкою, повідомляючи, що ваша функція повертає тип (наприклад, dict), який відрізняється від того, що ви оголосили (наприклад, модель Pydantic). -У таких випадках можна скористатися параметром `response_model` в декораторі маршруту (наприклад, @app.get()). +У таких випадках можна скористатися параметром *декоратора операції шляху* `response_model` замість типу повернення. -Параметр `response_model` працює з будь-яким *оператором шляху*: +Ви можете використовувати параметр `response_model` у будь-якій з *операцій шляху*: * `@app.get()` * `@app.post()` @@ -42,33 +41,33 @@ FastAPI використовуватиме цей тип, щоб: /// note | Примітка -Зверніть увагу, що `response_model` є параметром методу-декоратора (`get`, `post`, тощо), а не *функцією операцією шляху* (path operation function), як це робиться з параметрами або тілом запиту. +Зверніть увагу, що `response_model` є параметром методу «декоратора» (`get`, `post` тощо). А не вашої *функції операції шляху*, як усі параметри та тіло. /// -`response_model` приймає такий самий тип, який Ви б вказали для поля моделі Pydantic. Тобто це може бути як Pydantic-модель, так і, наприклад, `list` із моделей Pydantic — `List[Item]`. +`response_model` приймає такий самий тип, який ви б вказали для поля моделі Pydantic, тобто це може бути модель Pydantic, але також це може бути, наприклад, `list` моделей Pydantic, як-от `List[Item]`. -FastAPI використовуватиме `response_model` для створення документації, валідації даних та — найважливіше — **перетворення та фільтрації вихідних даних** згідно з оголошеним типом. +FastAPI використовуватиме цей `response_model` для виконання всієї документації даних, валідації тощо, а також для **перетворення та фільтрації вихідних даних** до оголошеного типу. /// tip | Порада -Якщо у Вас увімкнено сувору перевірку типів у редакторі, mypy тощо, Ви можете оголосити тип повернення функції як `Any`. +Якщо у вас увімкнено сувору перевірку типів у редакторі, mypy тощо, ви можете оголосити тип повернення функції як `Any`. -Таким чином, Ви повідомляєте редактору, що свідомо повертаєте будь-що. Але FastAPI усе одно виконуватиме створення документації, валідацію, фільтрацію тощо за допомогою параметра `response_model`. +Таким чином, ви повідомляєте редактору, що свідомо повертаєте будь-що. Але FastAPI усе одно виконуватиме документацію даних, валідацію, фільтрацію тощо за допомогою `response_model`. /// -### Пріоритет `response_model` +### Пріоритет `response_model` { #response-model-priority } -Якщо Ви вказуєте і тип повернення, і `response_model`, то FastAPI використовуватиме `response_model` з пріоритетом. +Якщо ви оголошуєте і тип повернення, і `response_model`, то `response_model` матиме пріоритет і буде використаний FastAPI. -Таким чином, Ви можете додати правильні анотації типів до ваших функцій, навіть якщо вони повертають тип, відмінний від `response_model`. Це буде корисно для редакторів коду та інструментів, таких як mypy. І при цьому FastAPI продовжить виконувати валідацію даних, генерувати документацію тощо на основі `response_model`. +Таким чином ви можете додати правильні анотації типів до ваших функцій, навіть коли повертаєте тип, відмінний від моделі відповіді, щоб це використовували редактор і інструменти на кшталт mypy. І при цьому FastAPI все одно виконуватиме валідацію даних, документацію тощо, використовуючи `response_model`. -Ви також можете використати `response_model=None`, щоб вимкнути створення моделі відповіді для цієї *операції шляху*. Це може знадобитися, якщо Ви додаєте анотації типів до об'єктів, які не є допустимими полями Pydantic — приклад цього Ви побачите в одному з наступних розділів. +Ви також можете використати `response_model=None`, щоб вимкнути створення моделі відповіді для цієї *операції шляху*; це може знадобитися, якщо ви додаєте анотації типів для речей, які не є валідними полями Pydantic, приклад цього ви побачите в одному з розділів нижче. -## Повернути ті самі вхідні дані +## Повернути ті самі вхідні дані { #return-the-same-input-data } -Тут ми оголошуємо модель `UserIn`, яка містить звичайний текстовий пароль: +Тут ми оголошуємо модель `UserIn`, вона міститиме пароль у відкритому вигляді: {* ../../docs_src/response_model/tutorial002_py310.py hl[7,9] *} @@ -76,7 +75,7 @@ FastAPI використовуватиме `response_model` для створе Щоб використовувати `EmailStr`, спочатку встановіть <a href="https://github.com/JoshData/python-email-validator" class="external-link" target="_blank">`email-validator`</a>. -Переконайтесь, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили пакет, наприклад: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили пакет, наприклад: ```console $ pip install email-validator @@ -90,29 +89,29 @@ $ pip install "pydantic[email]" /// -І ми використовуємо цю модель, щоб оголосити і вхідні, і вихідні дані: +І ми використовуємо цю модель, щоб оголосити наші вхідні дані, і цю ж модель, щоб оголосити наші вихідні дані: {* ../../docs_src/response_model/tutorial002_py310.py hl[16] *} -Тепер, коли браузер створює користувача з паролем, API поверне той самий пароль у відповіді. +Тепер, щоразу коли браузер створює користувача з паролем, API поверне той самий пароль у відповіді. -У цьому випадку це може не бути проблемою, адже саме користувач надіслав пароль. +У цьому випадку це може не бути проблемою, адже це той самий користувач надсилає пароль. -Але якщо ми використаємо цю ж модель для іншої операції шляху, ми можемо випадково надіслати паролі наших користувачів кожному клієнту. +Але якщо ми використаємо цю ж модель для іншої *операції шляху*, ми можемо надсилати паролі наших користувачів кожному клієнту. /// danger | Обережно -Ніколи не зберігайте пароль користувача у відкритому вигляді та не надсилайте його у відповіді, якщо тільки Ви не знаєте всі ризики і точно розумієте, що робите. +Ніколи не зберігайте пароль користувача у відкритому вигляді та не надсилайте його у відповіді таким чином, якщо тільки ви не знаєте всіх застережень і точно розумієте, що робите. /// -## Додайте окрему вихідну модель +## Додати вихідну модель { #add-an-output-model } -Замість цього ми можемо створити вхідну модель з відкритим паролем і вихідну модель без нього: +Замість цього ми можемо створити вхідну модель з паролем у відкритому вигляді і вихідну модель без нього: {* ../../docs_src/response_model/tutorial003_py310.py hl[9,11,16] *} -Тут, навіть якщо *функція операції шляху* повертає об'єкт користувача, який містить пароль: +Тут, хоча наша *функція операції шляху* повертає того самого вхідного користувача, який містить пароль: {* ../../docs_src/response_model/tutorial003_py310.py hl[24] *} @@ -120,107 +119,107 @@ $ pip install "pydantic[email]" {* ../../docs_src/response_model/tutorial003_py310.py hl[22] *} -Таким чином, **FastAPI** автоматично відфільтрує всі дані, які не вказані у вихідній моделі (за допомогою Pydantic). +Таким чином, **FastAPI** подбає про фільтрацію всіх даних, які не оголошені у вихідній моделі (використовуючи Pydantic). -### `response_model` або тип повернення +### `response_model` або тип повернення { #response-model-or-return-type } -У цьому випадку, оскільки дві моделі різні, якщо ми анотуємо тип повернення функції як `UserOut`, редактор і такі інструменти, як mypy, видадуть помилку, бо фактично ми повертаємо інший тип. +У цьому випадку, оскільки дві моделі різні, якщо ми анотуємо тип повернення функції як `UserOut`, редактор і інструменти скаржитимуться, що ми повертаємо невалідний тип, адже це різні класи. -Тому в цьому прикладі ми використовуємо параметр `response_model`, а не анотацію типу повернення. +Саме тому в цьому прикладі нам треба оголосити це через параметр `response_model`. -...але читайте далі, щоб дізнатися, як обійти це обмеження. +...але читайте далі нижче, щоб побачити, як це обійти. -## Тип повернення і фільтрація даних +## Тип повернення і фільтрація даних { #return-type-and-data-filtering } -Продовжимо з попереднього прикладу. Ми хотіли **анотувати функцію одним типом**, але при цьому повертати з неї більше даних. +Продовжимо з попереднього прикладу. Ми хотіли **анотувати функцію одним типом**, але хотіли мати змогу повертати з функції те, що насправді містить **більше даних**. -Ми хочемо, щоб FastAPI продовжував **фільтрувати** ці дані за допомогою response_model. Тобто навіть якщо функція повертає більше інформації, у відповіді будуть лише ті поля, які вказані у response_model. +Ми хочемо, щоб FastAPI продовжував **фільтрувати** дані, використовуючи модель відповіді. Тобто навіть якщо функція повертає більше даних, відповідь міститиме лише поля, оголошені в моделі відповіді. -У попередньому прикладі, оскільки класи були різні, нам довелося використовувати параметр `response_model`. Але це означає, що ми не отримуємо підтримки з боку редактора коду та інструментів перевірки типів щодо типу, який повертає функція. +У попередньому прикладі, оскільки класи були різні, нам довелося використовувати параметр `response_model`. Але це також означає, що ми не отримуємо підтримки від редактора та інструментів, які перевіряють тип повернення функції. -Проте в більшості випадків, коли нам потрібно зробити щось подібне, ми просто хочемо, щоб модель **відфільтрувала або прибрала** частину даних, як у цьому прикладі. +Проте в більшості випадків, коли нам потрібно зробити щось подібне, ми просто хочемо, щоб модель **відфільтрувала/прибрала** частину даних, як у цьому прикладі. -У таких випадках ми можемо використати класи та спадкування, щоб скористатися **анотаціями типів** функцій — це дає кращу підтримку з боку редактора та інструментів типу mypy, і при цьому FastAPI продовжує виконувати **фільтрацію даних** у відповіді. +І в таких випадках ми можемо використати класи та спадкування, щоб скористатися **анотаціями типів** функцій і отримати кращу підтримку в редакторі та інструментах, і при цьому зберегти **фільтрацію даних** у FastAPI. {* ../../docs_src/response_model/tutorial003_01_py310.py hl[7:10,13:14,18] *} -Завдяки цьому ми отримуємо підтримку інструментів — від редакторів і mypy, оскільки цей код є коректним з точки зору типів, — але ми також отримуємо фільтрацію даних від FastAPI. +Завдяки цьому ми отримуємо підтримку інструментів — від редакторів і mypy, адже цей код коректний з точки зору типів, — але ми також отримуємо фільтрацію даних від FastAPI. Як це працює? Давайте розберемося. 🤓 -### Типи та підтримка інструментів +### Анотації типів і підтримка інструментів { #type-annotations-and-tooling } -Спершу подивимось, як це бачать редактори, mypy та інші інструменти. +Спершу подивімося, як це бачать редактори, mypy та інші інструменти. -`BaseUser` має базові поля. Потім `UserIn` успадковує `BaseUser` і додає поле `password`, отже, він матиме всі поля з обох моделей. +`BaseUser` має базові поля. Потім `UserIn` успадковує `BaseUser` і додає поле `password`, отже він включатиме всі поля з обох моделей. -Ми зазначаємо тип повернення функції як `BaseUser`, але фактично повертаємо екземпляр `UserIn`. +Ми анотуємо тип повернення функції як `BaseUser`, але фактично повертаємо екземпляр `UserIn`. -Редактор, mypy та інші інструменти не скаржитимуться на це, тому що з точки зору типізації `UserIn` є підкласом `BaseUser`, а це означає, що він є `валідним` типом, коли очікується будь-що, що є `BaseUser`. +Редактор, mypy та інші інструменти не скаржитимуться на це, тому що з точки зору типізації `UserIn` є підкласом `BaseUser`, а це означає, що він є *валідним* типом, коли очікується будь-що, що є `BaseUser`. -### Фільтрація даних у FastAPI +### Фільтрація даних у FastAPI { #fastapi-data-filtering } -Тепер для FastAPI він бачить тип повернення і переконується, що те, що Ви повертаєте, містить **тільки** поля, які оголошені у цьому типі. +Тепер для FastAPI він побачить тип повернення і переконається, що те, що ви повертаєте, містить **лише** поля, які оголошені у цьому типі. -FastAPI виконує кілька внутрішніх операцій з Pydantic, щоб гарантувати, що правила наслідування класів не застосовуються для фільтрації повернених даних, інакше Ви могли б повернути значно більше даних, ніж очікували. +FastAPI виконує кілька внутрішніх операцій з Pydantic, щоб гарантувати, що ті самі правила наслідування класів не застосовуються для фільтрації повернених даних, інакше ви могли б зрештою повертати значно більше даних, ніж очікували. -Таким чином, Ви отримуєте найкраще з двох світів: анотації типів **з підтримкою інструментів** і **фільтрацію даних**. +Таким чином ви можете отримати найкраще з двох світів: анотації типів із **підтримкою інструментів** і **фільтрацію даних**. -## Подивитись у документації +## Подивитися в документації { #see-it-in-the-docs } -Коли Ви дивитесь автоматичну документацію, Ви можете побачити, що вхідна модель і вихідна модель мають власну JSON-схему: +Коли ви дивитеся автоматичну документацію, ви можете перевірити, що вхідна модель і вихідна модель матимуть власну JSON Schema: <img src="/img/tutorial/response-model/image01.png"> -І обидві моделі використовуються для інтерактивної API-документації: +І обидві моделі будуть використані для інтерактивної документації API: <img src="/img/tutorial/response-model/image02.png"> -## Інші анотації типів повернення +## Інші анотації типів повернення { #other-return-type-annotations } -Існують випадки, коли Ви повертаєте щось, що не є допустимим полем Pydantic, але анотуєте це у функції лише для того, щоб отримати підтримку від інструментів (редактора, mypy тощо). +Можуть бути випадки, коли ви повертаєте щось, що не є валідним полем Pydantic, і анотуєте це у функції лише для того, щоб отримати підтримку від інструментів (редактора, mypy тощо). -### Повернення Response напряму +### Повернути Response напряму { #return-a-response-directly } Найпоширенішим випадком буде [повернення Response напряму, як пояснюється пізніше у розширеній документації](../advanced/response-directly.md){.internal-link target=_blank}. -{* ../../docs_src/response_model/tutorial003_02.py hl[8,10:11] *} +{* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} Цей простий випадок автоматично обробляється FastAPI, тому що анотація типу повернення — це клас (або підклас) `Response`. І інструменти також будуть задоволені, бо і `RedirectResponse`, і `JSONResponse` є підкласами `Response`, отже анотація типу коректна. -### Анотація підкласу Response +### Анотувати підклас Response { #annotate-a-response-subclass } -Також можна використовувати підклас `Response` у анотації типу: +Ви також можете використати підклас `Response` в анотації типу: -{* ../../docs_src/response_model/tutorial003_03.py hl[8:9] *} +{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} Це теж працюватиме, бо `RedirectResponse` — підклас `Response`, і FastAPI автоматично обробить цей простий випадок. -### Некоректні анотації типу повернення +### Некоректні анотації типу повернення { #invalid-return-type-annotations } -Але коли Ви повертаєте якийсь інший довільний об’єкт, що не є валідним типом Pydantic (наприклад, об’єкт бази даних), і анотуєте його так у функції, FastAPI спробує створити Pydantic модель відповіді на основі цієї анотації типу, і це завершиться помилкою. +Але коли ви повертаєте якийсь інший довільний об’єкт, що не є валідним типом Pydantic (наприклад, об’єкт бази даних), і анотуєте його так у функції, FastAPI спробує створити модель відповіді Pydantic на основі цієї анотації типу і це завершиться помилкою. -Те саме станеться, якщо Ви використовуєте <abbr title="Об'єднання (union) кількох типів означає: «будь-який з цих типів».">union</abbr> між різними типами, де один або більше не є валідними типами Pydantic, наприклад, це спричинить помилку 💥: +Те саме станеться, якщо ви використаєте <abbr title='Об’єднання (union) між кількома типами означає «будь-який із цих типів».'>union</abbr> між різними типами, де один або більше не є валідними типами Pydantic, наприклад, це завершиться помилкою 💥: {* ../../docs_src/response_model/tutorial003_04_py310.py hl[8] *} -...це не працює, тому що тип анотації не є типом Pydantic і не є просто класом `Response` або його підкласом, а є об’єднанням (union) — або `Response`, або `dict`. +...це не працює, тому що анотація типу не є типом Pydantic і не є просто одним класом `Response` або його підкласом, це union (будь-який із двох) між `Response` і `dict`. -### Відключення Моделі Відповіді +### Вимкнути модель відповіді { #disable-response-model } -Продовжуючи приклад вище, можливо, Ви не хочете використовувати стандартну валідацію даних, автоматичну документацію, фільтрацію тощо, які FastAPI виконує за замовчуванням. +Продовжуючи приклад вище, можливо, ви не хочете мати стандартну валідацію даних, документацію, фільтрацію тощо, які виконує FastAPI. -Але ви все одно можете залишити анотацію типу у функції, щоб зберегти підтримку з боку інструментів, таких як редактори коду або статичні перевірки типів (наприклад, mypy). +Але ви можете все одно хотіти залишити анотацію типу повернення у функції, щоб отримати підтримку від інструментів, як-от редактори та перевірки типів (наприклад, mypy). У такому випадку ви можете вимкнути генерацію моделі відповіді, встановивши `response_model=None`: {* ../../docs_src/response_model/tutorial003_05_py310.py hl[7] *} -Це змусить FastAPI пропустити генерацію моделі відповіді, і таким чином Ви зможете використовувати будь-які анотації типів повернення без впливу на вашу FastAPI аплікацію. 🤓 +Це змусить FastAPI пропустити генерацію моделі відповіді, і таким чином ви зможете використовувати будь-які потрібні анотації типів повернення без впливу на ваш FastAPI застосунок. 🤓 -## Параметри кодування моделі відповіді +## Параметри кодування моделі відповіді { #response-model-encoding-parameters } Ваша модель відповіді може мати значення за замовчуванням, наприклад: @@ -230,19 +229,19 @@ FastAPI виконує кілька внутрішніх операцій з Pyd * `tax: float = 10.5` має значення за замовчуванням `10.5`. * `tags: List[str] = []` має значення за замовчуванням порожній список: `[]`. -Але Ви можете захотіти не включати їх у результат, якщо вони фактично не були збережені. +але ви можете захотіти не включати їх у результат, якщо вони фактично не були збережені. -Наприклад, якщо у Вас є моделі з багатьма необов’язковими атрибутами у NoSQL базі даних, але Ви не хочете відправляти дуже довгі JSON-відповіді, повні значень за замовчуванням. +Наприклад, якщо у вас є моделі з багатьма необов’язковими атрибутами у NoSQL базі даних, але ви не хочете надсилати дуже довгі JSON-відповіді, повні значень за замовчуванням. -### Використовуйте параметр `response_model_exclude_unset` +### Використовуйте параметр `response_model_exclude_unset` { #use-the-response-model-exclude-unset-parameter } -Ви можете встановити параметр декоратора шляху `response_model_exclude_unset=True`: +Ви можете встановити параметр *декоратора операції шляху* `response_model_exclude_unset=True`: {* ../../docs_src/response_model/tutorial004_py310.py hl[22] *} -і ці значення за замовчуванням не будуть включені у відповідь, тільки фактично встановлені значення. +і ці значення за замовчуванням не будуть включені у відповідь, лише значення, які фактично встановлені. -Отже, якщо Ви надішлете запит до цього оператора шляху для елемента з item_id `foo`, відповідь (без включення значень за замовчуванням) буде: +Отже, якщо ви надішлете запит до цієї *операції шляху* для елемента з ID `foo`, відповідь (без включення значень за замовчуванням) буде: ```JSON { @@ -253,32 +252,18 @@ FastAPI виконує кілька внутрішніх операцій з Pyd /// info | Інформація -У Pydantic версії 1 метод називався `.dict()`, він був застарілий (але ще підтримується) у Pydantic версії 2 і перейменований у `.model_dump()`. - -Приклади тут використовують `.dict()` для сумісності з Pydantic v1, але Вам слід використовувати `.model_dump()`, якщо Ви можете використовувати Pydantic v2. - -/// - -/// info | Інформація - -FastAPI використовує `.dict()` моделі Pydantic з <a href="https://docs.pydantic.dev/1.10/usage/exporting_models/#modeldict" class="external-link" target="_blank">параметром `exclude_unset`</a>, щоб досягти цього. - -/// - -/// info | Інформація - Ви також можете використовувати: * `response_model_exclude_defaults=True` * `response_model_exclude_none=True` -як описано в <a href="https://docs.pydantic.dev/1.10/usage/exporting_models/#modeldict" class="external-link" target="_blank">документації Pydantic</a> for `exclude_defaults` та `exclude_none`. +як описано в <a href="https://docs.pydantic.dev/1.10/usage/exporting_models/#modeldict" class="external-link" target="_blank">документації Pydantic</a> для `exclude_defaults` та `exclude_none`. /// -#### Дані зі значеннями для полів із типовими значеннями +#### Дані зі значеннями для полів із типовими значеннями { #data-with-values-for-fields-with-defaults } -Але якщо Ваші дані мають значення для полів моделі з типовими значеннями, як у елемента з item_id `bar`: +Але якщо ваші дані мають значення для полів моделі з типовими значеннями, як у елемента з ID `bar`: ```Python hl_lines="3 5" { @@ -288,11 +273,12 @@ FastAPI використовує `.dict()` моделі Pydantic з <a href="htt "tax": 20.2 } ``` + вони будуть включені у відповідь. -#### Дані з тими самими значеннями, що й типові +#### Дані з тими самими значеннями, що й типові { #data-with-the-same-values-as-the-defaults } -Якщо дані мають ті самі значення, що й типові, як у елемента з item_id `baz`: +Якщо дані мають ті самі значення, що й типові, як у елемента з ID `baz`: ```Python hl_lines="3 5-6" { @@ -304,7 +290,7 @@ FastAPI використовує `.dict()` моделі Pydantic з <a href="htt } ``` -FastAPI достатньо розумний (насправді, Pydantic достатньо розумний), щоб зрозуміти, що, хоча `description`, `tax` і `tags` мають ті самі значення, що й типові, вони були встановлені явно (а не взяті як значення за замовчуванням). +FastAPI достатньо розумний (насправді, Pydantic достатньо розумний), щоб зрозуміти, що, хоча `description`, `tax` і `tags` мають ті самі значення, що й типові, їх було встановлено явно (а не взято як значення за замовчуванням). Отже, вони будуть включені у JSON-відповідь. @@ -312,24 +298,23 @@ FastAPI достатньо розумний (насправді, Pydantic дос Зверніть увагу, що типові значення можуть бути будь-якими, не лише `None`. -Це може бути list (`[]`), `float` 10.5 тощо. +Це може бути list (`[]`), `float` зі значенням `10.5` тощо. /// -### `response_model_include` та `response_model_exclude` +### `response_model_include` та `response_model_exclude` { #response-model-include-and-response-model-exclude } Ви також можете використовувати параметри *декоратора операції шляху* `response_model_include` та `response_model_exclude`. -Вони приймають `set` (множину) рядків (`str`) з іменами атрибутів, які потрібно включити (пропускаючи інші) або виключити (включаючи інші). +Вони приймають `set` зі `str` з іменами атрибутів, які потрібно включити (пропускаючи решту) або виключити (включаючи решту). -Це можна використовувати як швидкий спосіб, якщо у Вас є лише одна модель Pydantic і Ви хочете видалити деякі дані з виводу. +Це можна використовувати як швидкий спосіб, якщо у вас є лише одна модель Pydantic і ви хочете видалити деякі дані з виводу. /// tip | Порада -Але все ж рекомендується використовувати описані вище підходи, із застосуванням кількох класів, замість цих параметрів. +Але все ж рекомендується використовувати описані вище підходи, застосовуючи кілька класів, замість цих параметрів. - -Це тому, що JSON Schema, який генерується у вашому OpenAPI додатку (і в документації), все одно буде відповідати повній моделі, навіть якщо Ви використовуєте `response_model_include` або `response_model_exclude` для виключення деяких атрибутів. +Це тому, що JSON Schema, який генерується в OpenAPI вашого застосунку (і в документації), все одно буде відповідати повній моделі, навіть якщо ви використовуєте `response_model_include` або `response_model_exclude`, щоб пропустити деякі атрибути. Це також стосується `response_model_by_alias`, який працює подібним чином. @@ -345,14 +330,14 @@ FastAPI достатньо розумний (насправді, Pydantic дос /// -#### Використання `list` замість `set` +#### Використання `list` замість `set` { #using-lists-instead-of-sets } -Якщо Ви забудете використати `set` і натомість застосуєте `list` або `tuple`, FastAPI все одно перетворить це на `set`, і все працюватиме правильно: +Якщо ви забудете використати `set` і натомість застосуєте `list` або `tuple`, FastAPI все одно перетворить це на `set`, і все працюватиме правильно: {* ../../docs_src/response_model/tutorial006_py310.py hl[29,35] *} -## Підсумок +## Підсумок { #recap } -Використовуйте параметр `response_model` *декоратора операції шляху*, щоб визначати моделі відповіді, особливо щоб гарантувати фільтрацію приватних даних. +Використовуйте параметр `response_model` *декоратора операції шляху*, щоб визначати моделі відповіді і особливо щоб гарантувати фільтрацію приватних даних. Використовуйте `response_model_exclude_unset`, щоб повертати лише явно встановлені значення. diff --git a/docs/uk/docs/tutorial/response-status-code.md b/docs/uk/docs/tutorial/response-status-code.md index 1ed69d6f2..5a08ee46b 100644 --- a/docs/uk/docs/tutorial/response-status-code.md +++ b/docs/uk/docs/tutorial/response-status-code.md @@ -1,6 +1,6 @@ -# Статус коди Відповідей +# Код статусу відповіді { #response-status-code } -Так само як Ви можете вказати модель відповіді, Ви також можете оголосити HTTP код статусу для відповіді за допомогою параметра `status_code` в будь-якій з *операцій шляху*: +Так само, як ви можете вказати модель відповіді, ви також можете оголосити HTTP код статусу, що використовується для відповіді, за допомогою параметра `status_code` в будь-якій з *операцій шляху*: * `@app.get()` * `@app.post()` @@ -8,82 +8,83 @@ * `@app.delete()` * тощо. -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} -/// note | Нотатка +/// note | Примітка -Зверніть увагу, що `status_code` є параметром методу "декоратора" (`get`, `post` і т.д.), а не Вашої *функції операції шляху*, як усі інші параметри та тіло запиту. +Зверніть увагу, що `status_code` є параметром методу «декоратора» (`get`, `post`, тощо). Не вашої *функції операції шляху*, як усі параметри та тіло. /// -Параметр `status_code` приймає число, яке відповідає HTTP коду статусу. +Параметр `status_code` приймає число з HTTP кодом статусу. /// info | Інформація -`status_code` також може отримувати значення з `IntEnum`, наприклад, з Python <a href="https://docs.python.org/3/library/http.html#http.HTTPStatus" class="external-link" target="_blank">`http.HTTPStatus`</a>. + +`status_code` також може, як альтернативу, приймати `IntEnum`, наприклад, Python <a href="https://docs.python.org/3/library/http.html#http.HTTPStatus" class="external-link" target="_blank">`http.HTTPStatus`</a>. /// Він буде: -* Повертати вказаний код статусу у відповіді. -* Документувати його як такий у схемі OpenAPI (і, таким чином, в інтерфейсі користувача): +* Повертати цей код статусу у відповіді. +* Документувати його як такий у схемі OpenAPI (і, таким чином, в інтерфейсах користувача): <img src="/img/tutorial/response-status-code/image01.png"> -/// note | Нотатка +/// note | Примітка Деякі коди відповіді (див. наступний розділ) вказують, що відповідь не має тіла. -FastAPI знає про це і створить OpenAPI документацію, яка вказує, що тіла відповіді немає. +FastAPI знає про це і створить документацію OpenAPI, яка вказує, що тіла відповіді немає. /// -## Про HTTP статус коди +## Про HTTP коди статусу { #about-http-status-codes } -/// note | Нотатка +/// note | Примітка -Якщо Ви вже знаєте, що таке HTTP коди статусу, переходьте до наступного розділу. +Якщо ви вже знаєте, що таке HTTP коди статусу, перейдіть до наступного розділу. /// -В HTTP Ви надсилаєте числовий код статусу з 3 цифр як частину відповіді. +В HTTP ви надсилаєте числовий код статусу з 3 цифр як частину відповіді. -Ці коди статусу мають пов’язану назву для їх розпізнавання, але найважливішою частиною є саме число. +Ці коди статусу мають пов’язану назву для їх розпізнавання, але важливою частиною є число. Коротко: -* **`100 - 199`** "Інформаційні" відповіді. Ви рідко використовуєте їх напряму. Відповіді з такими кодами не можуть мати тіла. -* **`200 - 299`** "Успішні" відповіді. Це ті, які Ви використовуватимете найчастіше. - * `200` - код за замовчуванням, який означає, що все пройшло "OK". - * Інший приклад – `201`, "Created" (створено). Його зазвичай використовують після створення нового запису в базі даних. - * Особливий випадок – `204`, "No Content" (немає вмісту). Ця відповідь використовується, коли немає даних для повернення клієнту, тому відповідь не повинна мати тіла. -* **`300 - 399`** "Перенаправлення". Відповіді з цими кодами можуть мати або не мати тіла, за винятком `304`, "Not Modified" (не змінено), яка не повинна мати тіла. -* **`400 - 499`** "Помилка клієнта". Це другий тип, який Ви, ймовірно, будете використовувати найчастіше. - * Приклад `404`, "Not Found" (не знайдено). - * Для загальних помилок клієнта можна використовувати `400`. -* `500 - 599` "Помилки сервера". Ви майже ніколи не використовуєте їх напряму. Якщо в коді Вашого застосунку або на сервері щось пішло не так, автоматично буде повернено один із цих кодів статусу. +* `100 - 199` — для «Information». Ви рідко використовуєте їх напряму. Відповіді з такими кодами статусу не можуть мати тіла. +* **`200 - 299`** — для «Successful» відповідей. Це ті, які ви використовуватимете найчастіше. + * `200` — код статусу за замовчуванням, який означає, що все було «OK». + * Інший приклад — `201`, «Created». Його зазвичай використовують після створення нового запису в базі даних. + * Особливий випадок — `204`, «No Content». Цю відповідь використовують, коли немає вмісту для повернення клієнту, і тому відповідь не повинна мати тіла. +* **`300 - 399`** — для «Redirection». Відповіді з цими кодами статусу можуть мати або не мати тіла, за винятком `304`, «Not Modified», яка не повинна мати тіла. +* **`400 - 499`** — для відповідей «Client error». Це другий тип, який ви, ймовірно, будете використовувати найчастіше. + * Приклад — `404`, для відповіді «Not Found». + * Для загальних помилок з боку клієнта ви можете просто використовувати `400`. +* `500 - 599` — для помилок сервера. Ви майже ніколи не використовуєте їх напряму. Коли щось піде не так у якійсь частині коду вашого застосунку або на сервері, автоматично буде повернено один із цих кодів статусу. /// tip | Порада -Щоб дізнатися більше про кожен код статусу і призначення кожного з них, перегляньте документацію <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> про HTTP коди статусу</a>. +Щоб дізнатися більше про кожен код статусу і для чого призначений кожен із них, перегляньте документацію <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> про HTTP коди статусу</a>. /// -## Легкий спосіб запам'ятати назви +## Скорочення, щоб запам’ятати назви { #shortcut-to-remember-the-names } -Розглянемо ще раз попередній приклад: +Розглянемо попередній приклад ще раз: -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} -`201` - це код статусу для "Created" (створено). +`201` — це код статусу для «Created». -Але Вам не потрібно запам'ятовувати, що означає кожен із цих кодів. +Але вам не потрібно запам'ятовувати, що означає кожен із цих кодів. -Ви можете використовувати зручні змінні з `fastapi.status` +Ви можете використовувати зручні змінні з `fastapi.status`. -{* ../../docs_src/response_status_code/tutorial002.py hl[1,6] *} +{* ../../docs_src/response_status_code/tutorial002_py39.py hl[1,6] *} -Ці змінні просто для зручності. Вони містять ті ж самі числа, але Ви можете скористатися автозаповненням в редакторі: +Вони — просто для зручності, містять те саме число, але так ви можете скористатися автозаповненням редактора, щоб знайти їх: <img src="/img/tutorial/response-status-code/image02.png"> @@ -91,10 +92,10 @@ FastAPI знає про це і створить OpenAPI документаці Ви також можете використати `from starlette import status`. -**FastAPI** надає ті ж самі змінні `starlette.status` як `fastapi.status`, просто для зручності розробника. Однак вони походять безпосередньо зі Starlette. +**FastAPI** надає той самий `starlette.status` як `fastapi.status` просто як зручність для вас, розробника. Але він походить безпосередньо зі Starlette. /// -## Зміна значення за замовчуванням +## Зміна значення за замовчуванням { #changing-the-default } -Далі, у Посібнику для досвідчених користувачів{.internal-link target=_blank}, Ви дізнаєтесь, як повернути інший код статусу, ніж той, який Ви оголосили тут. +Пізніше, у [Посібнику для досвідчених користувачів](../advanced/response-change-status-code.md){.internal-link target=_blank}, ви побачите, як повертати інший код статусу, ніж значення за замовчуванням, яке ви оголошуєте тут. diff --git a/docs/uk/docs/tutorial/schema-extra-example.md b/docs/uk/docs/tutorial/schema-extra-example.md index 853fd5e65..54608c2ab 100644 --- a/docs/uk/docs/tutorial/schema-extra-example.md +++ b/docs/uk/docs/tutorial/schema-extra-example.md @@ -1,44 +1,22 @@ -# Декларування прикладів вхідних даних +# Декларування прикладів вхідних даних { #declare-request-example-data } Ви можете задати приклади даних, які Ваш застосунок може отримувати. Ось кілька способів, як це зробити. -## Додаткові дані JSON-схеми в моделях Pydantic +## Додаткові дані JSON-схеми в моделях Pydantic { #extra-json-schema-data-in-pydantic-models } Ви можете задати `examples` для моделі Pydantic, які буде додано до згенерованої JSON-схеми. -//// tab | Pydantic v2 - {* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -//// +Ця додаткова інформація буде додана як є до **JSON-схеми** для цієї моделі, і вона буде використана в документації до API. -//// tab | Pydantic v1 - -{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *} - -//// - -Ця додаткова інформація буде додана як є до **JSON-схеми**, і вона буде використовуватися в документації до API. - -//// tab | Pydantic v2 - -У версії Pydantic 2 використовується атрибут `model_config`, який приймає `dict`, як описано в <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">документації Pydantic: Конфігурація</a>. +Ви можете використати атрибут `model_config`, який приймає `dict`, як описано в <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">документації Pydantic: Configuration</a>. Ви можете встановити `"json_schema_extra"` як `dict`, що містить будь-які додаткові дані, які Ви хочете відобразити у згенерованій JSON-схемі, включаючи `examples`. -//// - -//// tab | Pydantic v1 - -У версії Pydantic 1 використовується внутрішній клас `Config` і параметр `schema_extra`, як описано в <a href="https://docs.pydantic.dev/1.10/usage/schema/#schema-customization" class="external-link" target="_blank">документації Pydantic: Налаштування схеми</a>. - -Ви можете задати `schema_extra` як `dict`, що містить будь-які додаткові дані, які Ви хочете бачити у згенерованій JSON-схемі, включаючи `examples`. - -//// - -/// tip | Підказка +/// tip | Порада Ви можете використати ту ж техніку, щоб розширити JSON-схему і додати власну додаткову інформацію. @@ -50,19 +28,19 @@ OpenAPI 3.1.0 (який використовується починаючи з FastAPI 0.99.0) додав підтримку `examples`, що є частиною стандарту **JSON-схеми**. -До цього підтримувався лише ключ `example` з одним прикладом. Він все ще підтримується в OpenAPI 3.1.0, але є застарілим і не входить до стандарту JSON Schema. Тому рекомендується перейти з `example` на `examples`. 🤓 +До цього підтримувався лише ключ `example` з одним прикладом. Він все ще підтримується в OpenAPI 3.1.0, але є застарілим і не входить до стандарту JSON Schema. Тому рекомендується перейти з `example` на `examples`. 🤓 Більше про це можна прочитати в кінці цієї сторінки. /// -## Додаткові аргументи `Field` +## Додаткові аргументи `Field` { #field-additional-arguments } Коли ви використовуєте `Field()` у моделях Pydantic, Ви також можете вказати додаткові `examples`: {* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *} -## `examples` у JSON-схемі — OpenAPI +## `examples` у JSON-схемі — OpenAPI { #examples-in-json-schema-openapi } При використанні будь-кого з наступного: @@ -76,41 +54,41 @@ OpenAPI 3.1.0 (який використовується починаючи з F Ви також можете задати набір `examples` з додатковою інформацією, яка буде додана до їхніх **JSON-схем** у **OpenAPI**. -### `Body` з `examples` +### `Body` з `examples` { #body-with-examples } Тут ми передаємо `examples`, які містять один приклад очікуваних даних у `Body()`: {* ../../docs_src/schema_extra_example/tutorial003_an_py310.py hl[22:29] *} -### Приклад у UI документації +### Приклад у UI документації { #example-in-the-docs-ui } -За допомогою будь-якого з наведених вище методів це виглядатиме так у документації за `/docs`: +За допомогою будь-якого з наведених вище методів це виглядатиме так у `/docs`: <img src="/img/tutorial/body-fields/image01.png"> -### `Body` з кількома `examples` +### `Body` з кількома `examples` { #body-with-multiple-examples } Звичайно, Ви також можете передати кілька `examples`: {* ../../docs_src/schema_extra_example/tutorial004_an_py310.py hl[23:38] *} -Коли Ви це робите, приклади будуть частиною внутрішньої **JSON-схеми** для цих даних. +Коли Ви це робите, приклади будуть частиною внутрішньої **JSON-схеми** для цих даних тіла. -Втім, на момент написання цього (<abbr title="2023-08-26">26 серпня 2023</abbr>), Swagger UI — інструмент, який відповідає за відображення UI документації — не підтримує показ кількох прикладів у **JSON-схеми**. Але нижче можна прочитати про обхідний шлях. +Втім, на момент написання цього (<abbr title="2023-08-26">час написання цього</abbr>), Swagger UI — інструмент, який відповідає за відображення UI документації — не підтримує показ кількох прикладів для даних у **JSON-схемі**. Але нижче можна прочитати про обхідний шлях. -### Специфічні для OpenAPI `examples` +### Специфічні для OpenAPI `examples` { #openapi-specific-examples } -Ще до того, як **JSON-схема** почала підтримувати `examples`, OpenAPI вже мала підтримку поля з такою ж назвою — `examples`. +Ще до того, як **JSON-схема** почала підтримувати `examples`, OpenAPI вже мала підтримку іншого поля, яке також називається `examples`. -Це **специфічне для OpenAPI** поле `examples` розміщується в іншій частині специфікації OpenAPI — у **деталях кожної *операції шляху***, а не всередині самої JSON-схеми. +Це **специфічне для OpenAPI** поле `examples` розміщується в іншому розділі специфікації OpenAPI. Воно розміщується в **деталях кожної *операції шляху***, а не всередині кожної JSON-схеми. -Swagger UI вже давно підтримує це поле `examples`. Тому Ви можете використовувати його, щоб **відображати** кілька **прикладів у документації**. +І Swagger UI вже давно підтримує це поле `examples`. Тому Ви можете використовувати його, щоб **відображати** різні **приклади в UI документації**. -Це поле `examples` у специфікації OpenAPI — це `dict` (словник) з **кількома прикладами** (а не список `list`), кожен із яких може містити додаткову інформацію, що буде додана до **OpenAPI**. +Форма цього специфічного для OpenAPI поля `examples` — це `dict` з **кількома прикладами** (а не `list`), кожен із яких має додаткову інформацію, яка також буде додана до **OpenAPI**. -Воно не включається до JSON Schema кожного параметра, а розміщується зовні, безпосередньо в *операції шляху*. +Воно не включається всередину кожної JSON-схеми, що міститься в OpenAPI, воно розміщується зовні, безпосередньо в *операції шляху*. -### Використання параметра `openapi_examples` +### Використання параметра `openapi_examples` { #using-the-openapi-examples-parameter } Ви можете оголосити специфічні для OpenAPI `examples` у FastAPI за допомогою параметра `openapi_examples` для: @@ -122,30 +100,32 @@ Swagger UI вже давно підтримує це поле `examples`. Том * `Form()` * `File()` -Ключі словника (`dict`) ідентифікують кожен приклад, а кожне значення `dict` — кожен специфічний словник `dict` в `examples` може містити: +Ключі `dict` ідентифікують кожен приклад, а кожне значення — це інший `dict`. + +Кожен специфічний `dict` прикладу в `examples` може містити: * `summary`: короткий опис прикладу. -* `description`: розгорнутий опис (може містити Markdown). -* `value`: сам приклад, наприклад, словник (`dict`). -* `externalValue`: альтернатива `value`, URL-адреса, що вказує на приклад. Проте ця опція може не підтримуватися більшістю інструментів, на відміну від `value`. +* `description`: розгорнутий опис, який може містити Markdown. +* `value`: це сам приклад, який буде показано, наприклад `dict`. +* `externalValue`: альтернатива `value`, URL-адреса, що вказує на приклад. Проте це може не підтримуватися такою кількістю інструментів, як `value`. Використання виглядає так: {* ../../docs_src/schema_extra_example/tutorial005_an_py310.py hl[23:49] *} -### Приклади OpenAPI у UI документації +### Приклади OpenAPI в UI документації { #openapi-examples-in-the-docs-ui } -З параметром `openapi_examples`, доданим до `Body()`, документація `/docs` виглядатиме так: +З `openapi_examples`, доданим до `Body()`, `/docs` виглядатиме так: <img src="/img/tutorial/body-fields/image02.png"> -## Технічні деталі +## Технічні деталі { #technical-details } -/// tip | Підказка +/// tip | Порада -Якщо Ви вже використовуєте **FastAPI** версії **0.99.0 або вище**, Ви можете **пропустити** цей розділ. +Якщо Ви вже використовуєте **FastAPI** версії **0.99.0 або вище**, Ви, ймовірно, можете **пропустити** ці технічні деталі. -Він більш актуальний для старих версій, до появи OpenAPI 3.1.0. +Вони більш актуальні для старих версій, до появи OpenAPI 3.1.0. Можна вважати це коротким **історичним екскурсом** у OpenAPI та JSON Schema. 🤓 @@ -155,68 +135,68 @@ Swagger UI вже давно підтримує це поле `examples`. Том Це дуже технічна інформація про стандарти **JSON Schema** і **OpenAPI**. -Якщо вищезгадані ідеї вже працюють у Вас — можете не заглиблюватися в ці деталі. +Якщо вищезгадані ідеї вже працюють у Вас, цього може бути достатньо, і Вам, ймовірно, не потрібні ці деталі — можете пропустити. /// -До OpenAPI 3.1.0 специфікація використовувала стару та модифіковану версію **JSON Schema**. +До OpenAPI 3.1.0 OpenAPI використовував стару та модифіковану версію **JSON Schema**. -Оскільки JSON Schema раніше не підтримувала `examples`, OpenAPI додала власне поле `examples`. +JSON Schema не мала `examples`, тож OpenAPI додала власне поле `example` до своєї модифікованої версії. -OpenAPI також додала `example` і `examples` до інших частин специфікації: +OpenAPI також додала поля `example` і `examples` до інших частин специфікації: -* <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-object" class="external-link" target="_blank">`Parameter Object` (в специфікації)</a> використовується FastAPI для: +* <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-object" class="external-link" target="_blank">`Parameter Object` (в специфікації)</a>, який використовувався утилітами FastAPI: * `Path()` * `Query()` * `Header()` * `Cookie()` -* <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#media-type-object" class="external-link" target="_blank">`Request Body Object`, в полі `content`, в `Media Type Object` (в специфікації)</a> використовується FastAPI для: +* <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#media-type-object" class="external-link" target="_blank">`Request Body Object`, у полі `content`, у `Media Type Object` (в специфікації)</a>, який використовувався утилітами FastAPI: * `Body()` * `File()` * `Form()` /// info | Інформація -Цей старий параметр `examples`, специфічний для OpenAPI, тепер називається `openapi_examples`, починаючи з FastAPI версії `0.103.0`. +Цей старий специфічний для OpenAPI параметр `examples` тепер називається `openapi_examples`, починаючи з FastAPI `0.103.0`. /// -### Поле `examples` у JSON Schema +### Поле `examples` у JSON Schema { #json-schemas-examples-field } Пізніше JSON Schema додала поле <a href="https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.5" class="external-link" target="_blank">`examples`</a> у нову версію специфікації. -І вже OpenAPI 3.1.0 базується на цій новій версії (JSON Schema 2020-12), яка включає поле `examples`. +А потім новий OpenAPI 3.1.0 базувався на найновішій версії (JSON Schema 2020-12), яка включала це нове поле `examples`. -Тепер це поле `examples` є пріоритетним і замінює старе (і кастомне) поле `example`, яке стало застарілим. +І тепер це нове поле `examples` має вищий пріоритет за старе одиночне (і кастомне) поле `example`, яке тепер є застарілим. -Нове поле `examples` у JSON Schema — це **просто список (`list`)** прикладів, без додаткових метаданих (на відміну від OpenAPI). +Це нове поле `examples` у JSON Schema — це **просто `list`** прикладів, а не `dict` з додатковими метаданими, як в інших місцях OpenAPI (описаних вище). /// info | Інформація -Навіть після того, як з'явився OpenAPI 3.1.0, який підтримував examples у JSON Schema, інструмент Swagger UI ще деякий час не підтримував цю версію (підтримка з’явилась з версії 5.0.0 🎉). +Навіть після релізу OpenAPI 3.1.0 з цією новою простішою інтеграцією з JSON Schema, протягом певного часу Swagger UI, інструмент, який надає автоматичну документацію, не підтримував OpenAPI 3.1.0 (тепер підтримує, починаючи з версії 5.0.0 🎉). Через це версії FastAPI до 0.99.0 все ще використовували версії OpenAPI нижчі за 3.1.0. /// -### `Examples` в Pydantic і FastAPI +### `examples` у Pydantic і FastAPI { #pydantic-and-fastapi-examples } -Коли Ви додаєте `examples` у модель Pydantic через `schema_extra` або `Field(examples=["something"])`, ці приклади додаються до **JSON Schema** цієї моделі. +Коли Ви додаєте `examples` у модель Pydantic через `schema_extra` або `Field(examples=["something"])`, цей приклад додається до **JSON Schema** для цієї моделі Pydantic. -І ця **JSON Schema** Pydantic-моделі включається до **OpenAPI** Вашого API, а потім використовується в UI документації (docs UI). +І ця **JSON Schema** Pydantic-моделі включається до **OpenAPI** Вашого API, а потім використовується в UI документації. -У версіях FastAPI до 0.99.0 (починаючи з 0.99.0 використовується новіший OpenAPI 3.1.0), коли Ви використовували `example` або `examples` з іншими утилітами (`Query()`, `Body()` тощо), ці приклади не додавалися до JSON Schema, який описує ці дані (навіть не до власної версії JSON Schema у OpenAPI). Натомість вони додавалися безпосередньо до опису *обробника шляху* *(path operation)* в OpenAPI (тобто поза межами частин, які використовують JSON Schema). +У версіях FastAPI до 0.99.0 (0.99.0 і вище використовують новіший OpenAPI 3.1.0), коли Ви використовували `example` або `examples` з будь-якими іншими утилітами (`Query()`, `Body()` тощо), ці приклади не додавалися до JSON Schema, що описує ці дані (навіть не до власної версії JSON Schema в OpenAPI), натомість вони додавалися безпосередньо до декларації *операції шляху* в OpenAPI (поза межами частин OpenAPI, які використовують JSON Schema). -Але тепер, коли FastAPI 0.99.0 і вище використовують OpenAPI 3.1.0, а той — JSON Schema 2020-12, разом із Swagger UI 5.0.0 і вище — все стало більш узгодженим, і examples тепер включаються до JSON Schema. +Але тепер, коли FastAPI 0.99.0 і вище використовує OpenAPI 3.1.0, який використовує JSON Schema 2020-12, і Swagger UI 5.0.0 і вище, все стало більш узгодженим, і приклади включаються до JSON Schema. -### Swagger UI та специфічні для OpenAPI `examples` +### Swagger UI та специфічні для OpenAPI `examples` { #swagger-ui-and-openapi-specific-examples } -Раніше (станом на 26 серпня 2023 року) Swagger UI не підтримував кілька прикладів у JSON Schema, тому користувачі не мали можливості показати декілька прикладів у документації. +Оскільки Swagger UI не підтримував кілька прикладів JSON Schema (станом на 2023-08-26), користувачі не мали можливості показати кілька прикладів у документації. -Щоб вирішити це, FastAPI починаючи з версії 0.103.0 **додав підтримку** старого **OpenAPI-специфічного** поля `examples` через новий параметр `openapi_examples`. 🤓 +Щоб вирішити це, FastAPI `0.103.0` **додав підтримку** оголошення того самого старого **OpenAPI-специфічного** поля `examples` через новий параметр `openapi_examples`. 🤓 -### Підсумок +### Підсумок { #summary } -Раніше я казав, що не люблю історію... а тепер ось я — розповідаю "технічні історичні" лекції. 😅 +Раніше я казав, що не дуже люблю історію... а тепер подивіться на мене — читаю «технічні історичні» лекції. 😅 Коротко: **оновіться до FastAPI 0.99.0 або вище** — і все стане значно **простішим, узгодженим та інтуїтивно зрозумілим**, і Вам не доведеться знати всі ці історичні деталі. 😎 diff --git a/docs/uk/docs/tutorial/security/index.md b/docs/uk/docs/tutorial/security/index.md index c3d94be8d..f1fb25178 100644 --- a/docs/uk/docs/tutorial/security/index.md +++ b/docs/uk/docs/tutorial/security/index.md @@ -1,70 +1,72 @@ -# Безпека +# Безпека { #security } Існує багато способів реалізувати безпеку, автентифікацію та авторизацію. -Це зазвичай складна і "непроста" тема. +І зазвичай це складна і «непроста» тема. -У багатьох фреймворках і системах забезпечення безпеки та автентифікації займає величезну частину зусиль і коду (іноді — понад 50% всього написаного коду). +У багатьох фреймворках і системах лише обробка безпеки та автентифікації потребує великих зусиль і коду (у багатьох випадках це може бути 50% або більше від усього написаного коду). -**FastAPI** надає кілька інструментів, які допоможуть Вам впоратися з **безпекою** легко, швидко, стандартним способом, без необхідності вивчати всі специфікації безпеки. +**FastAPI** надає кілька інструментів, які допоможуть вам працювати з **безпекою** легко, швидко, стандартним способом, без необхідності вивчати всі специфікації безпеки. -Але спочатку — кілька коротких понять. +Але спочатку розгляньмо кілька невеликих понять. -## Поспішаєте? +## Поспішаєте? { #in-a-hurry } -Якщо Вам не цікаві всі ці терміни й просто потрібно *швидко* додати автентифікацію за логіном і паролем — переходьте до наступних розділів. +Якщо вам не цікаві всі ці терміни й вам просто потрібно додати безпеку з автентифікацією на основі імені користувача та пароля *прямо зараз*, переходьте до наступних розділів. -## OAuth2 +## OAuth2 { #oauth2 } -OAuth2 — це специфікація, що описує кілька способів обробки автентифікації та авторизації. +OAuth2 — це специфікація, що визначає кілька способів обробки автентифікації та авторизації. -Це досить об'ємна специфікація, яка охоплює складні випадки використання. +Це досить об'ємна специфікація, яка охоплює кілька складних випадків використання. -Вона включає способи автентифікації через "третю сторону". +Вона включає способи автентифікації через «третю сторону». -Саме це лежить в основі "входу через Google, Facebook, X (Twitter), GitHub" тощо. +Саме це лежить в основі всіх систем із «увійти через Facebook, Google, X (Twitter), GitHub». -### OAuth 1 +### OAuth 1 { #oauth-1 } -Раніше існував OAuth 1, який значно відрізняється від OAuth2 і є складнішим, оскільки містив специфікації для шифрування комунікацій. +Раніше існував OAuth 1, який значно відрізняється від OAuth2 і є складнішим, оскільки містив прямі специфікації щодо того, як шифрувати комунікацію. -Зараз майже не використовується. +Зараз він не дуже популярний або використовується. -OAuth2 не вказує, як саме шифрувати з'єднання — воно очікує, що ваш застосунок працює через HTTPS. +OAuth2 не вказує, як саме шифрувати комунікацію — він очікує, що ваш застосунок доступний через HTTPS. /// tip | Порада -У розділі про **деплой** Ви побачите, як налаштувати HTTPS безкоштовно з Traefik та Let's Encrypt. +У розділі про **деплой** ви побачите, як налаштувати HTTPS безкоштовно з Traefik та Let's Encrypt. /// -## OpenID Connect +## OpenID Connect { #openid-connect } OpenID Connect — ще одна специфікація, побудована на основі **OAuth2**. -Вона розширює OAuth2, уточнюючи деякі неоднозначності для досягнення кращої сумісності. +Вона просто розширює OAuth2, уточнюючи деякі відносно неоднозначні речі в OAuth2, щоб зробити його більш сумісним. -Наприклад, вхід через Google використовує OpenID Connect (який базується на OAuth2). +Наприклад, вхід через Google використовує OpenID Connect (який під капотом використовує OAuth2). -Але вхід через Facebook — ні. Він має власну реалізацію на базі OAuth2. +Але вхід через Facebook не підтримує OpenID Connect. Він має власний різновид OAuth2. -### OpenID (не "OpenID Connect") +### OpenID (не «OpenID Connect») { #openid-not-openid-connect } -Існувала також специфікація "OpenID", яка намагалася розвʼязати ті самі задачі, що й **OpenID Connect**, але не базувалась на OAuth2. +Існувала також специфікація «OpenID». Вона намагалася розвʼязати те саме, що й **OpenID Connect**, але не базувалась на OAuth2. -Це була зовсім інша система, і сьогодні вона майже не використовується. +Тож це була повністю додаткова система. -## OpenAPI +Зараз вона не дуже популярна або використовується. -OpenAPI (раніше Swagger) — це специфікація для побудови API (тепер під егідою Linux Foundation). +## OpenAPI { #openapi } + +OpenAPI (раніше відомий як Swagger) — це відкрита специфікація для побудови API (тепер частина Linux Foundation). **FastAPI** базується на **OpenAPI**. -Завдяки цьому Ви отримуєте автоматичну інтерактивну документацію, генерацію коду та багато іншого. +Саме це робить можливими кілька автоматичних інтерактивних інтерфейсів документації, генерацію коду тощо. -OpenAPI дозволяє описувати різні "схеми" безпеки. +OpenAPI має спосіб визначати різні «схеми» безпеки. -Використовуючи їх, Ви можете скористатися всіма цими інструментами, що базуються на стандартах, зокрема інтерактивними системами документації. +Використовуючи їх, ви можете скористатися всіма цими інструментами, що базуються на стандартах, зокрема цими інтерактивними системами документації. OpenAPI визначає такі схеми безпеки: @@ -72,33 +74,33 @@ OpenAPI визначає такі схеми безпеки: * Параметр запиту. * Заголовок. * Cookie. -* `http`: стандартні методи HTTP-автентифікації, включаючи: - * `bearer`: заголовок `Authorization` зі значенням `Bearer` та токеном. Це успадковано з OAuth2. - * HTTP Basic автентифікація - * HTTP Digest, тощо. +* `http`: стандартні системи HTTP-автентифікації, включаючи: + * `bearer`: заголовок `Authorization` зі значенням `Bearer ` та токеном. Це успадковано з OAuth2. + * HTTP Basic автентифікацію. + * HTTP Digest тощо. * `oauth2`: усі способи обробки безпеки за допомогою OAuth2 (так звані «потоки»). - * Деякі з цих потоків підходять для створення власного провайдера автентифікації OAuth 2.0 (наприклад, Google, Facebook, X (Twitter), GitHub тощо): - * `implicit`— неявний - * `clientCredentials`— облікові дані клієнта - * `authorizationCode` — код авторизації - * Але є один окремий «потік», який ідеально підходить для реалізації автентифікації всередині одного додатку: - * `password`: у наступних розділах буде приклад використання цього потоку. -* `openIdConnect`: дозволяє автоматично виявляти параметри автентифікації OAuth2. - * Це автоматичне виявлення визначається у специфікації OpenID Connect. + * Декілька з цих потоків підходять для створення провайдера автентифікації OAuth 2.0 (наприклад, Google, Facebook, X (Twitter), GitHub тощо): + * `implicit` + * `clientCredentials` + * `authorizationCode` + * Але є один окремий «потік», який можна ідеально використати для обробки автентифікації напряму в цьому ж застосунку: + * `password`: у кількох наступних розділах будуть приклади цього. +* `openIdConnect`: має спосіб визначити, як автоматично виявляти дані автентифікації OAuth2. + * Саме це автоматичне виявлення визначено у специфікації OpenID Connect. /// tip | Порада -Інтеграція інших провайдерів автентифікації/авторизації, таких як Google, Facebook, X (Twitter), GitHub тощо — також можлива і відносно проста. +Інтеграція інших провайдерів автентифікації/авторизації, таких як Google, Facebook, X (Twitter), GitHub тощо, також можлива і відносно проста. -Найскладніше — це створити власного провайдера автентифікації/авторизації, як Google чи Facebook. Але **FastAPI** надає Вам інструменти, щоб зробити це легко, беручи на себе важку частину роботи. +Найскладніше — це створити провайдера автентифікації/авторизації на кшталт таких, але **FastAPI** надає вам інструменти, щоб зробити це легко, виконуючи важку частину роботи за вас. /// -## Інструменти **FastAPI** +## Утиліти **FastAPI** { #fastapi-utilities } -FastAPI надає кілька інструментів для кожної з описаних схем безпеки в модулі `fastapi.security`, які спрощують використання цих механізмів захисту. +FastAPI надає кілька інструментів для кожної з описаних схем безпеки в модулі `fastapi.security`, які спрощують використання цих механізмів безпеки. -У наступних розділах Ви побачите, як додати безпеку до свого API за допомогою цих інструментів **FastAPI**. +У наступних розділах ви побачите, як додати безпеку до свого API за допомогою цих інструментів, які надає **FastAPI**. -А також побачите, як вона автоматично інтегрується в інтерактивну документацію вашого API. +А також побачите, як це автоматично інтегрується в інтерактивну систему документації. diff --git a/docs/uk/docs/tutorial/static-files.md b/docs/uk/docs/tutorial/static-files.md index 3427f2376..32ca1311d 100644 --- a/docs/uk/docs/tutorial/static-files.md +++ b/docs/uk/docs/tutorial/static-files.md @@ -1,13 +1,13 @@ -# Статичні файли +# Статичні файли { #static-files } Ви можете автоматично надавати статичні файли з каталогу, використовуючи `StaticFiles`. -## Використання `StaticFiles` +## Використання `StaticFiles` { #use-staticfiles } * Імпортуйте `StaticFiles`. -* "Під'єднати" екземпляр `StaticFiles()` з вказанням необхідного шляху. +* «Під'єднати» екземпляр `StaticFiles()` з вказанням необхідного шляху. -{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} +{* ../../docs_src/static_files/tutorial001_py39.py hl[2,6] *} /// note | Технічні деталі @@ -17,24 +17,24 @@ /// -### Що таке "Під'єднання" +### Що таке «Під'єднання» { #what-is-mounting } -"Під'єднання" означає додавання повноцінного "незалежного" застосунку за певним шляхом, який потім обробляє всі під шляхи. +«Під'єднання» означає додавання повноцінного «незалежного» застосунку за певним шляхом, який потім обробляє всі під шляхи. -Це відрізняється від використання `APIRouter`, оскільки під'єднаний застосунок є повністю незалежним. OpenAPI та документація вашого основного застосунку не будуть знати нічого про ваш під'єднаний застосунок. +Це відрізняється від використання `APIRouter`, оскільки під'єднаний застосунок є повністю незалежним. OpenAPI та документація вашого основного застосунку не будуть знати нічого про ваш під'єднаний застосунок тощо. Ви можете дізнатися більше про це в [Посібнику для просунутих користувачів](../advanced/index.md){.internal-link target=_blank}. -## Деталі +## Деталі { #details } -Перше `"/static"` вказує на під шлях, за яким буде "під'єднано" цей новий "застосунок". Тому будь-який шлях, який починається з `"/static"`, буде оброблятися ним. +Перше `"/static"` вказує на під шлях, за яким буде «під'єднано» цей новий «підзастосунок». Тому будь-який шлях, який починається з `"/static"`, буде оброблятися ним. -`directory="static"` визначає каталог, що містить ваші статичні файли. +`directory="static"` визначає назву каталогу, що містить ваші статичні файли. `name="static"` це ім'я, яке можна використовувати всередині **FastAPI**. -Усі ці параметри можуть бути змінені відповідно до потреб і особливостей вашого застосунку. +Усі ці параметри можуть бути іншими за "`static`", налаштуйте їх відповідно до потреб і особливостей вашого застосунку. -## Додаткова інформація +## Додаткова інформація { #more-info } Детальніше про налаштування та можливості можна дізнатися в <a href="https://www.starlette.dev/staticfiles/" class="external-link" target="_blank">документації Starlette про статичні файли</a>. diff --git a/docs/uk/docs/tutorial/testing.md b/docs/uk/docs/tutorial/testing.md index 1105c6b0a..462592829 100644 --- a/docs/uk/docs/tutorial/testing.md +++ b/docs/uk/docs/tutorial/testing.md @@ -1,17 +1,18 @@ -# Тестування +# Тестування { #testing } -Тестування **FastAPI** додатків є простим та ефективним завдяки бібліотеці <a href="https://www.starlette.dev/testclient/" class="external-link" target="_blank">Starlette</a>, яка базується на <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>. -Оскільки HTTPX розроблений на основі Requests, його API є інтуїтивно зрозумілим для тих, хто вже знайомий з Requests. +Завдяки <a href="https://www.starlette.dev/testclient/" class="external-link" target="_blank">Starlette</a> тестувати застосунки **FastAPI** просто й приємно. -З його допомогою Ви можете використовувати <a href="https://docs.pytest.org/" class="external-link" target="_blank">pytest</a> безпосередньо з **FastAPI**. +Воно базується на <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>, який, своєю чергою, спроєктований на основі Requests, тож він дуже знайомий та інтуїтивно зрозумілий. -## Використання `TestClient` +З його допомогою ви можете використовувати <a href="https://docs.pytest.org/" class="external-link" target="_blank">pytest</a> безпосередньо з **FastAPI**. + +## Використання `TestClient` { #using-testclient } /// info | Інформація Щоб використовувати `TestClient`, спочатку встановіть <a href="https://www.python-httpx.org" class="external-link" target="_blank">`httpx`</a>. -Переконайтеся, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили саму бібліотеку, наприклад: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили `httpx`, наприклад: ```console $ pip install httpx @@ -21,7 +22,7 @@ $ pip install httpx Імпортуйте `TestClient`. -Створіть `TestClient`, передавши йому Ваш застосунок **FastAPI**. +Створіть `TestClient`, передавши йому ваш застосунок **FastAPI**. Створюйте функції з іменами, що починаються з `test_` (це стандартна угода для `pytest`). @@ -29,8 +30,7 @@ $ pip install httpx Записуйте прості `assert`-вирази зі стандартними виразами Python, які потрібно перевірити (це також стандарт для `pytest`). -{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *} - +{* ../../docs_src/app_testing/tutorial001_py39.py hl[2,12,15:18] *} /// tip | Порада @@ -46,25 +46,25 @@ $ pip install httpx Ви також можете використовувати `from starlette.testclient import TestClient`. -**FastAPI** надає той самий `starlette.testclient` під назвою `fastapi.testclient` для зручності розробників, але він безпосередньо походить із Starlette. +**FastAPI** надає той самий `starlette.testclient` під назвою `fastapi.testclient` просто для зручності для вас, розробника. Але він безпосередньо походить із Starlette. /// /// tip | Порада -Якщо Вам потрібно викликати `async`-функції у ваших тестах, окрім відправлення запитів до FastAPI-застосунку (наприклад, асинхронні функції роботи з базою даних), перегляньте [Асинхронні тести](../advanced/async-tests.md){.internal-link target=_blank} у розширеному керівництві. +Якщо ви хочете викликати `async`-функції у ваших тестах, окрім відправлення запитів до вашого застосунку FastAPI (наприклад, асинхронні функції роботи з базою даних), перегляньте [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} у розширеному керівництві. /// -## Розділення тестів +## Розділення тестів { #separating-tests } -У реальному застосунку Ваші тести, ймовірно, будуть в окремому файлі. +У реальному застосунку ваші тести, ймовірно, будуть в окремому файлі. -Також Ваш **FastAPI**-застосунок може складатися з кількох файлів або модулів тощо. +Також ваш застосунок **FastAPI** може складатися з кількох файлів/модулів тощо. -### Файл застосунку **FastAPI** +### Файл застосунку **FastAPI** { #fastapi-app-file } -Припустимо, у Вас є структура файлів, описана в розділі [Більші застосунки](bigger-applications.md){.internal-link target=_blank}: +Припустимо, у вас є структура файлів, описана в розділі [Bigger Applications](bigger-applications.md){.internal-link target=_blank}: ``` . @@ -72,14 +72,15 @@ $ pip install httpx │   ├── __init__.py │   └── main.py ``` -У файлі `main.py` знаходиться Ваш застосунок **FastAPI** : -{* ../../docs_src/app_testing/main.py *} +У файлі `main.py` знаходиться ваш застосунок **FastAPI**: -### Файл тестування -Ви можете створити файл `test_main.py` з Вашими тестами. Він може знаходитися в тому ж пакеті Python (у тій самій директорії з файлом `__init__.py`): +{* ../../docs_src/app_testing/app_a_py39/main.py *} +### Файл тестування { #testing-file } + +Ви можете створити файл `test_main.py` з вашими тестами. Він може знаходитися в тому ж пакеті Python (у тій самій директорії з файлом `__init__.py`): ``` hl_lines="5" . @@ -89,18 +90,18 @@ $ pip install httpx │   └── test_main.py ``` -Оскільки цей файл знаходиться в тому ж пакеті, Ви можете використовувати відносний імпорт, щоб імпортувати об'єкт `app` із модуля `main` (`main.py`): +Оскільки цей файл знаходиться в тому ж пакеті, ви можете використовувати відносний імпорт, щоб імпортувати об'єкт `app` із модуля `main` (`main.py`): -{* ../../docs_src/app_testing/test_main.py hl[3] *} +{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *} ...і написати код для тестів так само як і раніше. -## Тестування: розширений приклад +## Тестування: розширений приклад { #testing-extended-example } Тепер розширимо цей приклад і додамо більше деталей, щоб побачити, як тестувати різні частини. -### Розширений файл застосунку **FastAPI** +### Розширений файл застосунку **FastAPI** { #extended-fastapi-app-file } Залишимо ту саму структуру файлів: @@ -112,75 +113,26 @@ $ pip install httpx │   └── test_main.py ``` -Припустимо, що тепер файл `main.py` із Вашим **FastAPI**-застосунком містить додаткові операції шляху (**path operations**). +Припустимо, що тепер файл `main.py` із вашим застосунком **FastAPI** містить інші **операції шляху**. Він має `GET`-операцію, яка може повертати помилку. Він має `POST`-операцію, яка може повертати кілька помилок. -Обидві операції шляху вимагають заголовок `X-Token`. +Обидві *операції шляху* вимагають заголовок `X-Token`. -//// tab | Python 3.10+ +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} -```Python -{!> ../../docs_src/app_testing/app_b_an_py310/main.py!} -``` +### Розширений тестовий файл { #extended-testing-file } -//// +Потім ви можете оновити `test_main.py`, додавши розширені тести: -//// tab | Python 3.9+ +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} -```Python -{!> ../../docs_src/app_testing/app_b_an_py39/main.py!} -``` -//// +Коли вам потрібно передати клієнту інформацію в запиті, але ви не знаєте, як це зробити, ви можете пошукати (Google), як це зробити в `httpx`, або навіть як це зробити з `requests`, оскільки дизайн HTTPX базується на дизайні Requests. -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/app_testing/app_b_an/main.py!} -``` - -//// - -//// tab | Python 3.10+ non-Annotated - -/// tip | Порада - -Бажано використовувати версію з `Annotated`, якщо це можливо - -/// - -```Python -{!> ../../docs_src/app_testing/app_b_py310/main.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated - -/// tip | Порада - -Бажано використовувати версію з `Annotated`, якщо це можливо - -/// - -```Python -{!> ../../docs_src/app_testing/app_b/main.py!} -``` - -//// - -### Розширений тестовий файл - -Потім Ви можете оновити `test_main.py`, додавши розширені тести: - -{* ../../docs_src/app_testing/app_b/test_main.py *} - -Коли Вам потрібно передати клієнту інформацію в запиті, але Ви не знаєте, як це зробити, Ви можете пошукати (наприклад, у Google) спосіб реалізації в `httpx`, або навіть у `requests`, оскільки HTTPX розроблений на основі дизайну Requests. - -Далі Ви просто повторюєте ці ж дії у ваших тестах. +Далі ви просто повторюєте ці ж дії у ваших тестах. Наприклад: @@ -195,15 +147,16 @@ $ pip install httpx /// info | Інформація Зверніть увагу, що `TestClient` отримує дані, які можна конвертувати в JSON, а не Pydantic-моделі. -Якщо у Вас є Pydantic-модель у тесті, і Ви хочете передати її дані в додаток під час тестування, Ви можете використати `jsonable_encoder`, описаний у розділі [JSON Compatible Encoder](encoder.md){.internal-link target=_blank}. + +Якщо у вас є Pydantic-модель у тесті, і ви хочете передати її дані в застосунок під час тестування, ви можете використати `jsonable_encoder`, описаний у розділі [JSON Compatible Encoder](encoder.md){.internal-link target=_blank}. /// -## Запуск тестів +## Запуск { #run-it } Після цього вам потрібно встановити `pytest`. -Переконайтеся, що Ви створили [віртуальне середовище]{.internal-link target=_blank}, активували його і встановили необхідні пакети, наприклад: +Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його і встановили необхідні пакети, наприклад: <div class="termy"> @@ -215,7 +168,7 @@ $ pip install pytest </div> -`pytest` автоматично знайде файли з тестами, виконає їх і надасть вам результати. +Він автоматично знайде файли та тести, виконає їх і повідомить вам результати. Запустіть тести за допомогою: diff --git a/scripts/docs.py b/scripts/docs.py index 84cf01c72..a9abce56b 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -25,6 +25,7 @@ SUPPORTED_LANGS = { "es", "pt", "ru", + "uk", } From cf8dc98aadc07d28c8f3dc0497324128e782c358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 10 Jan 2026 16:15:26 -0800 Subject: [PATCH 062/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20ko=20(update-outdated)=20(#14589)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Yurii Motov <yurii.motov.monte@gmail.com> --- docs/en/mkdocs.yml | 2 + docs/ko/docs/about/index.md | 4 +- .../docs/advanced/additional-status-codes.md | 10 +- .../ko/docs/advanced/advanced-dependencies.md | 123 +++- docs/ko/docs/advanced/async-tests.md | 71 +- docs/ko/docs/advanced/custom-response.md | 114 ++-- docs/ko/docs/advanced/events.md | 120 ++-- docs/ko/docs/advanced/index.md | 12 +- .../advanced/response-change-status-code.md | 16 +- docs/ko/docs/advanced/response-cookies.md | 30 +- docs/ko/docs/advanced/response-directly.md | 42 +- docs/ko/docs/advanced/response-headers.md | 22 +- docs/ko/docs/advanced/sub-applications.md | 60 +- docs/ko/docs/advanced/templates.md | 45 +- docs/ko/docs/advanced/testing-dependencies.md | 18 +- docs/ko/docs/advanced/testing-events.md | 13 +- docs/ko/docs/advanced/testing-websockets.md | 8 +- .../docs/advanced/using-request-directly.md | 26 +- docs/ko/docs/advanced/websockets.md | 70 +- docs/ko/docs/advanced/wsgi.md | 14 +- docs/ko/docs/benchmarks.md | 14 +- docs/ko/docs/deployment/cloud.md | 21 +- docs/ko/docs/deployment/docker.md | 621 +++++++----------- docs/ko/docs/deployment/index.md | 24 +- docs/ko/docs/deployment/server-workers.md | 186 ++---- docs/ko/docs/deployment/versions.md | 133 ++-- docs/ko/docs/environment-variables.md | 52 +- docs/ko/docs/how-to/conditional-openapi.md | 33 +- docs/ko/docs/how-to/configure-swagger-ui.md | 24 +- docs/ko/docs/index.md | 267 +++++--- docs/ko/docs/learn/index.md | 6 +- docs/ko/docs/project-generation.md | 28 +- docs/ko/docs/python-types.md | 527 +++++++++------ docs/ko/docs/resources/index.md | 4 +- docs/ko/docs/tutorial/background-tasks.md | 88 +-- docs/ko/docs/tutorial/body-fields.md | 16 +- docs/ko/docs/tutorial/body-multiple-params.md | 130 ++-- docs/ko/docs/tutorial/body-nested-models.md | 147 ++--- docs/ko/docs/tutorial/body.md | 58 +- docs/ko/docs/tutorial/cookie-param-models.md | 24 +- docs/ko/docs/tutorial/cookie-params.md | 22 +- docs/ko/docs/tutorial/cors.md | 92 +-- docs/ko/docs/tutorial/debugging.md | 20 +- .../dependencies/classes-as-dependencies.md | 239 +++++-- ...pendencies-in-path-operation-decorators.md | 20 +- .../dependencies/dependencies-with-yield.md | 124 ++-- .../dependencies/global-dependencies.md | 17 +- docs/ko/docs/tutorial/dependencies/index.md | 70 +- docs/ko/docs/tutorial/encoder.md | 30 +- docs/ko/docs/tutorial/extra-data-types.md | 12 +- docs/ko/docs/tutorial/extra-models.md | 72 +- docs/ko/docs/tutorial/first-steps.md | 213 +++--- docs/ko/docs/tutorial/header-param-models.md | 26 +- docs/ko/docs/tutorial/header-params.md | 24 +- docs/ko/docs/tutorial/index.md | 95 +-- docs/ko/docs/tutorial/metadata.md | 78 +-- docs/ko/docs/tutorial/middleware.md | 89 ++- .../tutorial/path-operation-configuration.md | 74 ++- .../path-params-numeric-validations.md | 135 ++-- docs/ko/docs/tutorial/path-params.md | 143 ++-- docs/ko/docs/tutorial/query-param-models.md | 34 +- .../tutorial/query-params-str-validations.md | 360 +++++++--- docs/ko/docs/tutorial/query-params.md | 65 +- docs/ko/docs/tutorial/request-files.md | 81 ++- docs/ko/docs/tutorial/request-form-models.md | 18 +- .../docs/tutorial/request-forms-and-files.md | 30 +- docs/ko/docs/tutorial/request-forms.md | 17 +- docs/ko/docs/tutorial/response-model.md | 277 +++++--- docs/ko/docs/tutorial/response-status-code.md | 72 +- docs/ko/docs/tutorial/schema-extra-example.md | 104 ++- .../tutorial/security/get-current-user.md | 96 +-- docs/ko/docs/tutorial/security/oauth2-jwt.md | 182 ++--- .../docs/tutorial/security/simple-oauth2.md | 58 +- docs/ko/docs/tutorial/sql-databases.md | 155 +++-- docs/ko/docs/tutorial/static-files.md | 39 +- docs/ko/docs/tutorial/testing.md | 146 ++-- docs/ko/docs/virtual-environments.md | 400 +++++------ scripts/docs.py | 1 + 78 files changed, 3842 insertions(+), 3111 deletions(-) diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 84255f0f8..1c2954619 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -317,6 +317,8 @@ extra: name: de - Deutsch - link: /es/ name: es - español + - link: /ko/ + name: ko - 한국어 - link: /pt/ name: pt - português - link: /ru/ diff --git a/docs/ko/docs/about/index.md b/docs/ko/docs/about/index.md index ee7804d32..dc2c72874 100644 --- a/docs/ko/docs/about/index.md +++ b/docs/ko/docs/about/index.md @@ -1,3 +1,3 @@ -# 소개 +# 소개 { #about } -FastAPI에 대한 디자인, 영감 등에 대해 🤓 +FastAPI, 그 디자인, 영감 등에 대해 🤓 diff --git a/docs/ko/docs/advanced/additional-status-codes.md b/docs/ko/docs/advanced/additional-status-codes.md index da06cb778..64a7eabd5 100644 --- a/docs/ko/docs/advanced/additional-status-codes.md +++ b/docs/ko/docs/advanced/additional-status-codes.md @@ -1,16 +1,16 @@ -# 추가 상태 코드 +# 추가 상태 코드 { #additional-status-codes } 기본적으로 **FastAPI**는 응답을 `JSONResponse`를 사용하여 반환하며, *경로 작업(path operation)*에서 반환한 내용을 해당 `JSONResponse` 안에 넣어 반환합니다. 기본 상태 코드 또는 *경로 작업*에서 설정한 상태 코드를 사용합니다. -## 추가 상태 코드 +## 추가 상태 코드 { #additional-status-codes_1 } 기본 상태 코드와 별도로 추가 상태 코드를 반환하려면 `JSONResponse`와 같이 `Response`를 직접 반환하고 추가 상태 코드를 직접 설정할 수 있습니다. 예를 들어 항목을 업데이트할 수 있는 *경로 작업*이 있고 성공 시 200 “OK”의 HTTP 상태 코드를 반환한다고 가정해 보겠습니다. -하지만 새로운 항목을 허용하기를 원할 것입니다. 항목이 이전에 존재하지 않았다면 이를 생성하고 HTTP 상태 코드 201 "Created"를 반환합니다. +하지만 새로운 항목을 허용하기를 원할 것입니다. 그리고 항목이 이전에 존재하지 않았다면 이를 생성하고 HTTP 상태 코드 201 "Created"를 반환합니다. 이를 위해서는 `JSONResponse`를 가져와서 원하는 `status_code`를 설정하여 콘텐츠를 직접 반환합니다: @@ -26,7 +26,7 @@ /// -/// note | 기술적 세부 정보 +/// note | 기술 세부사항 `from starlette.responses import JSONResponse`를 사용할 수도 있습니다. @@ -34,7 +34,7 @@ /// -## OpenAPI 및 API 문서 +## OpenAPI 및 API 문서 { #openapi-and-api-docs } 추가 상태 코드와 응답을 직접 반환하는 경우, FastAPI는 반환할 내용을 미리 알 수 있는 방법이 없기 때문에 OpenAPI 스키마(API 문서)에 포함되지 않습니다. diff --git a/docs/ko/docs/advanced/advanced-dependencies.md b/docs/ko/docs/advanced/advanced-dependencies.md index 7fa043fa3..04e557d15 100644 --- a/docs/ko/docs/advanced/advanced-dependencies.md +++ b/docs/ko/docs/advanced/advanced-dependencies.md @@ -1,6 +1,6 @@ -# 고급 의존성 +# 고급 의존성 { #advanced-dependencies } -## 매개변수화된 의존성 +## 매개변수화된 의존성 { #parameterized-dependencies } 지금까지 본 모든 의존성은 고정된 함수 또는 클래스입니다. @@ -10,7 +10,7 @@ 이때 해당 고정된 내용을 매개변수화할 수 있길 바랍니다. -## "호출 가능한" 인스턴스 +## "호출 가능한" 인스턴스 { #a-callable-instance } Python에는 클래스의 인스턴스를 "호출 가능"하게 만드는 방법이 있습니다. @@ -21,9 +21,9 @@ Python에는 클래스의 인스턴스를 "호출 가능"하게 만드는 방법 {* ../../docs_src/dependencies/tutorial011_an_py39.py hl[12] *} 이 경우, **FastAPI**는 추가 매개변수와 하위 의존성을 확인하기 위해 `__call__`을 사용하게 되며, -나중에 *경로 연산 함수*에서 매개변수에 값을 전달할 때 이를 호출하게 됩니다. +나중에 *경로 처리 함수*에서 매개변수에 값을 전달할 때 이를 호출하게 됩니다. -## 인스턴스 매개변수화하기 +## 인스턴스 매개변수화하기 { #parameterize-the-instance } 이제 `__init__`을 사용하여 의존성을 "매개변수화"할 수 있는 인스턴스의 매개변수를 선언할 수 있습니다: @@ -31,7 +31,7 @@ Python에는 클래스의 인스턴스를 "호출 가능"하게 만드는 방법 이 경우, **FastAPI**는 `__init__`에 전혀 관여하지 않으며, 우리는 이 메서드를 코드에서 직접 사용하게 됩니다. -## 인스턴스 생성하기 +## 인스턴스 생성하기 { #create-an-instance } 다음과 같이 이 클래스의 인스턴스를 생성할 수 있습니다: @@ -39,10 +39,9 @@ Python에는 클래스의 인스턴스를 "호출 가능"하게 만드는 방법 이렇게 하면 `checker.fixed_content` 속성에 `"bar"`라는 값을 담아 의존성을 "매개변수화"할 수 있습니다. -## 인스턴스를 의존성으로 사용하기 +## 인스턴스를 의존성으로 사용하기 { #use-the-instance-as-a-dependency } -그런 다음, `Depends(FixedContentQueryChecker)` 대신 `Depends(checker)`에서 이 `checker` 인스턴스를 사용할 수 있으며, -클래스 자체가 아닌 인스턴스 `checker`가 의존성이 됩니다. +그런 다음, 클래스 자체가 아닌 인스턴스 `checker`가 의존성이 되므로, `Depends(FixedContentQueryChecker)` 대신 `Depends(checker)`에서 이 `checker` 인스턴스를 사용할 수 있습니다. 의존성을 해결할 때 **FastAPI**는 이 `checker`를 다음과 같이 호출합니다: @@ -50,18 +49,116 @@ Python에는 클래스의 인스턴스를 "호출 가능"하게 만드는 방법 checker(q="somequery") ``` -...그리고 이때 반환되는 값을 *경로 연산 함수*의 `fixed_content_included` 매개변수로 전달합니다: +...그리고 이때 반환되는 값을 *경로 처리 함수*의 의존성 값으로, `fixed_content_included` 매개변수에 전달합니다: {* ../../docs_src/dependencies/tutorial011_an_py39.py hl[22] *} -/// tip | 참고 +/// tip | 팁 이 모든 과정이 복잡하게 느껴질 수 있습니다. 그리고 지금은 이 방법이 얼마나 유용한지 명확하지 않을 수도 있습니다. 이 예시는 의도적으로 간단하게 만들었지만, 전체 구조가 어떻게 작동하는지 보여줍니다. -보안 관련 장에서는 이와 같은 방식으로 구현된 편의 함수들이 있습니다. +보안 관련 장에서는 이와 같은 방식으로 구현된 유틸리티 함수들이 있습니다. -이 모든 과정을 이해했다면, 이러한 보안 도구들이 내부적으로 어떻게 작동하는지 이미 파악한 것입니다. +이 모든 과정을 이해했다면, 이러한 보안용 유틸리티 도구들이 내부적으로 어떻게 작동하는지 이미 파악한 것입니다. /// + +## `yield`, `HTTPException`, `except`, 백그라운드 태스크가 있는 의존성 { #dependencies-with-yield-httpexception-except-and-background-tasks } + +/// warning | 경고 + +대부분의 경우 이러한 기술 세부사항이 필요하지 않을 것입니다. + +이 세부사항은 주로 0.121.0 이전의 FastAPI 애플리케이션이 있고 `yield`가 있는 의존성에서 문제가 발생하는 경우에 유용합니다. + +/// + +`yield`가 있는 의존성은 여러 사용 사례를 수용하고 일부 문제를 해결하기 위해 시간이 지나며 발전해 왔습니다. 다음은 변경된 내용의 요약입니다. + +### `yield`와 `scope`가 있는 의존성 { #dependencies-with-yield-and-scope } + +0.121.0 버전에서 FastAPI는 `yield`가 있는 의존성에 대해 `Depends(scope="function")` 지원을 추가했습니다. + +`Depends(scope="function")`를 사용하면, `yield` 이후의 종료 코드는 *경로 처리 함수*가 끝난 직후(클라이언트에 응답이 반환되기 전)에 실행됩니다. + +그리고 `Depends(scope="request")`(기본값)를 사용하면, `yield` 이후의 종료 코드는 응답이 전송된 후에 실행됩니다. + +자세한 내용은 [Dependencies with `yield` - Early exit and `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope) 문서를 참고하세요. + +### `yield`가 있는 의존성과 `StreamingResponse`, 기술 세부사항 { #dependencies-with-yield-and-streamingresponse-technical-details } + +FastAPI 0.118.0 이전에는 `yield`가 있는 의존성을 사용하면, *경로 처리 함수*가 반환된 뒤 응답을 보내기 직전에 `yield` 이후의 종료 코드가 실행되었습니다. + +의도는 응답이 네트워크를 통해 전달되기를 기다리면서 필요한 것보다 더 오래 리소스를 점유하지 않도록 하는 것이었습니다. + +이 변경은 `StreamingResponse`를 반환하는 경우에도 `yield`가 있는 의존성의 종료 코드가 이미 실행된다는 의미이기도 했습니다. + +예를 들어, `yield`가 있는 의존성에 데이터베이스 세션이 있다면, `StreamingResponse`는 데이터를 스트리밍하는 동안 해당 세션을 사용할 수 없게 됩니다. `yield` 이후의 종료 코드에서 세션이 이미 닫혔기 때문입니다. + +이 동작은 0.118.0에서 되돌려져, `yield` 이후의 종료 코드가 응답이 전송된 뒤 실행되도록 변경되었습니다. + +/// info | 정보 + +아래에서 보시겠지만, 이는 0.106.0 버전 이전의 동작과 매우 비슷하지만, 여러 개선 사항과 코너 케이스에 대한 버그 수정이 포함되어 있습니다. + +/// + +#### 종료 코드를 조기에 실행하는 사용 사례 { #use-cases-with-early-exit-code } + +특정 조건의 일부 사용 사례에서는 응답을 보내기 전에 `yield`가 있는 의존성의 종료 코드를 실행하던 예전 동작이 도움이 될 수 있습니다. + +예를 들어, `yield`가 있는 의존성에서 데이터베이스 세션을 사용해 사용자를 검증만 하고, *경로 처리 함수*에서는 그 데이터베이스 세션을 다시는 사용하지 않으며(의존성에서만 사용), **그리고** 응답을 전송하는 데 오랜 시간이 걸리는 경우를 생각해 봅시다. 예를 들어 데이터를 천천히 보내는 `StreamingResponse`인데, 어떤 이유로든 데이터베이스를 사용하지는 않는 경우입니다. + +이 경우 데이터베이스 세션은 응답 전송이 끝날 때까지 유지되지만, 사용하지 않는다면 굳이 유지할 필요가 없습니다. + +다음과 같이 보일 수 있습니다: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py *} + +다음에서 `Session`을 자동으로 닫는 종료 코드는: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +...응답이 느린 데이터 전송을 마친 뒤에 실행됩니다: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +하지만 `generate_stream()`는 데이터베이스 세션을 사용하지 않으므로, 응답을 전송하는 동안 세션을 열린 채로 유지할 필요는 없습니다. + +SQLModel(또는 SQLAlchemy)을 사용하면서 이런 특정 사용 사례가 있다면, 더 이상 필요하지 않을 때 세션을 명시적으로 닫을 수 있습니다: + +{* ../../docs_src/dependencies/tutorial014_an_py310.py ln[24:28] hl[28] *} + +그러면 세션이 데이터베이스 연결을 해제하여, 다른 요청들이 이를 사용할 수 있게 됩니다. + +`yield`가 있는 의존성에서 조기 종료가 필요한 다른 사용 사례가 있다면, 여러분의 구체적인 사용 사례와 `yield`가 있는 의존성에 대한 조기 종료가 어떤 점에서 이득이 되는지를 포함해 <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">GitHub Discussion Question</a>을 생성해 주세요. + +`yield`가 있는 의존성에서 조기 종료에 대한 설득력 있는 사용 사례가 있다면, 조기 종료를 선택적으로 활성화할 수 있는 새로운 방법을 추가하는 것을 고려하겠습니다. + +### `yield`가 있는 의존성과 `except`, 기술 세부사항 { #dependencies-with-yield-and-except-technical-details } + +FastAPI 0.110.0 이전에는 `yield`가 있는 의존성을 사용한 다음 그 의존성에서 `except`로 예외를 잡고, 예외를 다시 발생시키지 않으면, 예외가 자동으로 어떤 예외 핸들러 또는 내부 서버 오류 핸들러로 raise/forward 되었습니다. + +이는 핸들러 없이 전달된 예외(내부 서버 오류)로 인해 처리되지 않은 메모리 사용이 발생하는 문제를 수정하고, 일반적인 Python 코드의 동작과 일관되게 하기 위해 0.110.0 버전에서 변경되었습니다. + +### 백그라운드 태스크와 `yield`가 있는 의존성, 기술 세부사항 { #background-tasks-and-dependencies-with-yield-technical-details } + +FastAPI 0.106.0 이전에는 `yield` 이후에 예외를 발생시키는 것이 불가능했습니다. `yield`가 있는 의존성의 종료 코드는 응답이 전송된 *후에* 실행되었기 때문에, [Exception Handlers](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}가 이미 실행된 뒤였습니다. + +이는 주로 백그라운드 태스크 안에서 의존성이 "yield"한 동일한 객체들을 사용할 수 있게 하기 위한 설계였습니다. 백그라운드 태스크가 끝난 뒤에 종료 코드가 실행되었기 때문입니다. + +이는 응답이 네트워크를 통해 전달되기를 기다리는 동안 리소스를 점유하지 않기 위한 의도로 FastAPI 0.106.0에서 변경되었습니다. + +/// tip | 팁 + +추가로, 백그라운드 태스크는 보통 별도의 리소스(예: 자체 데이터베이스 연결)를 가지고 따로 처리되어야 하는 독립적인 로직 집합입니다. + +따라서 이 방식이 코드를 더 깔끔하게 만들어줄 가능성이 큽니다. + +/// + +이 동작에 의존하던 경우라면, 이제는 백그라운드 태스크를 위한 리소스를 백그라운드 태스크 내부에서 생성하고, 내부적으로는 `yield`가 있는 의존성의 리소스에 의존하지 않는 데이터만 사용해야 합니다. + +예를 들어, 동일한 데이터베이스 세션을 사용하는 대신, 백그라운드 태스크 내부에서 새 데이터베이스 세션을 생성하고, 이 새 세션을 사용해 데이터베이스에서 객체를 가져오면 됩니다. 그리고 데이터베이스에서 가져온 객체를 백그라운드 태스크 함수의 매개변수로 전달하는 대신, 해당 객체의 ID를 전달한 다음 백그라운드 태스크 함수 내부에서 객체를 다시 가져오면 됩니다. diff --git a/docs/ko/docs/advanced/async-tests.md b/docs/ko/docs/advanced/async-tests.md index 37dfe2979..6c8593681 100644 --- a/docs/ko/docs/advanced/async-tests.md +++ b/docs/ko/docs/advanced/async-tests.md @@ -1,31 +1,26 @@ -# 비동기 테스트 코드 작성 +# 비동기 테스트 { #async-tests } -이전 장에서 `TestClient` 를 이용해 **FastAPI** 어플리케이션 테스트를 작성하는 법을 배우셨을텐데요. -지금까지는 `async` 키워드 사용없이 동기 함수의 테스트 코드를 작성하는 법만 익혔습니다. +제공된 `TestClient`를 사용하여 **FastAPI** 애플리케이션을 테스트하는 방법을 이미 살펴보았습니다. 지금까지는 `async` 함수를 사용하지 않고, 동기 테스트를 작성하는 방법만 보았습니다. -하지만 비동기 함수를 사용하여 테스트 코드를 작성하는 것은 매우 유용할 수 있습니다. -예를 들면 데이터베이스에 비동기로 쿼리하는 경우를 생각해봅시다. -FastAPI 애플리케이션에 요청을 보내고, 비동기 데이터베이스 라이브러리를 사용하여 백엔드가 데이터베이스에 올바르게 데이터를 기록했는지 확인하고 싶을 때가 있을 겁니다. +테스트에서 비동기 함수를 사용할 수 있으면 유용할 수 있습니다. 예를 들어 데이터베이스를 비동기로 쿼리하는 경우를 생각해 보세요. FastAPI 애플리케이션에 요청을 보낸 다음, async 데이터베이스 라이브러리를 사용하면서 백엔드가 데이터베이스에 올바른 데이터를 성공적으로 기록했는지 검증하고 싶을 수 있습니다. -이런 경우의 테스트 코드를 어떻게 비동기로 작성하는지 알아봅시다. +어떻게 동작하게 만들 수 있는지 살펴보겠습니다. -## pytest.mark.anyio +## pytest.mark.anyio { #pytest-mark-anyio } -앞에서 작성한 테스트 함수에서 비동기 함수를 호출하고 싶다면, 테스트 코드도 비동기 함수여야합니다. -AnyIO는 특정 테스트 함수를 비동기 함수로 호출 할 수 있는 깔끔한 플러그인을 제공합니다. +테스트에서 비동기 함수를 호출하려면, 테스트 함수도 비동기여야 합니다. AnyIO는 이를 위한 깔끔한 플러그인을 제공하며, 일부 테스트 함수를 비동기로 호출하도록 지정할 수 있습니다. +## HTTPX { #httpx } -## HTTPX +**FastAPI** 애플리케이션이 `async def` 대신 일반 `def` 함수를 사용하더라도, 내부적으로는 여전히 `async` 애플리케이션입니다. -**FastAPI** 애플리케이션이 `async def` 대신 `def` 키워드로 선언된 함수를 사용하더라도, 내부적으로는 여전히 `비동기` 애플리케이션입니다. +`TestClient`는 표준 pytest를 사용하여, 일반 `def` 테스트 함수 안에서 비동기 FastAPI 애플리케이션을 호출하도록 내부에서 마법 같은 처리를 합니다. 하지만 비동기 함수 안에서 이를 사용하면 그 마법은 더 이상 동작하지 않습니다. 테스트를 비동기로 실행하면, 테스트 함수 안에서 `TestClient`를 더 이상 사용할 수 없습니다. -`TestClient`는 pytest 표준을 사용하여 비동기 FastAPI 애플리케이션을 일반적인 `def` 테스트 함수 내에서 호출할 수 있도록 내부에서 마술을 부립니다. 하지만 이 마술은 비동기 함수 내부에서 사용할 때는 더 이상 작동하지 않습니다. 테스트를 비동기로 실행하면, 더 이상 테스트 함수 내부에서 `TestClient`를 사용할 수 없습니다. +`TestClient`는 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>를 기반으로 하며, 다행히 HTTPX를 직접 사용해 API를 테스트할 수 있습니다. -`TestClient`는 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>를 기반으로 하고 있으며, 다행히 이를 직접 사용하여 API를 테스트할 수 있습니다. +## 예시 { #example } -## 예시 - -간단한 예시를 위해 [더 큰 어플리케이션 만들기](../ko/tutorial/bigger-applications.md){.internal-link target=_blank} 와 [테스트](../ko/tutorial/testing.md){.internal-link target=_blank}:에서 다룬 파일 구조와 비슷한 형태를 확인해봅시다: +간단한 예시로, [더 큰 애플리케이션](../tutorial/bigger-applications.md){.internal-link target=_blank}과 [테스트](../tutorial/testing.md){.internal-link target=_blank}에서 설명한 것과 비슷한 파일 구조를 살펴보겠습니다: ``` . @@ -35,17 +30,17 @@ AnyIO는 특정 테스트 함수를 비동기 함수로 호출 할 수 있는 │   └── test_main.py ``` - `main.py`는 아래와 같아야 합니다: +`main.py` 파일은 다음과 같습니다: -{* ../../docs_src/async_tests/main.py *} +{* ../../docs_src/async_tests/app_a_py39/main.py *} -`test_main.py` 파일은 `main.py`에 대한 테스트가 있을 텐데, 다음과 같을 수 있습니다: +`test_main.py` 파일에는 `main.py`에 대한 테스트가 있으며, 이제 다음과 같이 보일 수 있습니다: -{* ../../docs_src/async_tests/test_main.py *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py *} -## 실행하기 +## 실행하기 { #run-it } -아래의 명령어로 테스트 코드를 실행합니다: +다음과 같이 평소처럼 테스트를 실행할 수 있습니다: <div class="termy"> @@ -57,52 +52,48 @@ $ pytest </div> -## 자세히 보기 +## 자세히 보기 { #in-detail } -`@pytest.mark.anyio` 마커는 pytest에게 이 테스트 함수가 비동기로 호출되어야 함을 알려줍니다: +`@pytest.mark.anyio` 마커는 pytest에게 이 테스트 함수가 비동기로 호출되어야 한다고 알려줍니다: -{* ../../docs_src/async_tests/test_main.py hl[7] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[7] *} /// tip | 팁 -테스트 함수가 이제 `TestClient`를 사용할 때처럼 단순히 `def`가 아니라 `async def`로 작성된 점에 주목해주세요. +`TestClient`를 사용할 때처럼 단순히 `def`가 아니라, 이제 테스트 함수가 `async def`라는 점에 주목하세요. /// -그 다음에 `AsyncClient` 로 앱을 만들고 비동기 요청을 `await` 키워드로 보낼 수 있습니다: +그 다음 앱으로 `AsyncClient`를 만들고, `await`를 사용해 비동기 요청을 보낼 수 있습니다. -{* ../../docs_src/async_tests/test_main.py hl[9:12] *} +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[9:12] *} -위의 코드는: +이는 다음과 동등합니다: ```Python response = client.get('/') ``` -`TestClient` 에 요청을 보내던 것과 동일합니다. +`TestClient`로 요청을 보내기 위해 사용하던 코드입니다. /// tip | 팁 -새로운 `AsyncClient`를 사용할 때 async/await를 사용하고 있다는 점에 주목하세요. 이 요청은 비동기적으로 처리됩니다. +새 `AsyncClient`와 함께 async/await를 사용하고 있다는 점에 주목하세요. 요청은 비동기입니다. /// /// warning | 경고 -만약의 어플리케이션이 Lifespan 이벤트에 의존성을 갖고 있다면 `AsyncClient` 가 이러한 이벤트를 실행시키지 않습니다. -`AsyncClient` 가 테스트를 실행시켰다는 것을 확인하기 위해 -`LifespanManager` from <a href="https://github.com/florimondmanca/asgi-lifespan#usage" class="external-link" target="_blank">florimondmanca/asgi-lifespan</a>.확인해주세요. - +애플리케이션이 lifespan 이벤트에 의존한다면, `AsyncClient`는 이러한 이벤트를 트리거하지 않습니다. 이벤트가 트리거되도록 하려면 <a href="https://github.com/florimondmanca/asgi-lifespan#usage" class="external-link" target="_blank">florimondmanca/asgi-lifespan</a>의 `LifespanManager`를 사용하세요. /// -## 그 외의 비동기 함수 호출 +## 기타 비동기 함수 호출 { #other-asynchronous-function-calls } -테스트 함수가 이제 비동기 함수이므로, FastAPI 애플리케이션에 요청을 보내는 것 외에도 다른 `async` 함수를 호출하고 `await` 키워드를 사용 할 수 있습니다. +테스트 함수가 이제 비동기이므로, 테스트에서 FastAPI 애플리케이션에 요청을 보내는 것 외에도 다른 `async` 함수를 코드의 다른 곳에서 호출하듯이 동일하게 호출하고 (`await`) 사용할 수도 있습니다. /// tip | 팁 -테스트에 비동기 함수 호출을 통합할 때 (예: <a href="https://stackoverflow.com/questions/41584243/runtimeerror-task-attached-to-a-different-loop" class="external-link" target="_blank">MongoDB의 MotorClient</a>를 사용할 때) `RuntimeError: Task attached to a different loop` 오류가 발생한다면, 이벤트 루프가 필요한 객체는 반드시 비동기 함수 내에서만 인스턴스화해야 한다는 점을 주의하세요! -예를 들어 `@app.on_event("startup")` 콜백 내에서 인스턴스화하는 것이 좋습니다. +테스트에 비동기 함수 호출을 통합할 때(예: <a href="https://stackoverflow.com/questions/41584243/runtimeerror-task-attached-to-a-different-loop" class="external-link" target="_blank">MongoDB의 MotorClient</a>를 사용할 때) `RuntimeError: Task attached to a different loop`를 마주친다면, 이벤트 루프가 필요한 객체는 async 함수 안에서만 인스턴스화해야 한다는 점을 기억하세요. 예를 들어 `@app.on_event("startup")` 콜백에서 인스턴스화할 수 있습니다. /// diff --git a/docs/ko/docs/advanced/custom-response.md b/docs/ko/docs/advanced/custom-response.md index 2001956fa..55dc2a4be 100644 --- a/docs/ko/docs/advanced/custom-response.md +++ b/docs/ko/docs/advanced/custom-response.md @@ -1,4 +1,4 @@ -# 사용자 정의 응답 - HTML, Stream, 파일, 기타 +# 사용자 정의 응답 - HTML, Stream, 파일, 기타 { #custom-response-html-stream-file-others } 기본적으로, **FastAPI** 응답을 `JSONResponse`를 사용하여 반환합니다. @@ -6,11 +6,11 @@ 그러나 `Response` (또는 `JSONResponse`와 같은 하위 클래스)를 직접 반환하면, 데이터가 자동으로 변환되지 않으며 (심지어 `response_model`을 선언했더라도), 문서화가 자동으로 생성되지 않습니다(예를 들어, 생성된 OpenAPI의 일부로 HTTP 헤더 `Content-Type`에 특정 "미디어 타입"을 포함하는 경우). -하지만 *경로 작업 데코레이터*에서 `response_class` 매개변수를 사용하여 원하는 `Response`(예: 모든 `Response` 하위 클래스)를 선언할 수도 있습니다. +하지만 *경로 처리 데코레이터*에서 `response_class` 매개변수를 사용하여 원하는 `Response`(예: 모든 `Response` 하위 클래스)를 선언할 수도 있습니다. -*경로 작업 함수*에서 반환하는 내용은 해당 `Response`안에 포함됩니다. +*경로 처리 함수*에서 반환하는 내용은 해당 `Response`안에 포함됩니다. -그리고 만약 그 `Response`가 `JSONResponse`와 `UJSONResponse`의 경우 처럼 JSON 미디어 타입(`application/json`)을 가지고 있다면, *경로 작업 데코레이터*에서 선언한 Pydantic의 `response_model`을 사용해 자동으로 변환(및 필터링) 됩니다. +그리고 만약 그 `Response`가 `JSONResponse`와 `UJSONResponse`의 경우 처럼 JSON 미디어 타입(`application/json`)을 가지고 있다면, *경로 처리 데코레이터*에서 선언한 Pydantic의 `response_model`을 사용해 자동으로 변환(및 필터링) 됩니다. /// note | 참고 @@ -18,11 +18,11 @@ /// -## `ORJSONResponse` 사용하기 +## `ORJSONResponse` 사용하기 { #use-orjsonresponse } -예를 들어, 성능을 극대화하려는 경우, <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">orjson</a>을 설치하여 사용하고 응답을 `ORJSONResponse`로 설정할 수 있습니다. +예를 들어, 성능을 극대화하려는 경우, <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>을 설치하여 사용하고 응답을 `ORJSONResponse`로 설정할 수 있습니다. -사용하고자 하는 `Response` 클래스(하위 클래스)를 임포트한 후, **경로 작업 데코레이터*에서 선언하세요. +사용하고자 하는 `Response` 클래스(하위 클래스)를 임포트한 후, *경로 처리 데코레이터*에서 선언하세요. 대규모 응답의 경우, 딕셔너리를 반환하는 것보다 `Response`를 반환하는 것이 훨씬 빠릅니다. @@ -30,7 +30,7 @@ 하지만 반환하는 내용이 **JSON으로 직렬화 가능**하다고 확신하는 경우, 해당 내용을 응답 클래스에 직접 전달할 수 있으며, FastAPI가 반환 내용을 `jsonable_encoder`를 통해 처리한 뒤 응답 클래스에 전달하는 오버헤드를 피할 수 있습니다. -{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *} /// info | 정보 @@ -48,14 +48,14 @@ /// -## HTML 응답 +## HTML 응답 { #html-response } **FastAPI**에서 HTML 응답을 직접 반환하려면 `HTMLResponse`를 사용하세요. * `HTMLResponse`를 임포트 합니다. -* *경로 작업 데코레이터*의 `response_class` 매개변수로 `HTMLResponse`를 전달합니다. +* *경로 처리 데코레이터*의 `response_class` 매개변수로 `HTMLResponse`를 전달합니다. -{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *} /// info | 정보 @@ -67,17 +67,17 @@ /// -### `Response` 반환하기 +### `Response` 반환하기 { #return-a-response } -[응답을 직접 반환하기](response-directly.md){.internal-link target=_blank}에서 본 것 처럼, *경로 작업*에서 응답을 직접 반환하여 재정의할 수도 있습니다. +[응답을 직접 반환하기](response-directly.md){.internal-link target=_blank}에서 본 것 처럼, *경로 처리*에서 응답을 직접 반환하여 재정의할 수도 있습니다. 위의 예제와 동일하게 `HTMLResponse`를 반환하는 코드는 다음과 같을 수 있습니다: -{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} +{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *} /// warning | 경고 -*경로 작업 함수*에서 직접 반환된 `Response`는 OpenAPI에 문서화되지 않습니다(예를들어, `Content-Type`이 문서화되지 않음) 자동 대화형 문서에서도 표시되지 않습니다. +*경로 처리 함수*에서 직접 반환된 `Response`는 OpenAPI에 문서화되지 않습니다(예를들어, `Content-Type`이 문서화되지 않음) 자동 대화형 문서에서도 표시되지 않습니다. /// @@ -87,27 +87,27 @@ /// -### OpenAPI에 문서화하고 `Response` 재정의 하기 +### OpenAPI에 문서화하고 `Response` 재정의 하기 { #document-in-openapi-and-override-response } 함수 내부에서 응답을 재정의하면서 동시에 OpenAPI에서 "미디어 타입"을 문서화하고 싶다면, `response_class` 매게변수를 사용하면서 `Response` 객체를 반환할 수 있습니다. -이 경우 `response_class`는 OpenAPI *경로 작업*을 문서화하는 데만 사용되고, 실제로는 여러분이 반환한 `Response`가 그대로 사용됩니다. +이 경우 `response_class`는 OpenAPI *경로 처리*를 문서화하는 데만 사용되고, 실제로는 여러분이 반환한 `Response`가 그대로 사용됩니다. -### `HTMLResponse`직접 반환하기 +#### `HTMLResponse`직접 반환하기 { #return-an-htmlresponse-directly } 예를 들어, 다음과 같이 작성할 수 있습니다: -{* ../../docs_src/custom_response/tutorial004.py hl[7,21,23] *} +{* ../../docs_src/custom_response/tutorial004_py39.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_class`에 `HTMLResponse`를 함께 전달했기 때문에, **FastAPI**는 이를 OpenAPI 및 대화형 문서에서 `text/html`로 HTML을 문서화 하는 방법을 알 수 있습니다. <img src="/img/tutorial/custom-response/image01.png"> -## 사용 가능한 응답들 +## 사용 가능한 응답들 { #available-responses } 다음은 사용할 수 있는 몇가지 응답들 입니다. @@ -121,7 +121,7 @@ /// -### `Response` +### `Response` { #response } 기본 `Response` 클래스는 다른 모든 응답 클래스의 부모 클래스 입니다. @@ -134,27 +134,27 @@ * `headers` - 문자열로 이루어진 `dict`. * `media_type` - 미디어 타입을 나타내는 `str` 예: `"text/html"`. -FastAPI (실제로는 Starlette)가 자동으로 `Content-Length` 헤더를 포함시킵니다. 또한 `media_type`에 기반하여 `Content-Type` 헤더를 포함하며, 텍스트 타입의 경우 문자 집합을 추가 합니다. +FastAPI (실제로는 Starlette)가 자동으로 Content-Length 헤더를 포함시킵니다. 또한 `media_type`에 기반하여 Content-Type 헤더를 포함하며, 텍스트 타입의 경우 문자 집합을 추가 합니다. -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} -### `HTMLResponse` +### `HTMLResponse` { #htmlresponse } 텍스트 또는 바이트를 받아 HTML 응답을 반환합니다. 위에서 설명한 내용과 같습니다. -### `PlainTextResponse` +### `PlainTextResponse` { #plaintextresponse } 텍스트 또는 바이트를 받아 일반 텍스트 응답을 반환합니다. -{* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial005_py39.py hl[2,7,9] *} -### `JSONResponse` +### `JSONResponse` { #jsonresponse } 데이터를 받아 `application/json`으로 인코딩된 응답을 반환합니다. 이는 위에서 설명했듯이 **FastAPI**에서 기본적으로 사용되는 응답 형식입니다. -### `ORJSONResponse` +### `ORJSONResponse` { #orjsonresponse } <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>을 사용하여 빠른 JSON 응답을 제공하는 대안입니다. 위에서 설명한 내용과 같습니다. @@ -164,13 +164,13 @@ FastAPI (실제로는 Starlette)가 자동으로 `Content-Length` 헤더를 포 /// -### `UJSONResponse` +### `UJSONResponse` { #ujsonresponse } <a href="https://github.com/ultrajson/ultrajson" class="external-link" target="_blank">`ujson`</a>을 사용한 또 다른 JSON 응답 형식입니다. /// info | 정보 -이 응답을 사용하려면 `ujson`을 설치해야합니다. 예: 'pip install ujson`. +이 응답을 사용하려면 `ujson`을 설치해야합니다. 예: `pip install ujson`. /// @@ -180,7 +180,7 @@ FastAPI (실제로는 Starlette)가 자동으로 `Content-Length` 헤더를 포 /// -{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *} /// tip | 팁 @@ -188,22 +188,22 @@ FastAPI (실제로는 Starlette)가 자동으로 `Content-Length` 헤더를 포 /// -### `RedirectResponse` +### `RedirectResponse` { #redirectresponse } HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 307(임시 리디렉션)으로 설정됩니다. `RedirectResponse`를 직접 반환할 수 있습니다. -{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} +{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *} --- 또는 `response_class` 매개변수에서 사용할 수도 있습니다: -{* ../../docs_src/custom_response/tutorial006b.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *} -이 경우, *경로 작업* 함수에서 URL을 직접 반환할 수 있습니다. +이 경우, *경로 처리* 함수에서 URL을 직접 반환할 수 있습니다. 이 경우, 사용되는 `status_code`는 `RedirectResponse`의 기본값인 `307` 입니다. @@ -211,23 +211,23 @@ HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 30 `status_code` 매개변수를 `response_class` 매개변수와 함께 사용할 수도 있습니다: -{* ../../docs_src/custom_response/tutorial006c.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *} -### `StreamingResponse` +### `StreamingResponse` { #streamingresponse } 비동기 제너레이터 또는 일반 제너레이터/이터레이터를 받아 응답 본문을 스트리밍 합니다. -{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} +{* ../../docs_src/custom_response/tutorial007_py39.py hl[2,14] *} -#### 파일과 같은 객체를 사용한 `StreamingResponse` +#### 파일과 같은 객체를 사용한 `StreamingResponse` { #using-streamingresponse-with-file-like-objects } -파일과 같은 객체(예: `open()`으로 반환된 객체)가 있는 경우, 해당 파일과 같은 객체를 반복(iterate)하는 제너레이터 함수를 만들 수 있습니다. +<a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> 객체(예: `open()`으로 반환된 객체)가 있는 경우, 해당 file-like 객체를 반복(iterate)하는 제너레이터 함수를 만들 수 있습니다. 이 방식으로, 파일 전체를 메모리에 먼저 읽어들일 필요 없이, 제너레이터 함수를 `StreamingResponse`에 전달하여 반환할 수 있습니다. 이 방식은 클라우드 스토리지, 비디오 처리 등의 다양한 라이브러리와 함께 사용할 수 있습니다. -{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *} +{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *} 1. 이것이 제너레이터 함수입니다. `yield` 문을 포함하고 있으므로 "제너레이터 함수"입니다. 2. `with` 블록을 사용함으로써, 제너레이터 함수가 완료된 후 파일과 같은 객체가 닫히도록 합니다. 즉, 응답 전송이 끝난 후 닫힙니다. @@ -235,15 +235,15 @@ HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 30 이렇게 하면 "생성(generating)" 작업을 내부적으로 다른 무언가에 위임하는 제너레이터 함수가 됩니다. - 이 방식을 사용하면 `with` 블록 안에서 파일을 열 수 있어, 작업이 완료된 후 파일과 같은 객체가 닫히는 것을 보장할 수 있습니다. + 이 방식을 사용하면 `with` 블록 안에서 파일을 열 수 있어, 작업이 완료된 후 파일과 같은 객체가 닫히는 것을 보장할 수 있습니다. /// tip | 팁 -여기서 표준 `open()`을 사용하고 있기 때문에 `async`와 `await`를 지원하지 않습니다. 따라서 경로 작업은 일반 `def`로 선언합니다. +여기서 표준 `open()`을 사용하고 있기 때문에 `async`와 `await`를 지원하지 않습니다. 따라서 경로 처리는 일반 `def`로 선언합니다. /// -### `FileResponse` +### `FileResponse` { #fileresponse } 파일을 비동기로 스트리밍하여 응답합니다. @@ -256,25 +256,25 @@ HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 30 파일 응답에는 적절한 `Content-Length`, `Last-Modified`, 및 `ETag` 헤더가 포함됩니다. -{* ../../docs_src/custom_response/tutorial009.py hl[2,10] *} +{* ../../docs_src/custom_response/tutorial009_py39.py hl[2,10] *} 또한 `response_class` 매개변수를 사용할 수도 있습니다: -{* ../../docs_src/custom_response/tutorial009b.py hl[2,8,10] *} +{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *} -이 경우, 경로 작업 함수에서 파일 경로를 직접 반환할 수 있습니다. +이 경우, 경로 처리 함수에서 파일 경로를 직접 반환할 수 있습니다. -## 사용자 정의 응답 클래스 +## 사용자 정의 응답 클래스 { #custom-response-class } `Response`를 상속받아 사용자 정의 응답 클래스를 생성하고 사용할 수 있습니다. -예를 들어, 포함된 `ORJSONResponse` 클래스에서 사용되지 않는 설정으로 <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">orjson</a>을 사용하고 싶다고 가정해봅시다. +예를 들어, 포함된 `ORJSONResponse` 클래스에서 사용되지 않는 설정으로 <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>을 사용하고 싶다고 가정해봅시다. 만약 들여쓰기 및 포맷된 JSON을 반환하고 싶다면, `orjson.OPT_INDENT_2` 옵션을 사용할 수 있습니다. `CustomORJSONResponse`를 생성할 수 있습니다. 여기서 핵심은 `Response.render(content)` 메서드를 생성하여 내용을 `bytes`로 반환하는 것입니다: -{* ../../docs_src/custom_response/tutorial009c.py hl[9:14,17] *} +{* ../../docs_src/custom_response/tutorial009c_py39.py hl[9:14,17] *} 이제 다음 대신: @@ -282,7 +282,7 @@ HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 30 {"message": "Hello World"} ``` -이 응답은 이렇게 반환됩니다: +...이 응답은 이렇게 반환됩니다: ```json { @@ -292,22 +292,22 @@ HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 30 물론 JSON 포맷팅보다 더 유용하게 활용할 방법을 찾을 수 있을 것입니다. 😉 -## 기본 응답 클래스 +## 기본 응답 클래스 { #default-response-class } **FastAPI** 클래스 객체 또는 `APIRouter`를 생성할 때 기본적으로 사용할 응답 클래스를 지정할 수 있습니다. 이를 정의하는 매개변수는 `default_response_class`입니다. -아래 예제에서 **FastAPI**는 모든 경로 작업에서 기본적으로 `JSONResponse` 대신 `ORJSONResponse`를 사용합니다. +아래 예제에서 **FastAPI**는 모든 *경로 처리*에서 기본적으로 `JSONResponse` 대신 `ORJSONResponse`를 사용합니다. -{* ../../docs_src/custom_response/tutorial010.py hl[2,4] *} +{* ../../docs_src/custom_response/tutorial010_py39.py hl[2,4] *} /// tip | 팁 -여전히 이전처럼 *경로 작업*에서 `response_class`를 재정의할 수 있습니다. +여전히 이전처럼 *경로 처리*에서 `response_class`를 재정의할 수 있습니다. /// -## 추가 문서화 +## 추가 문서화 { #additional-documentation } OpenAPI에서 `responses`를 사용하여 미디어 타입 및 기타 세부 정보를 선언할 수도 있습니다: [OpenAPI에서 추가 응답](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/ko/docs/advanced/events.md b/docs/ko/docs/advanced/events.md index 4318ada54..35223eaf3 100644 --- a/docs/ko/docs/advanced/events.md +++ b/docs/ko/docs/advanced/events.md @@ -1,67 +1,66 @@ -# Lifespan 이벤트 +# Lifespan 이벤트 { #lifespan-events } -애플리케이션 **시작 전**에 실행되어야 하는 로직(코드)을 정의할 수 있습니다. 이는 이 코드가 **한 번**만 실행되며, **애플리케이션이 요청을 받기 시작하기 전**에 실행된다는 의미입니다. +애플리케이션이 **시작**하기 전에 실행되어야 하는 로직(코드)을 정의할 수 있습니다. 이는 이 코드가 **한 번**만 실행되며, 애플리케이션이 **요청을 받기 시작하기 전**에 실행된다는 의미입니다. -마찬가지로, 애플리케이션이 **종료될 때** 실행되어야 하는 로직(코드)을 정의할 수 있습니다. 이 경우, 이 코드는 **한 번**만 실행되며, **여러 요청을 처리한 후**에 실행됩니다. +마찬가지로, 애플리케이션이 **종료**될 때 실행되어야 하는 로직(코드)을 정의할 수 있습니다. 이 경우, 이 코드는 **한 번**만 실행되며, **여러 요청을 처리한 후**에 실행됩니다. -이 코드가 애플리케이션이 **요청을 받기 시작하기 전에** 실행되고, 요청 처리가 끝난 후 **종료 직전에** 실행되기 때문에 전체 애플리케이션의 **수명(Lifespan)**을 다룹니다. (잠시 후 "수명"이라는 단어가 중요해집니다 😉) +이 코드는 애플리케이션이 요청을 받기 **시작**하기 전에 실행되고, 요청 처리를 **끝낸 직후**에 실행되기 때문에 전체 애플리케이션의 **수명(lifespan)**을 다룹니다(잠시 후 "lifespan"이라는 단어가 중요해집니다 😉). -이 방법은 전체 애플리케이션에서 사용해야 하는 **자원**을 설정하거나 요청 간에 **공유되는** 자원을 설정하고, 또는 그 후에 **정리**하는 데 매우 유용할 수 있습니다. 예를 들어, 데이터베이스 연결 풀 또는 공유되는 머신러닝 모델을 로드하는 경우입니다. +이는 전체 앱에서 사용해야 하는 **자원**을 설정하고, 요청 간에 **공유되는** 자원을 설정하고, 그리고/또는 이후에 **정리**하는 데 매우 유용할 수 있습니다. 예를 들어, 데이터베이스 연결 풀 또는 공유 머신러닝 모델을 로드하는 경우입니다. +## 사용 사례 { #use-case } -## 사용 사례 +먼저 **사용 사례** 예시로 시작한 다음, 이를 어떻게 해결할지 살펴보겠습니다. -먼저 **사용 사례**를 예로 들어보고, 이를 어떻게 해결할 수 있는지 살펴보겠습니다. +요청을 처리하는 데 사용하고 싶은 **머신러닝 모델**이 있다고 상상해 봅시다. 🤖 -우리가 요청을 처리하기 위해 사용하고 싶은 **머신러닝 모델**이 있다고 상상해 봅시다. 🤖 +동일한 모델이 요청 간에 공유되므로, 요청마다 모델이 하나씩 있거나 사용자마다 하나씩 있는 등의 방식이 아닙니다. -이 모델들은 요청 간에 공유되므로, 요청마다 모델이 하나씩 있는 것이 아니라, 여러 요청에서 동일한 모델을 사용합니다. +모델을 로드하는 데 **상당한 시간이 걸린다고 상상해 봅시다**, 왜냐하면 모델이 **디스크에서 많은 데이터를 읽어야** 하기 때문입니다. 그래서 모든 요청마다 이를 수행하고 싶지는 않습니다. -모델을 로드하는 데 **상당한 시간이 걸린다고 상상해 봅시다**, 왜냐하면 모델이 **디스크에서 많은 데이터를 읽어야** 하기 때문입니다. 따라서 모든 요청에 대해 모델을 매번 로드하고 싶지 않습니다. +모듈/파일의 최상위에서 로드할 수도 있지만, 그러면 단순한 자동화된 테스트를 실행하는 경우에도 **모델을 로드**하게 되고, 테스트가 코드의 독립적인 부분을 실행하기 전에 모델이 로드될 때까지 기다려야 하므로 **느려집니다**. -모듈/파일의 최상위에서 모델을 로드할 수도 있지만, 그러면 **모델을 로드하는데** 시간이 걸리기 때문에, 단순한 자동화된 테스트를 실행할 때도 모델이 로드될 때까지 기다려야 해서 **테스트 속도가 느려집니다**. +이것이 우리가 해결할 문제입니다. 요청을 처리하기 전에 모델을 로드하되, 코드가 로드되는 동안이 아니라 애플리케이션이 요청을 받기 시작하기 직전에만 로드하겠습니다. -이 문제를 해결하려고 하는 것입니다. 요청을 처리하기 전에 모델을 로드하되, 애플리케이션이 요청을 받기 시작하기 직전에만 로드하고, 코드가 로드되는 동안은 로드하지 않도록 하겠습니다. +## Lifespan { #lifespan } -## Lifespan +`FastAPI` 앱의 `lifespan` 매개변수와 "컨텍스트 매니저"를 사용하여 *시작*과 *종료* 로직을 정의할 수 있습니다(컨텍스트 매니저가 무엇인지 잠시 후에 보여드리겠습니다). -`FastAPI` 애플리케이션의 `lifespan` 매개변수와 "컨텍스트 매니저"를 사용하여 *시작*과 *종료* 로직을 정의할 수 있습니다. (컨텍스트 매니저가 무엇인지 잠시 후에 설명드리겠습니다.) +예제로 시작한 다음 자세히 살펴보겠습니다. -예제를 통해 시작하고, 그 후에 자세히 살펴보겠습니다. +`yield`를 사용해 비동기 함수 `lifespan()`을 다음과 같이 생성합니다: -우리는 `yield`를 사용하여 비동기 함수 `lifespan()`을 다음과 같이 생성합니다: +{* ../../docs_src/events/tutorial003_py39.py hl[16,19] *} -{* ../../docs_src/events/tutorial003.py hl[16,19] *} +여기서는 `yield` 이전에 (가짜) 모델 함수를 머신러닝 모델이 들어 있는 딕셔너리에 넣어 모델을 로드하는 비용이 큰 *시작* 작업을 시뮬레이션합니다. 이 코드는 애플리케이션이 **요청을 받기 시작하기 전**, *시작* 동안에 실행됩니다. -여기서 우리는 모델을 로드하는 비싼 *시작* 작업을 시뮬레이션하고 있습니다. `yield` 앞에서 (가짜) 모델 함수를 머신러닝 모델이 담긴 딕셔너리에 넣습니다. 이 코드는 **애플리케이션이 요청을 받기 시작하기 전**, *시작* 동안에 실행됩니다. - -그리고 `yield` 직후에는 모델을 언로드합니다. 이 코드는 **애플리케이션이 요청 처리 완료 후**, *종료* 직전에 실행됩니다. 예를 들어, 메모리나 GPU와 같은 자원을 해제하는 작업을 할 수 있습니다. +그리고 `yield` 직후에는 모델을 언로드합니다. 이 코드는 애플리케이션이 **요청 처리를 마친 후**, *종료* 직전에 실행됩니다. 예를 들어 메모리나 GPU 같은 자원을 해제할 수 있습니다. /// tip | 팁 -`shutdown`은 애플리케이션을 **종료**할 때 발생합니다. +`shutdown`은 애플리케이션을 **중지**할 때 발생합니다. -새로운 버전을 시작해야 하거나, 그냥 실행을 멈추고 싶을 수도 있습니다. 🤷 +새 버전을 시작해야 할 수도 있고, 그냥 실행하는 게 지겨워졌을 수도 있습니다. 🤷 /// -### Lifespan 함수 +### Lifespan 함수 { #lifespan-function } -먼저 주목할 점은, `yield`를 사용하여 비동기 함수(async function)를 정의하고 있다는 것입니다. 이는 `yield`를 사용한 의존성과 매우 유사합니다. +먼저 주목할 점은 `yield`를 사용하여 비동기 함수를 정의하고 있다는 것입니다. 이는 `yield`를 사용하는 의존성과 매우 유사합니다. -{* ../../docs_src/events/tutorial003.py hl[14:19] *} +{* ../../docs_src/events/tutorial003_py39.py hl[14:19] *} 함수의 첫 번째 부분, 즉 `yield` 이전의 코드는 애플리케이션이 시작되기 **전에** 실행됩니다. -그리고 `yield` 이후의 부분은 애플리케이션이 완료된 후 **나중에** 실행됩니다. +그리고 `yield` 이후의 부분은 애플리케이션이 종료된 **후에** 실행됩니다. -### 비동기 컨텍스트 매니저 +### 비동기 컨텍스트 매니저 { #async-context-manager } -함수를 확인해보면, `@asynccontextmanager`로 장식되어 있습니다. +확인해 보면, 함수는 `@asynccontextmanager`로 데코레이션되어 있습니다. -이것은 함수를 "**비동기 컨텍스트 매니저**"라고 불리는 것으로 변환시킵니다. +이는 함수를 "**비동기 컨텍스트 매니저**"라고 불리는 것으로 변환합니다. -{* ../../docs_src/events/tutorial003.py hl[1,13] *} +{* ../../docs_src/events/tutorial003_py39.py hl[1,13] *} 파이썬에서 **컨텍스트 매니저**는 `with` 문에서 사용할 수 있는 것입니다. 예를 들어, `open()`은 컨텍스트 매니저로 사용할 수 있습니다: @@ -69,97 +68,98 @@ with open("file.txt") as file: file.read() ``` -최근 버전의 파이썬에서는 **비동기 컨텍스트 매니저**도 있습니다. 이를 `async with`와 함께 사용합니다: + +최근 버전의 파이썬에는 **비동기 컨텍스트 매니저**도 있습니다. 이를 `async with`와 함께 사용합니다: ```Python async with lifespan(app): await do_stuff() ``` -컨텍스트 매니저나 위와 같은 비동기 컨텍스트 매니저를 만들면, `with` 블록에 들어가기 전에 `yield` 이전의 코드가 실행되고, `with` 블록을 벗어난 후에는 `yield` 이후의 코드가 실행됩니다. +위와 같은 컨텍스트 매니저 또는 비동기 컨텍스트 매니저를 만들면, `with` 블록에 들어가기 전에 `yield` 이전의 코드를 실행하고, `with` 블록을 벗어난 후에는 `yield` 이후의 코드를 실행합니다. -위의 코드 예제에서는 직접 사용하지 않고, FastAPI에 전달하여 사용하도록 합니다. +위의 코드 예제에서는 직접 사용하지 않고, FastAPI에 전달하여 FastAPI가 이를 사용하도록 합니다. -`FastAPI` 애플리케이션의 `lifespan` 매개변수는 **비동기 컨텍스트 매니저**를 받기 때문에, 새로운 `lifespan` 비동기 컨텍스트 매니저를 FastAPI에 전달할 수 있습니다. +`FastAPI` 앱의 `lifespan` 매개변수는 **비동기 컨텍스트 매니저**를 받으므로, 새 `lifespan` 비동기 컨텍스트 매니저를 전달할 수 있습니다. -{* ../../docs_src/events/tutorial003.py hl[22] *} +{* ../../docs_src/events/tutorial003_py39.py hl[22] *} -## 대체 이벤트 (사용 중단) +## 대체 이벤트(사용 중단) { #alternative-events-deprecated } /// warning | 경고 -*시작*과 *종료*를 처리하는 권장 방법은 위에서 설명한 대로 `FastAPI` 애플리케이션의 `lifespan` 매개변수를 사용하는 것입니다. `lifespan` 매개변수를 제공하면 `startup`과 `shutdown` 이벤트 핸들러는 더 이상 호출되지 않습니다. `lifespan`을 사용할지, 모든 이벤트를 사용할지 선택해야 하며 둘 다 사용할 수는 없습니다. +*시작*과 *종료*를 처리하는 권장 방법은 위에서 설명한 대로 `FastAPI` 앱의 `lifespan` 매개변수를 사용하는 것입니다. `lifespan` 매개변수를 제공하면 `startup`과 `shutdown` 이벤트 핸들러는 더 이상 호출되지 않습니다. `lifespan`만 쓰거나 이벤트만 쓰거나 둘 중 하나이지, 둘 다는 아닙니다. -이 부분은 건너뛰셔도 좋습니다. +이 부분은 아마 건너뛰셔도 됩니다. /// *시작*과 *종료* 동안 실행될 이 로직을 정의하는 대체 방법이 있습니다. -애플리케이션이 시작되기 전에 또는 종료될 때 실행해야 하는 이벤트 핸들러(함수)를 정의할 수 있습니다. +애플리케이션이 시작되기 전에 또는 애플리케이션이 종료될 때 실행되어야 하는 이벤트 핸들러(함수)를 정의할 수 있습니다. 이 함수들은 `async def` 또는 일반 `def`로 선언할 수 있습니다. -### `startup` 이벤트 +### `startup` 이벤트 { #startup-event } 애플리케이션이 시작되기 전에 실행되어야 하는 함수를 추가하려면, `"startup"` 이벤트로 선언합니다: -{* ../../docs_src/events/tutorial001.py hl[8] *} +{* ../../docs_src/events/tutorial001_py39.py hl[8] *} -이 경우, `startup` 이벤트 핸들러 함수는 "database"라는 항목(단지 `dict`)을 일부 값으로 초기화합니다. +이 경우, `startup` 이벤트 핸들러 함수는 "database"(그냥 `dict`) 항목을 일부 값으로 초기화합니다. 여러 개의 이벤트 핸들러 함수를 추가할 수 있습니다. -애플리케이션은 모든 `startup` 이벤트 핸들러가 완료될 때까지 요청을 받기 시작하지 않습니다. +그리고 모든 `startup` 이벤트 핸들러가 완료될 때까지 애플리케이션은 요청을 받기 시작하지 않습니다. -### `shutdown` 이벤트 +### `shutdown` 이벤트 { #shutdown-event } 애플리케이션이 종료될 때 실행되어야 하는 함수를 추가하려면, `"shutdown"` 이벤트로 선언합니다: -{* ../../docs_src/events/tutorial002.py hl[6] *} +{* ../../docs_src/events/tutorial002_py39.py hl[6] *} -여기서, `shutdown` 이벤트 핸들러 함수는 `"Application shutdown"`이라는 텍스트를 `log.txt` 파일에 기록합니다. +여기서 `shutdown` 이벤트 핸들러 함수는 텍스트 한 줄 `"Application shutdown"`을 `log.txt` 파일에 기록합니다. /// info | 정보 -`open()` 함수에서 `mode="a"`는 "추가"를 의미하므로, 파일에 있는 기존 내용은 덮어쓰지 않고 새로운 줄이 추가됩니다. +`open()` 함수에서 `mode="a"`는 "append"(추가)를 의미하므로, 기존 내용을 덮어쓰지 않고 파일에 있던 내용 뒤에 줄이 추가됩니다. /// /// tip | 팁 -이 경우, 우리는 표준 파이썬 `open()` 함수를 사용하여 파일과 상호작용하고 있습니다. +이 경우에는 파일과 상호작용하는 표준 파이썬 `open()` 함수를 사용하고 있습니다. -따라서 I/O(입출력) 작업이 포함되어 있어 디스크에 기록되는 것을 "기다리는" 과정이 필요합니다. +따라서 I/O(input/output)가 포함되어 있어 디스크에 기록되는 것을 "기다리는" 과정이 필요합니다. 하지만 `open()`은 `async`와 `await`를 사용하지 않습니다. -그래서 우리는 이벤트 핸들러 함수를 `async def` 대신 일반 `def`로 선언합니다. +그래서 이벤트 핸들러 함수는 `async def` 대신 표준 `def`로 선언합니다. /// -### `startup`과 `shutdown`을 함께 사용 +### `startup`과 `shutdown`을 함께 { #startup-and-shutdown-together } -*시작*과 *종료* 로직이 연결될 가능성이 높습니다. 예를 들어, 무언가를 시작한 후 끝내거나, 자원을 획득한 후 해제하는 등의 작업을 할 수 있습니다. +*시작*과 *종료* 로직은 연결되어 있을 가능성이 높습니다. 무언가를 시작했다가 끝내거나, 자원을 획득했다가 해제하는 등의 작업이 필요할 수 있습니다. -이러한 작업을 별도의 함수로 처리하면 서로 로직이나 변수를 공유하지 않기 때문에 더 어려워집니다. 값들을 전역 변수에 저장하거나 비슷한 트릭을 사용해야 할 수 있습니다. +로직이나 변수를 함께 공유하지 않는 분리된 함수에서 이를 처리하면, 전역 변수에 값을 저장하거나 비슷한 트릭이 필요해져 더 어렵습니다. -그렇기 때문에 위에서 설명한 대로 `lifespan`을 사용하는 것이 권장됩니다. +그 때문에, 이제는 위에서 설명한 대로 `lifespan`을 사용하는 것이 권장됩니다. -## 기술적 세부사항 +## 기술적 세부사항 { #technical-details } 호기심 많은 분들을 위한 기술적인 세부사항입니다. 🤓 -ASGI 기술 사양에 따르면, 이는 <a href="https://asgi.readthedocs.io/en/latest/specs/lifespan.html" class="external-link" target="_blank">Lifespan Protocol</a>의 일부이며, `startup`과 `shutdown`이라는 이벤트를 정의합니다. +내부적으로 ASGI 기술 사양에서는 이것이 <a href="https://asgi.readthedocs.io/en/latest/specs/lifespan.html" class="external-link" target="_blank">Lifespan Protocol</a>의 일부이며, `startup`과 `shutdown`이라는 이벤트를 정의합니다. /// info | 정보 -Starlette의 `lifespan` 핸들러에 대해 더 읽고 싶다면 <a href="https://www.starlette.dev/lifespan/" class="external-link" target="_blank">Starlette의 Lifespan 문서</a>에서 확인할 수 있습니다. +Starlette `lifespan` 핸들러에 대해서는 <a href="https://www.starlette.dev/lifespan/" class="external-link" target="_blank">Starlette의 Lifespan 문서</a>에서 더 읽어볼 수 있습니다. -이 문서에는 코드의 다른 영역에서 사용할 수 있는 lifespan 상태를 처리하는 방법도 포함되어 있습니다. +또한 코드의 다른 영역에서 사용할 수 있는 lifespan 상태를 처리하는 방법도 포함되어 있습니다. /// -## 서브 애플리케이션 +## 서브 애플리케이션 { #sub-applications } -🚨 이 lifespan 이벤트(`startup`과 `shutdown`)는 메인 애플리케이션에 대해서만 실행되며, [서브 애플리케이션 - Mounts](sub-applications.md){.internal-link target=_blank}에는 실행되지 않음을 유의하세요. +🚨 이 lifespan 이벤트(startup 및 shutdown)는 메인 애플리케이션에 대해서만 실행되며, [서브 애플리케이션 - Mounts](sub-applications.md){.internal-link target=_blank}에는 실행되지 않음을 유의하세요. diff --git a/docs/ko/docs/advanced/index.md b/docs/ko/docs/advanced/index.md index 31704727c..78ef5ffec 100644 --- a/docs/ko/docs/advanced/index.md +++ b/docs/ko/docs/advanced/index.md @@ -1,6 +1,6 @@ -# 심화 사용자 안내서 - 도입부 +# 심화 사용자 안내서 - 도입부 { #advanced-user-guide } -## 추가 기능 +## 추가 기능 { #additional-features } 메인 [자습서 - 사용자 안내서](../tutorial/index.md){.internal-link target=_blank}는 여러분이 **FastAPI**의 모든 주요 기능을 둘러보시기에 충분할 것입니다. @@ -14,14 +14,8 @@ /// -## 자습서를 먼저 읽으십시오 +## 자습서를 먼저 읽으십시오 { #read-the-tutorial-first } 여러분은 메인 [자습서 - 사용자 안내서](../tutorial/index.md){.internal-link target=_blank}의 지식으로 **FastAPI**의 대부분의 기능을 사용하실 수 있습니다. 이어지는 장들은 여러분이 메인 자습서 - 사용자 안내서를 이미 읽으셨으며 주요 아이디어를 알고 계신다고 가정합니다. - -## TestDriven.io 강좌 - -여러분이 문서의 이 부분을 보완하시기 위해 심화-기초 강좌 수강을 희망하신다면 다음을 참고 하시기를 바랍니다: **TestDriven.io**의 <a href="https://testdriven.io/courses/tdd-fastapi/" class="external-link" target="_blank">FastAPI와 Docker를 사용한 테스트 주도 개발</a>. - -그들은 현재 전체 수익의 10퍼센트를 **FastAPI** 개발에 기부하고 있습니다. 🎉 😄 diff --git a/docs/ko/docs/advanced/response-change-status-code.md b/docs/ko/docs/advanced/response-change-status-code.md index 1ba9aa3cc..4dfadde9d 100644 --- a/docs/ko/docs/advanced/response-change-status-code.md +++ b/docs/ko/docs/advanced/response-change-status-code.md @@ -1,31 +1,31 @@ -# 응답 - 상태 코드 변경 +# 응답 - 상태 코드 변경 { #response-change-status-code } 기본 [응답 상태 코드 설정](../tutorial/response-status-code.md){.internal-link target=_blank}이 가능하다는 걸 이미 알고 계실 겁니다. 하지만 경우에 따라 기본 설정과 다른 상태 코드를 반환해야 할 때가 있습니다. -## 사용 예 +## 사용 예 { #use-case } 예를 들어 기본적으로 HTTP 상태 코드 "OK" `200`을 반환하고 싶다고 가정해 봅시다. 하지만 데이터가 존재하지 않으면 이를 새로 생성하고, HTTP 상태 코드 "CREATED" `201`을 반환하고자 할 때가 있을 수 있습니다. -이때도 여전히 `response_model`을 사용하여 반환하는 데이터를 필터링하고 변환하고 싶을 수 있습니다. +하지만 여전히 `response_model`을 사용하여 반환하는 데이터를 필터링하고 변환할 수 있기를 원합니다. 이런 경우에는 `Response` 파라미터를 사용할 수 있습니다. -## `Response` 파라미터 사용하기 +## `Response` 파라미터 사용하기 { #use-a-response-parameter } -*경로 작동 함수*에 `Response` 타입의 파라미터를 선언할 수 있습니다. (쿠키와 헤더에 대해 선언하는 것과 유사하게) +*경로 처리 함수*에 `Response` 타입의 파라미터를 선언할 수 있습니다. (쿠키와 헤더에 대해 선언하는 것과 유사하게) 그리고 이 *임시* 응답 객체에서 `status_code`를 설정할 수 있습니다. -{* ../../docs_src/response_change_status_code/tutorial001.py hl[1,9,12] *} +{* ../../docs_src/response_change_status_code/tutorial001_py39.py hl[1,9,12] *} -그리고 평소처럼 원하는 객체(`dict`, 데이터베이스 모델 등)를 반환할 수 있습니다. +그리고 평소처럼 필요한 어떤 객체든 반환할 수 있습니다(`dict`, 데이터베이스 모델 등). `response_model`을 선언했다면 반환된 객체는 여전히 필터링되고 변환됩니다. -**FastAPI**는 이 *임시* 응답 객체에서 상태 코드(쿠키와 헤더 포함)를 추출하여, `response_model`로 필터링된 반환 값을 최종 응답에 넣습니다. +**FastAPI**는 이 *임시* 응답 객체에서 상태 코드(쿠키와 헤더 포함)를 추출하여, `response_model`로 필터링된 반환 값을 포함하는 최종 응답에 넣습니다. 또한, 의존성에서도 `Response` 파라미터를 선언하고 그 안에서 상태 코드를 설정할 수 있습니다. 단, 마지막으로 설정된 상태 코드가 우선 적용된다는 점을 유의하세요. diff --git a/docs/ko/docs/advanced/response-cookies.md b/docs/ko/docs/advanced/response-cookies.md index 50da713fe..eef74276f 100644 --- a/docs/ko/docs/advanced/response-cookies.md +++ b/docs/ko/docs/advanced/response-cookies.md @@ -1,49 +1,51 @@ -# 응답 쿠키 +# 응답 쿠키 { #response-cookies } -## `Response` 매개변수 사용하기 +## `Response` 매개변수 사용하기 { #use-a-response-parameter } -*경로 작동 함수*에서 `Response` 타입의 매개변수를 선언할 수 있습니다. +*경로 처리 함수*에서 `Response` 타입의 매개변수를 선언할 수 있습니다. 그런 다음 해당 *임시* 응답 객체에서 쿠키를 설정할 수 있습니다. -{* ../../docs_src/response_cookies/tutorial002.py hl[1,8:9] *} +{* ../../docs_src/response_cookies/tutorial002_py39.py hl[1, 8:9] *} -그런 다음 필요한 객체(`dict`, 데이터베이스 모델 등)를 반환할 수 있습니다. +그런 다음 일반적으로 하듯이 필요한 어떤 객체든 반환할 수 있습니다(`dict`, 데이터베이스 모델 등). 그리고 `response_model`을 선언했다면 반환한 객체를 거르고 변환하는 데 여전히 사용됩니다. -**FastAPI**는 그 *임시* 응답에서 쿠키(또한 헤더 및 상태 코드)를 추출하고, 반환된 값이 포함된 최종 응답에 이를 넣습니다. 이 값은 `response_model`로 걸러지게 됩니다. +**FastAPI**는 그 *임시* 응답에서 쿠키(또한 헤더 및 상태 코드)를 추출하고, `response_model`로 필터링된 반환 값이 포함된 최종 응답에 이를 넣습니다. 또한 의존관계에서 `Response` 매개변수를 선언하고, 해당 의존성에서 쿠키(및 헤더)를 설정할 수도 있습니다. -## `Response`를 직접 반환하기 +## `Response`를 직접 반환하기 { #return-a-response-directly } 코드에서 `Response`를 직접 반환할 때도 쿠키를 생성할 수 있습니다. 이를 위해 [Response를 직접 반환하기](response-directly.md){.internal-link target=_blank}에서 설명한 대로 응답을 생성할 수 있습니다. 그런 다음 쿠키를 설정하고 반환하면 됩니다: -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} -/// tip + +{* ../../docs_src/response_cookies/tutorial001_py39.py hl[10:12] *} + +/// tip | 팁 `Response` 매개변수를 사용하지 않고 응답을 직접 반환하는 경우, FastAPI는 이를 직접 반환한다는 점에 유의하세요. 따라서 데이터가 올바른 유형인지 확인해야 합니다. 예: `JSONResponse`를 반환하는 경우, JSON과 호환되는지 확인하세요. -또한 `response_model`로 걸러져야 할 데이터가 전달되지 않도록 확인하세요. +또한 `response_model`로 필터링되어야 했던 데이터를 전송하지 않도록 하세요. /// -### 추가 정보 +### 추가 정보 { #more-info } -/// note | 기술적 세부사항 +/// note | 기술 세부사항 `from starlette.responses import Response` 또는 `from starlette.responses import JSONResponse`를 사용할 수도 있습니다. -**FastAPI**는 개발자의 편의를 위해 `fastapi.responses`로 동일한 `starlette.responses`를 제공합니다. 그러나 대부분의 응답은 Starlette에서 직접 제공됩니다. +**FastAPI**는 개발자의 편의를 위해 `fastapi.responses`로 동일한 `starlette.responses`를 제공합니다. 하지만 사용 가능한 대부분의 응답은 Starlette에서 직접 제공됩니다. 또한 `Response`는 헤더와 쿠키를 설정하는 데 자주 사용되므로, **FastAPI**는 이를 `fastapi.Response`로도 제공합니다. /// -사용 가능한 모든 매개변수와 옵션은 <a href="https://www.starlette.dev/responses/#set-cookie" class="external-link" target="_blank">Starlette 문서</a>에서 확인할 수 있습니다. +사용 가능한 모든 매개변수와 옵션은 <a href="https://www.starlette.dev/responses/#set-cookie" class="external-link" target="_blank">Starlette의 문서</a>에서 확인할 수 있습니다. diff --git a/docs/ko/docs/advanced/response-directly.md b/docs/ko/docs/advanced/response-directly.md index 08d63c43c..abf06bb18 100644 --- a/docs/ko/docs/advanced/response-directly.md +++ b/docs/ko/docs/advanced/response-directly.md @@ -1,20 +1,20 @@ -# 응답을 직접 반환하기 +# 응답을 직접 반환하기 { #return-a-response-directly } -**FastAPI**에서 *경로 작업(path operation)*을 생성할 때, 일반적으로 `dict`, `list`, Pydantic 모델, 데이터베이스 모델 등의 데이터를 반환할 수 있습니다. +**FastAPI**에서 *경로 처리(path operation)*를 생성할 때, 일반적으로 `dict`, `list`, Pydantic 모델, 데이터베이스 모델 등의 데이터를 반환할 수 있습니다. -기본적으로 **FastAPI**는 [JSON 호환 가능 인코더](../tutorial/encoder.md){.internal-link target=_blank}에 설명된 `jsonable_encoder`를 사용해 해당 반환 값을 자동으로 `JSON`으로 변환합니다. +기본적으로 **FastAPI**는 [JSON 호환 가능 인코더](../tutorial/encoder.md){.internal-link target=_blank}에 설명된 `jsonable_encoder`를 사용해 해당 반환 값을 자동으로 JSON으로 변환합니다. -그런 다음, JSON 호환 데이터(예: `dict`)를 `JSONResponse`에 넣어 사용자의 응답을 전송하는 방식으로 처리됩니다. +그런 다음, 내부적으로는 JSON 호환 데이터(예: `dict`)를 `JSONResponse`에 넣어 클라이언트로 응답을 전송하는 데 사용합니다. -그러나 *경로 작업*에서 `JSONResponse`를 직접 반환할 수도 있습니다. +하지만 *경로 처리*에서 `JSONResponse`를 직접 반환할 수도 있습니다. 예를 들어, 사용자 정의 헤더나 쿠키를 반환해야 하는 경우에 유용할 수 있습니다. -## `Response` 반환하기 +## `Response` 반환하기 { #return-a-response } 사실, `Response` 또는 그 하위 클래스를 반환할 수 있습니다. -/// tip +/// tip | 팁 `JSONResponse` 자체도 `Response`의 하위 클래스입니다. @@ -26,38 +26,40 @@ Pydantic 모델로 데이터 변환을 수행하지 않으며, 내용을 다른 이로 인해 많은 유연성을 얻을 수 있습니다. 어떤 데이터 유형이든 반환할 수 있고, 데이터 선언이나 유효성 검사를 재정의할 수 있습니다. -## `Response`에서 `jsonable_encoder` 사용하기 +## `Response`에서 `jsonable_encoder` 사용하기 { #using-the-jsonable-encoder-in-a-response } -**FastAPI**는 반환하는 `Response`에 아무런 변환을 하지 않으므로, 그 내용이 준비되어 있어야 합니다. +**FastAPI**는 반환하는 `Response`에 아무런 변경도 하지 않으므로, 그 내용이 준비되어 있는지 확인해야 합니다. -예를 들어, Pydantic 모델을 `dict`로 변환해 `JSONResponse`에 넣지 않으면 JSON 호환 유형으로 변환된 데이터 유형(예: `datetime`, `UUID` 등)이 사용되지 않습니다. +예를 들어, Pydantic 모델을 먼저 `dict`로 변환하고 `datetime`, `UUID` 등의 모든 데이터 타입을 JSON 호환 타입으로 변환하지 않으면 Pydantic 모델을 `JSONResponse`에 넣을 수 없습니다. 이러한 경우, 데이터를 응답에 전달하기 전에 `jsonable_encoder`를 사용하여 변환할 수 있습니다: -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} -/// note | 기술적 세부 사항 +/// note | 기술 세부사항 `from starlette.responses import JSONResponse`를 사용할 수도 있습니다. -**FastAPI**는 개발자의 편의를 위해 `starlette.responses`를 `fastapi.responses`로 제공합니다. 그러나 대부분의 가능한 응답은 Starlette에서 직접 제공합니다. +**FastAPI**는 개발자의 편의를 위해 `starlette.responses`를 `fastapi.responses`로 제공합니다. 하지만 대부분의 사용 가능한 응답은 Starlette에서 직접 제공합니다. /// -## 사용자 정의 `Response` 반환하기 -위 예제는 필요한 모든 부분을 보여주지만, 아직 유용하지는 않습니다. 사실 데이터를 직접 반환하면 **FastAPI**가 이를 `JSONResponse`에 넣고 `dict`로 변환하는 등 모든 작업을 자동으로 처리합니다. +## 사용자 정의 `Response` 반환하기 { #returning-a-custom-response } -이제, 사용자 정의 응답을 반환하는 방법을 알아보겠습니다. +위 예제는 필요한 모든 부분을 보여주지만, 아직은 그다지 유용하지 않습니다. `item`을 그냥 직접 반환했어도 **FastAPI**가 기본으로 이를 `JSONResponse`에 넣고 `dict`로 변환하는 등의 작업을 모두 수행해 주었을 것이기 때문입니다. -예를 들어 <a href="https://en.wikipedia.org/wiki/XML" class="external-link" target="_blank">XML</a> 응답을 반환하고 싶다고 가정해보겠습니다. +이제, 이를 사용해 사용자 정의 응답을 반환하는 방법을 알아보겠습니다. + +예를 들어 <a href="https://en.wikipedia.org/wiki/XML" class="external-link" target="_blank">XML</a> 응답을 반환하고 싶다고 가정해 보겠습니다. XML 내용을 문자열에 넣고, 이를 `Response`에 넣어 반환할 수 있습니다: -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} + +## 참고 사항 { #notes } -## 참고 사항 `Response`를 직접 반환할 때, 그 데이터는 자동으로 유효성 검사되거나, 변환(직렬화)되거나, 문서화되지 않습니다. 그러나 [OpenAPI에서 추가 응답](additional-responses.md){.internal-link target=_blank}에서 설명된 대로 문서화할 수 있습니다. -이후 단락에서 자동 데이터 변환, 문서화 등을 사용하면서 사용자 정의 `Response`를 선언하는 방법을 확인할 수 있습니다. +이후 섹션에서 자동 데이터 변환, 문서화 등을 계속 사용하면서 이러한 사용자 정의 `Response`를 사용하는/선언하는 방법을 확인할 수 있습니다. diff --git a/docs/ko/docs/advanced/response-headers.md b/docs/ko/docs/advanced/response-headers.md index e4e022c9b..1c36db9b9 100644 --- a/docs/ko/docs/advanced/response-headers.md +++ b/docs/ko/docs/advanced/response-headers.md @@ -1,12 +1,12 @@ -# 응답 헤더 +# 응답 헤더 { #response-headers } -## `Response` 매개변수 사용하기 +## `Response` 매개변수 사용하기 { #use-a-response-parameter } -여러분은 *경로 작동 함수*에서 `Response` 타입의 매개변수를 선언할 수 있습니다 (쿠키와 같이 사용할 수 있습니다). +여러분은 *경로 처리 함수*에서 `Response` 타입의 매개변수를 선언할 수 있습니다 (쿠키와 같이 사용할 수 있습니다). 그런 다음, 여러분은 해당 *임시* 응답 객체에서 헤더를 설정할 수 있습니다. -{* ../../docs_src/response_headers/tutorial002.py hl[1,7:8] *} +{* ../../docs_src/response_headers/tutorial002_py39.py hl[1, 7:8] *} 그 후, 일반적으로 사용하듯이 필요한 객체(`dict`, 데이터베이스 모델 등)를 반환할 수 있습니다. @@ -16,26 +16,26 @@ 또한, 종속성에서 `Response` 매개변수를 선언하고 그 안에서 헤더(및 쿠키)를 설정할 수 있습니다. -## `Response` 직접 반환하기 +## `Response` 직접 반환하기 { #return-a-response-directly } `Response`를 직접 반환할 때에도 헤더를 추가할 수 있습니다. [응답을 직접 반환하기](response-directly.md){.internal-link target=_blank}에서 설명한 대로 응답을 생성하고, 헤더를 추가 매개변수로 전달하세요. -{* ../../docs_src/response_headers/tutorial001.py hl[10:12] *} +{* ../../docs_src/response_headers/tutorial001_py39.py hl[10:12] *} -/// note | 기술적 세부사항 +/// note | 기술 세부사항 `from starlette.responses import Response`나 `from starlette.responses import JSONResponse`를 사용할 수도 있습니다. -**FastAPI**는 `starlette.responses`를 `fastapi.responses`로 개발자의 편의를 위해 직접 제공하지만, 대부분의 응답은 Starlette에서 직접 제공됩니다. +**FastAPI**는 해당 *임시* 응답에서 헤더(쿠키와 상태 코드도 포함)를 추출하여, 여러분이 반환한 값을 포함하는 최종 응답에 `response_model`로 필터링된 값을 넣습니다. 그리고 `Response`는 헤더와 쿠키를 설정하는 데 자주 사용될 수 있으므로, **FastAPI**는 `fastapi.Response`로도 이를 제공합니다. /// -## 커스텀 헤더 +## 커스텀 헤더 { #custom-headers } -<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">‘X-’ 접두어를 사용하여</a> 커스텀 사설 헤더를 추가할 수 있습니다. +<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">`X-` 접두어를 사용하여</a> 커스텀 사설 헤더를 추가할 수 있다는 점을 기억하세요. -하지만, 여러분이 브라우저에서 클라이언트가 볼 수 있기를 원하는 커스텀 헤더가 있는 경우, CORS 설정에 이를 추가해야 합니다([CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}에서 자세히 알아보세요). `expose_headers` 매개변수를 사용하여 <a href="https://www.starlette.dev/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette의 CORS 설명서</a>에 문서화된 대로 설정할 수 있습니다. +하지만, 여러분이 브라우저에서 클라이언트가 볼 수 있기를 원하는 커스텀 헤더가 있는 경우, CORS 설정에 이를 추가해야 합니다([CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}에서 자세히 알아보세요). <a href="https://www.starlette.dev/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette의 CORS 문서</a>에 문서화된 `expose_headers` 매개변수를 사용하세요. diff --git a/docs/ko/docs/advanced/sub-applications.md b/docs/ko/docs/advanced/sub-applications.md index c5835de15..e1554ca5d 100644 --- a/docs/ko/docs/advanced/sub-applications.md +++ b/docs/ko/docs/advanced/sub-applications.md @@ -1,67 +1,67 @@ -# 하위 응용프로그램 - 마운트 +# 하위 응용프로그램 - 마운트 { #sub-applications-mounts } -만약 각각의 독립적인 OpenAPI와 문서 UI를 갖는 두 개의 독립적인 FastAPI 응용프로그램이 필요하다면, 메인 어플리케이션에 하나 (또는 그 이상의) 하위-응용프로그램(들)을 “마운트"해서 사용할 수 있습니다. +각각의 독립적인 OpenAPI와 문서 UI를 갖는 두 개의 독립적인 FastAPI 애플리케이션이 필요하다면, 메인 앱을 두고 하나(또는 그 이상)의 하위 응용프로그램을 "마운트"할 수 있습니다. -## **FastAPI** 응용프로그램 마운트 +## **FastAPI** 애플리케이션 마운트 { #mounting-a-fastapi-application } -“마운트"이란 완전히 “독립적인" 응용프로그램을 특정 경로에 추가하여 해당 하위 응용프로그램에서 선언된 *경로 동작*을 통해 해당 경로 아래에 있는 모든 작업들을 처리할 수 있도록 하는 것을 의미합니다. +"마운트"란 완전히 "독립적인" 애플리케이션을 특정 경로에 추가하고, 그 하위 응용프로그램에 선언된 _경로 처리_로 해당 경로 아래의 모든 것을 처리하도록 하는 것을 의미합니다. -### 최상단 응용프로그램 +### 최상위 애플리케이션 { #top-level-application } -먼저, 메인, 최상단의 **FastAPI** 응용프로그램과 이것의 *경로 동작*을 생성합니다: +먼저, 메인 최상위 **FastAPI** 애플리케이션과 그 *경로 처리*를 생성합니다: -{* ../../docs_src/sub_applications/tutorial001.py hl[3, 6:8] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[3, 6:8] *} -### 하위 응용프로그램 +### 하위 응용프로그램 { #sub-application } -다음으로, 하위 응용프로그램과 이것의 *경로 동작*을 생성합니다: +그 다음, 하위 응용프로그램과 그 *경로 처리*를 생성합니다. -이 하위 응용프로그램은 또 다른 표준 FastAPI 응용프로그램입니다. 다만 이것은 “마운트”될 것입니다: +이 하위 응용프로그램은 또 다른 표준 FastAPI 애플리케이션이지만, "마운트"될 애플리케이션입니다: -{* ../../docs_src/sub_applications/tutorial001.py hl[11, 14:16] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 14:16] *} -### 하위 응용프로그램 마운트 +### 하위 응용프로그램 마운트 { #mount-the-sub-application } -최상단 응용프로그램, `app`에 하위 응용프로그램, `subapi`를 마운트합니다. +최상위 애플리케이션 `app`에서 하위 응용프로그램 `subapi`를 마운트합니다. -이 예시에서, 하위 응용프로그램션은 `/subapi` 경로에 마운트 될 것입니다: +이 경우 `/subapi` 경로에 마운트됩니다: -{* ../../docs_src/sub_applications/tutorial001.py hl[11, 19] *} +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 19] *} -### 자동으로 생성된 API 문서 확인 +### 자동 API 문서 확인 { #check-the-automatic-api-docs } -이제, `uvicorn`으로 메인 응용프로그램을 실행하십시오. 당신의 파일이 `main.py`라면, 이렇게 실행합니다: +이제 파일과 함께 `fastapi` 명령을 실행하세요: <div class="termy"> ```console -$ uvicorn main:app --reload +$ fastapi dev main.py <span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` </div> -그리고 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>에서 문서를 여십시오. +그리고 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>에서 문서를 여세요. -메인 응용프로그램의 *경로 동작*만을 포함하는, 메인 응용프로그램에 대한 자동 API 문서를 확인할 수 있습니다: +메인 앱의 자동 API 문서를 보게 될 것이며, 메인 앱 자체의 _경로 처리_만 포함됩니다: -<img src="https://fastapi.tiangolo.com//img/tutorial/sub-applications/image01.png"> +<img src="/img/tutorial/sub-applications/image01.png"> -다음으로, <a href="http://127.0.0.1:8000/subapi/docs" class="external-link" target="_blank">http://127.0.0.1:8000/subapi/docs</a>에서 하위 응용프로그램의 문서를 여십시오. +그 다음, <a href="http://127.0.0.1:8000/subapi/docs" class="external-link" target="_blank">http://127.0.0.1:8000/subapi/docs</a>에서 하위 응용프로그램의 문서를 여세요. -하위 경로 접두사 `/subapi` 아래에 선언된 *경로 동작* 을 포함하는, 하위 응용프로그램에 대한 자동 API 문서를 확인할 수 있습니다: +하위 응용프로그램의 자동 API 문서를 보게 될 것이며, 하위 경로 접두사 `/subapi` 아래에 올바르게 포함된 하위 응용프로그램 자체의 _경로 처리_만 포함됩니다: -<img src="https://fastapi.tiangolo.com//img/tutorial/sub-applications/image02.png"> +<img src="/img/tutorial/sub-applications/image02.png"> -두 사용자 인터페이스 중 어느 하나를 사용해야하는 경우, 브라우저는 특정 응용프로그램 또는 하위 응용프로그램과 각각 통신할 수 있기 때문에 올바르게 동작할 것입니다. +두 사용자 인터페이스 중 어느 것과 상호작용을 시도하더라도 올바르게 동작할 것입니다. 브라우저가 각 특정 앱 또는 하위 앱과 통신할 수 있기 때문입니다. -### 기술적 세부사항: `root_path` +### 기술적 세부사항: `root_path` { #technical-details-root-path } -위에 설명된 것과 같이 하위 응용프로그램을 마운트하는 경우, FastAPI는 `root_path`라고 하는 ASGI 명세의 매커니즘을 사용하여 하위 응용프로그램에 대한 마운트 경로 통신을 처리합니다. +위에서 설명한 대로 하위 응용프로그램을 마운트하면, FastAPI는 ASGI 명세의 메커니즘인 `root_path`를 사용해 하위 응용프로그램에 대한 마운트 경로를 전달하는 작업을 처리합니다. -이를 통해, 하위 응용프로그램은 문서 UI를 위해 경로 접두사를 사용해야 한다는 사실을 인지합니다. +이렇게 하면 하위 응용프로그램은 문서 UI를 위해 해당 경로 접두사를 사용해야 한다는 것을 알게 됩니다. -하위 응용프로그램에도 역시 다른 하위 응용프로그램을 마운트하는 것이 가능하며 FastAPI가 모든 `root_path` 들을 자동적으로 처리하기 때문에 모든 것은 올바르게 동작할 것입니다. +또한 하위 응용프로그램도 자체적으로 하위 앱을 마운트할 수 있으며, FastAPI가 이 모든 `root_path`를 자동으로 처리하기 때문에 모든 것이 올바르게 동작합니다. -`root_path`와 이것을 사용하는 방법에 대해서는 [프록시의 뒷단](./behind-a-proxy.md){.internal-link target=_blank} 섹션에서 배울 수 있습니다. +`root_path`와 이를 명시적으로 사용하는 방법에 대해서는 [프록시 뒤](behind-a-proxy.md){.internal-link target=_blank} 섹션에서 더 알아볼 수 있습니다. diff --git a/docs/ko/docs/advanced/templates.md b/docs/ko/docs/advanced/templates.md index 612635713..fffffa6a5 100644 --- a/docs/ko/docs/advanced/templates.md +++ b/docs/ko/docs/advanced/templates.md @@ -1,4 +1,4 @@ -# 템플릿 +# 템플릿 { #templates } **FastAPI**와 함께 원하는 어떤 템플릿 엔진도 사용할 수 있습니다. @@ -6,10 +6,9 @@ 설정을 쉽게 할 수 있는 유틸리티가 있으며, 이를 **FastAPI** 애플리케이션에서 직접 사용할 수 있습니다(Starlette 제공). -## 의존성 설치 - -가상 환경을 생성하고(virtual environment{.internal-link target=_blank}), 활성화한 후 jinja2를 설치해야 합니다: +## 의존성 설치 { #install-dependencies } +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고, 활성화한 후 `jinja2`를 설치해야 합니다: <div class="termy"> @@ -21,39 +20,38 @@ $ pip install jinja2 </div> -## 사용하기 `Jinja2Templates` +## `Jinja2Templates` 사용하기 { #using-jinja2templates } * `Jinja2Templates`를 가져옵니다. * 나중에 재사용할 수 있는 `templates` 객체를 생성합니다. -* 템플릿을 반환할 경로 작업에 `Request` 매개변수를 선언합니다. +* 템플릿을 반환할 *경로 처리*에 `Request` 매개변수를 선언합니다. * 생성한 `templates`를 사용하여 `TemplateResponse`를 렌더링하고 반환합니다. 템플릿의 이름, 요청 객체 및 Jinja2 템플릿 내에서 사용될 키-값 쌍이 포함된 "컨텍스트" 딕셔너리도 전달합니다. - -```Python hl_lines="4 11 15-18" -{!../../docs_src/templates/tutorial001.py!} -``` +{* ../../docs_src/templates/tutorial001_py39.py hl[4,11,15:18] *} /// note | 참고 -FastAPI 0.108.0 이전과 Starlette 0.29.0에서는 `name`이 첫 번째 매개변수였습니다. +FastAPI 0.108.0 이전, Starlette 0.29.0에서는 `name`이 첫 번째 매개변수였습니다. -또한 이전 버전에서는 `request` 객체가 Jinja2의 컨텍스트에서 키-값 쌍의 일부로 전달되었습니다. +또한 그 이전 버전에서는 `request` 객체가 Jinja2의 컨텍스트에서 키-값 쌍의 일부로 전달되었습니다. /// /// tip | 팁 -`response_class=HTMLResponse`를 선언하면 문서 UI 응답이 HTML임을 알 수 있습니다. +`response_class=HTMLResponse`를 선언하면 문서 UI가 응답이 HTML임을 알 수 있습니다. /// -/// note | 기술 세부 사항 +/// note | 기술 세부사항 + `from starlette.templating import Jinja2Templates`를 사용할 수도 있습니다. -**FastAPI**는 개발자를 위한 편리함으로 `fastapi.templating` 대신 `starlette.templating`을 제공합니다. 하지만 대부분의 사용 가능한 응답은 Starlette에서 직접 옵니다. `Request` 및 `StaticFiles`도 마찬가지입니다. +**FastAPI**는 개발자를 위한 편리함으로 `fastapi.templating`과 동일하게 `starlette.templating`을 제공합니다. 하지만 대부분의 사용 가능한 응답은 Starlette에서 직접 옵니다. `Request` 및 `StaticFiles`도 마찬가지입니다. + /// -## 템플릿 작성하기 +## 템플릿 작성하기 { #writing-templates } 그런 다음 `templates/item.html`에 템플릿을 작성할 수 있습니다. 예를 들면: @@ -61,7 +59,7 @@ FastAPI 0.108.0 이전과 Starlette 0.29.0에서는 `name`이 첫 번째 매개 {!../../docs_src/templates/templates/item.html!} ``` -### 템플릿 컨텍스트 값 +### 템플릿 컨텍스트 값 { #template-context-values } 다음과 같은 HTML에서: @@ -85,9 +83,9 @@ Item ID: {{ id }} Item ID: 42 ``` -### 템플릿 `url_for` 인수 +### 템플릿 `url_for` 인수 { #template-url-for-arguments } -템플릿 내에서 `url_for()`를 사용할 수도 있으며, 이는 *경로 작업 함수*에서 사용될 인수와 동일한 인수를 받습니다. +템플릿 내에서 `url_for()`를 사용할 수도 있으며, 이는 *경로 처리 함수*에서 사용될 인수와 동일한 인수를 받습니다. 따라서 다음과 같은 부분에서: @@ -99,14 +97,15 @@ Item ID: 42 {% endraw %} -...이는 *경로 작업 함수* `read_item(id=id)`가 처리할 동일한 URL로 링크를 생성합니다. +...이는 *경로 처리 함수* `read_item(id=id)`가 처리할 동일한 URL로 링크를 생성합니다. 예를 들어, ID가 `42`일 경우, 이는 다음과 같이 렌더링됩니다: + ```html <a href="/items/42"> ``` -## 템플릿과 정적 파일 +## 템플릿과 정적 파일 { #templates-and-static-files } 템플릿 내에서 `url_for()`를 사용할 수 있으며, 예를 들어 `name="static"`으로 마운트한 `StaticFiles`와 함께 사용할 수 있습니다. @@ -114,7 +113,7 @@ Item ID: 42 {!../../docs_src/templates/templates/item.html!} ``` -이 예제에서는 `static/styles.css`에 있는 CSS 파일에 연결될 것입니다: +이 예제에서는 다음을 통해 `static/styles.css`에 있는 CSS 파일에 링크합니다: ```CSS hl_lines="4" {!../../docs_src/templates/static/styles.css!} @@ -122,6 +121,6 @@ Item ID: 42 그리고 `StaticFiles`를 사용하고 있으므로, 해당 CSS 파일은 **FastAPI** 애플리케이션에서 `/static/styles.css` URL로 자동 제공됩니다. -## 더 많은 세부 사항 +## 더 많은 세부 사항 { #more-details } 템플릿 테스트를 포함한 더 많은 세부 사항은 <a href="https://www.starlette.dev/templates/" class="external-link" target="_blank">Starlette의 템플릿 문서</a>를 확인하세요. diff --git a/docs/ko/docs/advanced/testing-dependencies.md b/docs/ko/docs/advanced/testing-dependencies.md index 780e19431..ed90fe472 100644 --- a/docs/ko/docs/advanced/testing-dependencies.md +++ b/docs/ko/docs/advanced/testing-dependencies.md @@ -1,14 +1,14 @@ -# 테스트 의존성 오버라이드 +# 오버라이드로 의존성 테스트하기 { #testing-dependencies-with-overrides } -## 테스트 중 의존성 오버라이드하기 +## 테스트 중 의존성 오버라이드하기 { #overriding-dependencies-during-testing } -테스트를 진행하다 보면 의존성을 오버라이드해야 하는 경우가 있습니다. +테스트를 진행하다 보면 테스트 중에 의존성을 오버라이드해야 하는 경우가 있습니다. 원래 의존성을 실행하고 싶지 않을 수도 있습니다(또는 그 의존성이 가지고 있는 하위 의존성까지도 실행되지 않길 원할 수 있습니다). 대신, 테스트 동안(특정 테스트에서만) 사용될 다른 의존성을 제공하고, 원래 의존성이 사용되던 곳에서 사용할 수 있는 값을 제공하기를 원할 수 있습니다. -### 사용 사례: 외부 서비스 +### 사용 사례: 외부 서비스 { #use-cases-external-service } 예를 들어, 외부 인증 제공자를 호출해야 하는 경우를 생각해봅시다. @@ -18,11 +18,11 @@ 외부 제공자를 한 번만 테스트하고 싶을 수도 있지만 테스트를 실행할 때마다 반드시 호출할 필요는 없습니다. -이 경우 해당 공급자를 호출하는 종속성을 오버라이드하고 테스트에 대해서만 모의 사용자를 반환하는 사용자 지정 종속성을 사용할 수 있습니다. +이 경우 해당 공급자를 호출하는 의존성을 오버라이드하고 테스트에 대해서만 모의 사용자를 반환하는 사용자 지정 의존성을 사용할 수 있습니다. -### `app.dependency_overrides` 속성 사용하기 +### `app.dependency_overrides` 속성 사용하기 { #use-the-app-dependency-overrides-attribute } -이런 경우를 위해 **FastAPI** 응용 프로그램에는 `app.dependency_overrides`라는 속성이 있습니다. 이는 간단한 `dict`입니다. +이런 경우를 위해 **FastAPI** 애플리케이션에는 `app.dependency_overrides`라는 속성이 있습니다. 이는 간단한 `dict`입니다. 테스트를 위해 의존성을 오버라이드하려면, 원래 의존성(함수)을 키로 설정하고 오버라이드할 의존성(다른 함수)을 값으로 설정합니다. @@ -34,7 +34,7 @@ **FastAPI** 애플리케이션 어디에서든 사용된 의존성에 대해 오버라이드를 설정할 수 있습니다. -원래 의존성은 *경로 동작 함수*, *경로 동작 데코레이터*(반환값을 사용하지 않는 경우), `.include_router()` 호출 등에서 사용될 수 있습니다. +원래 의존성은 *경로 처리 함수*, *경로 처리 데코레이터*(반환값을 사용하지 않는 경우), `.include_router()` 호출 등에서 사용될 수 있습니다. FastAPI는 여전히 이를 오버라이드할 수 있습니다. @@ -42,7 +42,7 @@ FastAPI는 여전히 이를 오버라이드할 수 있습니다. 그런 다음, `app.dependency_overrides`를 빈 `dict`로 설정하여 오버라이드를 재설정(제거)할 수 있습니다: -```python +```Python app.dependency_overrides = {} ``` diff --git a/docs/ko/docs/advanced/testing-events.md b/docs/ko/docs/advanced/testing-events.md index 502762f23..8dbd4f6e6 100644 --- a/docs/ko/docs/advanced/testing-events.md +++ b/docs/ko/docs/advanced/testing-events.md @@ -1,5 +1,12 @@ -# 이벤트 테스트: 시작 - 종료 +# 이벤트 테스트: 라이프스팬 및 시작 - 종료 { #testing-events-lifespan-and-startup-shutdown } -테스트에서 이벤트 핸들러(`startup` 및 `shutdown`)를 실행해야 하는 경우, `with` 문과 함께 `TestClient`를 사용할 수 있습니다. +테스트에서 `lifespan`을 실행해야 하는 경우, `with` 문과 함께 `TestClient`를 사용할 수 있습니다: -{* ../../docs_src/app_testing/tutorial003.py hl[9:12,20:24] *} +{* ../../docs_src/app_testing/tutorial004_py39.py hl[9:15,18,27:28,30:32,41:43] *} + + +["공식 Starlette 문서 사이트에서 테스트에서 라이프스팬 실행하기."](https://www.starlette.dev/lifespan/#running-lifespan-in-tests)에 대한 자세한 내용을 더 읽을 수 있습니다. + +더 이상 권장되지 않는 `startup` 및 `shutdown` 이벤트의 경우, 다음과 같이 `TestClient`를 사용할 수 있습니다: + +{* ../../docs_src/app_testing/tutorial003_py39.py hl[9:12,20:24] *} diff --git a/docs/ko/docs/advanced/testing-websockets.md b/docs/ko/docs/advanced/testing-websockets.md index 9b6782429..1cb3cad67 100644 --- a/docs/ko/docs/advanced/testing-websockets.md +++ b/docs/ko/docs/advanced/testing-websockets.md @@ -1,13 +1,13 @@ -# WebSocket 테스트하기 +# WebSocket 테스트하기 { #testing-websockets } -`TestClient`를 사용하여 WebSocket을 테스트할 수 있습니다. +같은 `TestClient`를 사용하여 WebSocket을 테스트할 수 있습니다. 이를 위해 `with` 문에서 `TestClient`를 사용하여 WebSocket에 연결합니다: -{* ../../docs_src/app_testing/tutorial002.py hl[27:31] *} +{* ../../docs_src/app_testing/tutorial002_py39.py hl[27:31] *} /// note | 참고 -자세한 내용은 Starlette의 <a href="https://www.starlette.dev/testclient/#testing-websocket-sessions" class="external-link" target="_blank"> WebSocket 테스트</a>에 관한 설명서를 참고하시길 바랍니다. +자세한 내용은 Starlette의 <a href="https://www.starlette.dev/testclient/#testing-websocket-sessions" class="external-link" target="_blank">testing WebSockets</a> 문서를 확인하세요. /// diff --git a/docs/ko/docs/advanced/using-request-directly.md b/docs/ko/docs/advanced/using-request-directly.md index b88a83bf4..e0a5e99f8 100644 --- a/docs/ko/docs/advanced/using-request-directly.md +++ b/docs/ko/docs/advanced/using-request-directly.md @@ -1,10 +1,10 @@ -# `Request` 직접 사용하기 +# `Request` 직접 사용하기 { #using-the-request-directly } 지금까지 요청에서 필요한 부분을 각 타입으로 선언하여 사용해 왔습니다. 다음과 같은 곳에서 데이터를 가져왔습니다: -* 경로의 파라미터로부터. +* 경로를 매개변수로. * 헤더. * 쿠키. * 기타 등등. @@ -13,29 +13,29 @@ 하지만 `Request` 객체에 직접 접근해야 하는 상황이 있을 수 있습니다. -## `Request` 객체에 대한 세부 사항 +## `Request` 객체에 대한 세부 사항 { #details-about-the-request-object } **FastAPI**는 실제로 내부에 **Starlette**을 사용하며, 그 위에 여러 도구를 덧붙인 구조입니다. 따라서 여러분이 필요할 때 Starlette의 <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">`Request`</a> 객체를 직접 사용할 수 있습니다. -`Request` 객체에서 데이터를 직접 가져오는 경우(예: 본문을 읽기)에는 FastAPI가 해당 데이터를 검증하거나 변환하지 않으며, 문서화(OpenAPI를 통한 문서 자동화(로 생성된) API 사용자 인터페이스)도 되지 않습니다. +또한 이는 `Request` 객체에서 데이터를 직접 가져오는 경우(예: 본문을 읽기) FastAPI가 해당 데이터를 검증하거나 변환하지 않으며, 문서화(OpenAPI를 통한 자동 API 사용자 인터페이스용)도 되지 않는다는 의미이기도 합니다. 그러나 다른 매개변수(예: Pydantic 모델을 사용한 본문)는 여전히 검증, 변환, 주석 추가 등이 이루어집니다. -하지만 특정한 경우에는 `Request` 객체에 직접 접근하는 것이 유용할 수 있습니다. +하지만 특정한 경우에는 `Request` 객체를 가져오는 것이 유용할 수 있습니다. -## `Request` 객체를 직접 사용하기 +## `Request` 객체를 직접 사용하기 { #use-the-request-object-directly } -여러분이 클라이언트의 IP 주소/호스트 정보를 *경로 작동 함수* 내부에서 가져와야 한다고 가정해 보겠습니다. +여러분이 클라이언트의 IP 주소/호스트 정보를 *경로 처리 함수* 내부에서 가져와야 한다고 가정해 보겠습니다. 이를 위해서는 요청에 직접 접근해야 합니다. -{* ../../docs_src/using_request_directly/tutorial001.py hl[1,7:8] *} +{* ../../docs_src/using_request_directly/tutorial001_py39.py hl[1,7:8] *} -*경로 작동 함수* 매개변수를 `Request` 타입으로 선언하면 **FastAPI**가 해당 매개변수에 `Request` 객체를 전달하는 것을 알게 됩니다. +*경로 처리 함수* 매개변수를 `Request` 타입으로 선언하면 **FastAPI**가 해당 매개변수에 `Request`를 전달하는 것을 알게 됩니다. /// tip | 팁 -이 경우, 요청 매개변수와 함께 경로 매개변수를 선언한 것을 볼 수 있습니다. +이 경우, 요청 매개변수 옆에 경로 매개변수를 선언하고 있다는 점을 참고하세요. 따라서, 경로 매개변수는 추출되고 검증되며 지정된 타입으로 변환되고 OpenAPI로 주석이 추가됩니다. @@ -43,14 +43,14 @@ /// -## `Request` 설명서 +## `Request` 설명서 { #request-documentation } -여러분은 `Request` 객체에 대한 더 자세한 내용을 <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">공식 Starlette 설명서 사이트</a>에서 읽어볼 수 있습니다. +여러분은 <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">공식 Starlette 설명서 사이트의 `Request` 객체</a>에 대한 더 자세한 내용을 읽어볼 수 있습니다. /// note | 기술 세부사항 `from starlette.requests import Request`를 사용할 수도 있습니다. -**FastAPI**는 여러분(개발자)를 위한 편의를 위해 이를 직접 제공하지만, 실제로는 Starlette에서 가져온 것입니다. +**FastAPI**는 여러분(개발자)를 위한 편의를 위해 이를 직접 제공하지만, Starlette에서 직접 가져온 것입니다. /// diff --git a/docs/ko/docs/advanced/websockets.md b/docs/ko/docs/advanced/websockets.md index d9d0dd95c..b6817870b 100644 --- a/docs/ko/docs/advanced/websockets.md +++ b/docs/ko/docs/advanced/websockets.md @@ -1,10 +1,10 @@ -# WebSockets +# WebSockets { #websockets } 여러분은 **FastAPI**에서 <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API" class="external-link" target="_blank">WebSockets</a>를 사용할 수 있습니다. -## `WebSockets` 설치 +## `websockets` 설치 { #install-websockets } -[가상 환경](../virtual-environments.md){.internal-link target=_blank)를 생성하고 활성화한 다음, `websockets`를 설치하세요: +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고 활성화한 다음, `websockets`("WebSocket" 프로토콜을 쉽게 사용할 수 있게 해주는 Python 라이브러리)를 설치하세요: <div class="termy"> @@ -16,13 +16,13 @@ $ pip install websockets </div> -## WebSockets 클라이언트 +## WebSockets 클라이언트 { #websockets-client } -### 프로덕션 환경에서 +### 프로덕션 환경에서 { #in-production } 여러분의 프로덕션 시스템에서는 React, Vue.js 또는 Angular와 같은 최신 프레임워크로 생성된 프런트엔드를 사용하고 있을 가능성이 높습니다. -백엔드와 WebSockets을 사용해 통신하려면 아마도 프런트엔드의 유틸리티를 사용할 것입니다. +그리고 백엔드와 WebSockets을 사용해 통신하려면 아마도 프런트엔드의 유틸리티를 사용할 것입니다. 또는 네이티브 코드로 WebSocket 백엔드와 직접 통신하는 네이티브 모바일 응용 프로그램을 가질 수도 있습니다. @@ -30,23 +30,23 @@ $ pip install websockets --- -하지만 이번 예제에서는 일부 자바스크립트를 포함한 간단한 HTML 문서를 사용하겠습니다. 모든 것을 긴 문자열 안에 넣습니다. +하지만 이번 예제에서는 일부 자바스크립트를 포함한 매우 간단한 HTML 문서를 사용하겠습니다. 모든 것을 긴 문자열 안에 넣습니다. 물론, 이는 최적의 방법이 아니며 프로덕션 환경에서는 사용하지 않을 것입니다. -프로덕션 환경에서는 위에서 설명한 옵션 중 하나를 사용하는 것이 좋습니다. +프로덕션 환경에서는 위에서 설명한 옵션 중 하나를 사용할 것입니다. 그러나 이는 WebSockets의 서버 측에 집중하고 동작하는 예제를 제공하는 가장 간단한 방법입니다: -{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[2,6:38,41:43] *} -## `websocket` 생성하기 +## `websocket` 생성하기 { #create-a-websocket } **FastAPI** 응용 프로그램에서 `websocket`을 생성합니다: -{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[1,46:47] *} -/// note | 기술적 세부사항 +/// note | 기술 세부사항 `from starlette.websockets import WebSocket`을 사용할 수도 있습니다. @@ -54,17 +54,17 @@ $ pip install websockets /// -## 메시지를 대기하고 전송하기 +## 메시지를 대기하고 전송하기 { #await-for-messages-and-send-messages } WebSocket 경로에서 메시지를 대기(`await`)하고 전송할 수 있습니다. -{* ../../docs_src/websockets/tutorial001.py hl[48:52] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[48:52] *} 여러분은 이진 데이터, 텍스트, JSON 데이터를 받을 수 있고 전송할 수 있습니다. -## 시도해보기 +## 시도해보기 { #try-it } -파일 이름이 `main.py`라고 가정하고 응용 프로그램을 실행합니다: +파일 이름이 `main.py`라고 가정하고 다음으로 응용 프로그램을 실행합니다: <div class="termy"> @@ -76,7 +76,7 @@ $ fastapi dev main.py </div> -브라우저에서 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>을 열어보세요. +브라우저에서 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>을 여세요. 간단한 페이지가 나타날 것입니다: @@ -86,7 +86,7 @@ $ fastapi dev main.py <img src="/img/tutorial/websockets/image02.png"> -**FastAPI** WebSocket 응용 프로그램이 응답을 돌려줄 것입니다: +그리고 WebSockets가 포함된 **FastAPI** 응용 프로그램이 응답을 돌려줄 것입니다: <img src="/img/tutorial/websockets/image03.png"> @@ -94,9 +94,9 @@ $ fastapi dev main.py <img src="/img/tutorial/websockets/image04.png"> -모든 메시지는 동일한 WebSocket 연결을 사용합니다. +그리고 모든 메시지는 동일한 WebSocket 연결을 사용합니다. -## `Depends` 및 기타 사용하기 +## `Depends` 및 기타 사용하기 { #using-depends-and-others } WebSocket 엔드포인트에서 `fastapi`에서 다음을 가져와 사용할 수 있습니다: @@ -107,21 +107,21 @@ WebSocket 엔드포인트에서 `fastapi`에서 다음을 가져와 사용할 * `Path` * `Query` -이들은 다른 FastAPI 엔드포인트/*경로 작동*과 동일하게 동작합니다: +이들은 다른 FastAPI 엔드포인트/*경로 처리*와 동일하게 동작합니다: {* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} /// info | 정보 -WebSocket에서는 `HTTPException`을 발생시키는 것이 적합하지 않습니다. 대신 `WebSocketException`을 발생시킵니다. +WebSocket이기 때문에 `HTTPException`을 발생시키는 것은 적절하지 않습니다. 대신 `WebSocketException`을 발생시킵니다. 명세서에 정의된 <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1" class="external-link" target="_blank">유효한 코드</a>를 사용하여 종료 코드를 설정할 수 있습니다. /// -### 종속성을 가진 WebSockets 테스트 +### 종속성을 가진 WebSockets 시도해보기 { #try-the-websockets-with-dependencies } -파일 이름이 `main.py`라고 가정하고 응용 프로그램을 실행합니다: +파일 이름이 `main.py`라고 가정하고 다음으로 응용 프로그램을 실행합니다: <div class="termy"> @@ -133,9 +133,9 @@ $ fastapi dev main.py </div> -브라우저에서 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>을 열어보세요. +브라우저에서 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>을 여세요. -다음과 같은 값을 설정할 수 있습니다: +여기에서 다음을 설정할 수 있습니다: * 경로에 사용된 "Item ID". * 쿼리 매개변수로 사용된 "Token". @@ -146,13 +146,13 @@ $ fastapi dev main.py /// -이제 WebSocket에 연결하고 메시지를 전송 및 수신할 수 있습니다: +이렇게 하면 WebSocket에 연결하고 메시지를 전송 및 수신할 수 있습니다: <img src="/img/tutorial/websockets/image05.png"> -## 연결 해제 및 다중 클라이언트 처리 +## 연결 해제 및 다중 클라이언트 처리 { #handling-disconnections-and-multiple-clients } -WebSocket 연결이 닫히면, `await websocket.receive_text()`가 `WebSocketDisconnect` 예외를 발생시킵니다. 이를 잡아 처리할 수 있습니다: +WebSocket 연결이 닫히면, `await websocket.receive_text()`가 `WebSocketDisconnect` 예외를 발생시킵니다. 그러면 이 예제처럼 이를 잡아 처리할 수 있습니다. {* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *} @@ -160,7 +160,7 @@ WebSocket 연결이 닫히면, `await websocket.receive_text()`가 `WebSocketDis * 여러 브라우저 탭에서 앱을 엽니다. * 각 탭에서 메시지를 작성합니다. -* 한 탭을 닫아보세요. +* 그런 다음 탭 중 하나를 닫아보세요. `WebSocketDisconnect` 예외가 발생하며, 다른 모든 클라이언트가 다음과 같은 메시지를 수신합니다: @@ -170,17 +170,17 @@ Client #1596980209979 left the chat /// tip | 팁 -위 응용 프로그램은 여러 WebSocket 연결에 메시지를 브로드캐스트하는 방법을 보여주는 간단한 예제입니다. +위 앱은 여러 WebSocket 연결에 메시지를 처리하고 브로드캐스트하는 방법을 보여주는 최소한의 간단한 예제입니다. -그러나 모든 것을 메모리의 단일 리스트로 처리하므로, 프로세스가 실행 중인 동안만 동작하며 단일 프로세스에서만 작동합니다. +하지만 모든 것을 메모리의 단일 리스트로 처리하므로, 프로세스가 실행 중인 동안만 동작하며 단일 프로세스에서만 작동한다는 점을 기억하세요. -FastAPI와 쉽게 통합할 수 있으면서 더 견고하고 Redis, PostgreSQL 등을 지원하는 도구를 찾고 있다면, <a href="https://github.com/encode/broadcaster" class="external-link" target="_blank">encode/broadcaster</a>를 확인하세요. +FastAPI와 쉽게 통합할 수 있으면서 더 견고하고 Redis, PostgreSQL 등을 지원하는 도구가 필요하다면, <a href="https://github.com/encode/broadcaster" class="external-link" target="_blank">encode/broadcaster</a>를 확인하세요. /// -## 추가 정보 +## 추가 정보 { #more-info } -다음 옵션에 대한 자세한 내용을 보려면 Starlette의 문서를 확인하세요: +다음 옵션에 대해 더 알아보려면 Starlette의 문서를 확인하세요: * <a href="https://www.starlette.dev/websockets/" class="external-link" target="_blank">`WebSocket` 클래스</a>. * <a href="https://www.starlette.dev/endpoints/#websocketendpoint" class="external-link" target="_blank">클래스 기반 WebSocket 처리</a>. diff --git a/docs/ko/docs/advanced/wsgi.md b/docs/ko/docs/advanced/wsgi.md index 3e9de3e6c..89cf57cfe 100644 --- a/docs/ko/docs/advanced/wsgi.md +++ b/docs/ko/docs/advanced/wsgi.md @@ -1,10 +1,10 @@ -# WSGI 포함하기 - Flask, Django 그 외 +# WSGI 포함하기 - Flask, Django 그 외 { #including-wsgi-flask-django-others } -[서브 응용 프로그램 - 마운트](sub-applications.md){.internal-link target=_blank}, [프록시 뒤편에서](behind-a-proxy.md){.internal-link target=_blank}에서 보았듯이 WSGI 응용 프로그램들을 다음과 같이 마운트 할 수 있습니다. +[서브 응용 프로그램 - 마운트](sub-applications.md){.internal-link target=_blank}, [프록시 뒤편에서](behind-a-proxy.md){.internal-link target=_blank}에서 보았듯이 WSGI 응용 프로그램들을 마운트 할 수 있습니다. -`WSGIMiddleware`를 사용하여 WSGI 응용 프로그램(예: Flask, Django 등)을 감쌀 수 있습니다. +이를 위해 `WSGIMiddleware`를 사용해 WSGI 응용 프로그램(예: Flask, Django 등)을 감쌀 수 있습니다. -## `WSGIMiddleware` 사용하기 +## `WSGIMiddleware` 사용하기 { #using-wsgimiddleware } `WSGIMiddleware`를 불러와야 합니다. @@ -12,9 +12,9 @@ 그 후, 해당 경로에 마운트합니다. -{* ../../docs_src/wsgi/tutorial001.py hl[2:3,23] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} -## 확인하기 +## 확인하기 { #check-it } 이제 `/v1/` 경로에 있는 모든 요청은 Flask 응용 프로그램에서 처리됩니다. @@ -26,7 +26,7 @@ Hello, World from Flask! ``` -그리고 다음으로 이동하면 <a href="http://localhost:8000/v2" class="external-link" target="_blank">http://localhost:8000/v2</a> Flask의 응답을 볼 수 있습니다: +그리고 다음으로 이동하면 <a href="http://localhost:8000/v2" class="external-link" target="_blank">http://localhost:8000/v2</a> **FastAPI**의 응답을 볼 수 있습니다: ```JSON { diff --git a/docs/ko/docs/benchmarks.md b/docs/ko/docs/benchmarks.md index aff8ae70e..2d4fdbedd 100644 --- a/docs/ko/docs/benchmarks.md +++ b/docs/ko/docs/benchmarks.md @@ -1,10 +1,10 @@ -# 벤치마크 +# 벤치마크 { #benchmarks } -독립적인 TechEmpower 벤치마크에 따르면 **FastAPI** 애플리케이션이 Uvicorn을 사용하여 <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">가장 빠른 Python 프레임워크 중 하나</a>로 실행되며, Starlette와 Uvicorn 자체(내부적으로 FastAPI가 사용하는 도구)보다 조금 아래에 위치합니다. +독립적인 TechEmpower 벤치마크에 따르면 **FastAPI** 애플리케이션이 Uvicorn을 사용하여 <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">사용 가능한 가장 빠른 Python 프레임워크 중 하나</a>로 실행되며, Starlette와 Uvicorn 자체(내부적으로 FastAPI가 사용하는 도구)보다 조금 아래에 위치합니다. 그러나 벤치마크와 비교를 확인할 때 다음 사항을 염두에 두어야 합니다. -## 벤치마크와 속도 +## 벤치마크와 속도 { #benchmarks-and-speed } 벤치마크를 확인할 때, 일반적으로 여러 가지 유형의 도구가 동등한 것으로 비교되는 것을 볼 수 있습니다. @@ -16,7 +16,7 @@ * **Uvicorn**: ASGI 서버 * **Starlette**: (Uvicorn 사용) 웹 마이크로 프레임워크 - * **FastAPI**: (Starlette 사용) API 구축을 위한 데이터 검증 등 여러 추가 기능이 포함된 API 마이크로 프레임워크 + * **FastAPI**: (Starlette 사용) 데이터 검증 등 API를 구축하기 위한 여러 추가 기능이 포함된 API 마이크로 프레임워크 * **Uvicorn**: * 서버 자체 외에는 많은 추가 코드가 없기 때문에 최고의 성능을 발휘합니다. @@ -29,6 +29,6 @@ * **FastAPI**: * Starlette가 Uvicorn을 사용하므로 Uvicorn보다 빨라질 수 없는 것과 마찬가지로, **FastAPI**는 Starlette를 사용하므로 더 빠를 수 없습니다. * FastAPI는 Starlette에 추가적으로 더 많은 기능을 제공합니다. API를 구축할 때 거의 항상 필요한 데이터 검증 및 직렬화와 같은 기능들이 포함되어 있습니다. 그리고 이를 사용하면 문서 자동화 기능도 제공됩니다(문서 자동화는 응용 프로그램 실행 시 오버헤드를 추가하지 않고 시작 시 생성됩니다). - * FastAPI를 사용하지 않고 직접 Starlette(또는 Sanic, Flask, Responder 등)를 사용했다면 데이터 검증 및 직렬화를 직접 구현해야 합니다. 따라서 최종 응용 프로그램은 FastAPI를 사용한 것과 동일한 오버헤드를 가지게 될 것입니다. 많은 경우 데이터 검증 및 직렬화가 응용 프로그램에서 작성된 코드 중 가장 많은 부분을 차지합니다. - * 따라서 FastAPI를 사용함으로써 개발 시간, 버그, 코드 라인을 줄일 수 있으며, FastAPI를 사용하지 않았을 때와 동일하거나 더 나은 성능을 얻을 수 있습니다(코드에서 모두 구현해야 하기 때문에). - * FastAPI를 비교할 때는 Flask-apispec, NestJS, Molten 등 데이터 검증, 직렬화 및 문서화가 통합된 자동 데이터 검증, 직렬화 및 문서화를 제공하는 웹 응용 프로그램 프레임워크(또는 도구 집합)와 비교하세요. + * FastAPI를 사용하지 않고 직접 Starlette(또는 다른 도구, 예: Sanic, Flask, Responder 등)를 사용했다면 데이터 검증 및 직렬화를 직접 구현해야 합니다. 따라서 최종 응용 프로그램은 FastAPI를 사용한 것과 동일한 오버헤드를 가지게 될 것입니다. 많은 경우 데이터 검증 및 직렬화가 응용 프로그램에서 작성된 코드 중 가장 많은 부분을 차지합니다. + * 따라서 FastAPI를 사용함으로써 개발 시간, 버그, 코드 라인을 줄일 수 있으며, FastAPI를 사용하지 않았을 때와 동일한 성능(또는 더 나은 성능)을 얻을 수 있을 것입니다(코드에서 모두 구현해야 하기 때문에). + * FastAPI를 비교할 때는 Flask-apispec, NestJS, Molten 등 데이터 검증, 직렬화 및 문서화를 제공하는 웹 애플리케이션 프레임워크(또는 도구 집합)와 비교하세요. 통합된 자동 데이터 검증, 직렬화 및 문서화를 제공하는 프레임워크입니다. diff --git a/docs/ko/docs/deployment/cloud.md b/docs/ko/docs/deployment/cloud.md index dbc814bbd..0705e120c 100644 --- a/docs/ko/docs/deployment/cloud.md +++ b/docs/ko/docs/deployment/cloud.md @@ -1,13 +1,24 @@ -# FastAPI를 클라우드 제공업체에서 배포하기 +# 클라우드 제공업체에서 FastAPI 배포하기 { #deploy-fastapi-on-cloud-providers } 사실상 거의 **모든 클라우드 제공업체**를 사용하여 여러분의 FastAPI 애플리케이션을 배포할 수 있습니다. 대부분의 경우, 주요 클라우드 제공업체에서는 FastAPI를 배포할 수 있도록 가이드를 제공합니다. -## 클라우드 제공업체 - 후원자들 +## FastAPI Cloud { #fastapi-cloud } -몇몇 클라우드 제공업체들은 [**FastAPI를 후원하며**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, 이를 통해 FastAPI와 FastAPI **생태계**가 지속적이고 건전한 **발전**을 할 수 있습니다. +**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>**는 **FastAPI**를 만든 동일한 작성자와 팀이 구축했습니다. -이는 FastAPI와 **커뮤니티** (여러분)에 대한 진정한 헌신을 보여줍니다. 그들은 여러분에게 **좋은 서비스**를 제공할 뿐 만이 아니라 여러분이 **훌륭하고 건강한 프레임워크인** FastAPI 를 사용하길 원하기 때문입니다. 🙇 +최소한의 노력으로 API를 **구축**, **배포**, **접근**하는 과정을 간소화합니다. -아래와 같은 서비스를 사용해보고 각 서비스의 가이드를 따를 수도 있습니다. +FastAPI로 앱을 빌드할 때의 동일한 **개발자 경험**을 클라우드에 **배포**하는 데에도 제공합니다. 🎉 + +FastAPI Cloud는 *FastAPI and friends* 오픈 소스 프로젝트의 주요 후원자이자 자금 제공자입니다. ✨ + +## 클라우드 제공업체 - 후원자들 { #cloud-providers-sponsors } + +다른 몇몇 클라우드 제공업체들도 ✨ [**FastAPI를 후원합니다**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨. 🙇 + +가이드를 따라 하고 서비스를 사용해보기 위해 이들도 고려해볼 수 있습니다: + +* <a href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" class="external-link" target="_blank">Render</a> +* <a href="https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi" class="external-link" target="_blank">Railway</a> diff --git a/docs/ko/docs/deployment/docker.md b/docs/ko/docs/deployment/docker.md index e8b2746c5..be04c923a 100644 --- a/docs/ko/docs/deployment/docker.md +++ b/docs/ko/docs/deployment/docker.md @@ -1,17 +1,17 @@ -# 컨테이너의 FastAPI - 도커 +# 컨테이너의 FastAPI - 도커 { #fastapi-in-containers-docker } -FastAPI 어플리케이션을 배포할 때 일반적인 접근 방법은 **리눅스 컨테이너 이미지**를 생성하는 것입니다. 이 방법은 주로 <a href="https://www.docker.com/" class="external-link" target="_blank">**도커**</a>를 사용해 이루어집니다. 그런 다음 해당 컨테이너 이미지를 몇가지 방법으로 배포할 수 있습니다. +FastAPI 애플리케이션을 배포할 때 일반적인 접근 방법은 **리눅스 컨테이너 이미지**를 빌드하는 것입니다. 보통 <a href="https://www.docker.com/" class="external-link" target="_blank">**Docker**</a>를 사용해 수행합니다. 그런 다음 해당 컨테이너 이미지를 몇 가지 가능한 방법 중 하나로 배포할 수 있습니다. -리눅스 컨테이너를 사용하는 데에는 **보안**, **반복 가능성**, **단순함** 등의 장점이 있습니다. +리눅스 컨테이너를 사용하면 **보안**, **재현 가능성**, **단순함** 등 여러 장점이 있습니다. /// tip | 팁 -시간에 쫓기고 있고 이미 이런것들을 알고 있다면 [`Dockerfile`👇](#build-a-docker-image-for-fastapi)로 점프할 수 있습니다. +시간이 없고 이미 이런 내용들을 알고 계신가요? 아래의 [`Dockerfile` 👇](#build-a-docker-image-for-fastapi)로 이동하세요. /// <details> -<summary>도커파일 미리보기 👀</summary> +<summary>Dockerfile Preview 👀</summary> ```Dockerfile FROM python:3.9 @@ -24,128 +24,125 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./app /code/app -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +CMD ["fastapi", "run", "app/main.py", "--port", "80"] # If running behind a proxy like Nginx or Traefik add --proxy-headers -# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"] +# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"] ``` </details> -## 컨테이너란 +## 컨테이너란 { #what-is-a-container } -컨테이너(주로 리눅스 컨테이너)는 어플리케이션의 의존성과 필요한 파일들을 모두 패키징하는 매우 **가벼운** 방법입니다. 컨테이너는 같은 시스템에 있는 다른 컨테이너(다른 어플리케이션이나 요소들)와 독립적으로 유지됩니다. +컨테이너(주로 리눅스 컨테이너)는 모든 의존성과 필요한 파일을 포함해 애플리케이션을 패키징하면서, 같은 시스템의 다른 컨테이너(다른 애플리케이션이나 컴포넌트)와는 분리된 상태로 유지할 수 있는 매우 **가벼운** 방법입니다. -리눅스 컨테이너는 호스트(머신, 가상 머신, 클라우드 서버 등)와 같은 리눅스 커널을 사용해 실행됩니다. 이말은 리눅스 컨테이너가 (전체 운영체제를 모방하는 다른 가상 머신과 비교했을 때) 매우 가볍다는 것을 의미합니다. +리눅스 컨테이너는 호스트(머신, 가상 머신, 클라우드 서버 등)와 같은 리눅스 커널을 사용해 실행됩니다. 즉, 전체 운영체제를 에뮬레이션하는 완전한 가상 머신에 비해 매우 가볍습니다. -이 방법을 통해, 컨테이너는 직접 프로세스를 실행하는 것과 비슷한 정도의 **적은 자원**을 소비합니다 (가상 머신은 훨씬 많은 자원을 소비할 것입니다). +이 방식으로 컨테이너는 프로세스를 직접 실행하는 것과 비슷한 수준의 **적은 자원**을 소비합니다(가상 머신은 훨씬 더 많은 자원을 소비합니다). -컨테이너는 또한 그들만의 **독립된** 실행 프로세스 (일반적으로 하나의 프로세스로 충분합니다), 파일 시스템, 그리고 네트워크를 가지므로 배포, 보안, 개발 및 기타 과정을 단순화 합니다. +또한 컨테이너는 자체적인 **격리된** 실행 프로세스(보통 하나의 프로세스), 파일 시스템, 네트워크를 가지므로 배포, 보안, 개발 등을 단순화합니다. -## 컨테이너 이미지란 +## 컨테이너 이미지란 { #what-is-a-container-image } -**컨테이너**는 **컨테이너 이미지**를 실행한 것 입니다. +**컨테이너**는 **컨테이너 이미지**에서 실행됩니다. -컨테이너 이미지란 컨테이너에 필요한 모든 파일, 환경 변수 그리고 디폴트 명령/프로그램의 **정적** 버전입니다. 여기서 **정적**이란 말은 컨테이너 **이미지**가 작동되거나 실행되지 않으며, 단지 패키지 파일과 메타 데이터라는 것을 의미합니다. +컨테이너 이미지는 컨테이너에 있어야 하는 모든 파일, 환경 변수, 기본 명령/프로그램의 **정적** 버전입니다. 여기서 **정적**이라는 것은 컨테이너 **이미지**가 실행 중이거나 수행되는 것이 아니라, 패키징된 파일과 메타데이터일 뿐이라는 뜻입니다. -저장된 정적 컨텐츠인 **컨테이너 이미지**와 대조되게, **컨테이너**란 보통 실행될 수 있는 작동 인스턴스를 의미합니다. +저장된 정적 콘텐츠인 "**컨테이너 이미지**"와 달리, "**컨테이너**"는 보통 실행 중인 인스턴스, 즉 **실행되는** 대상을 의미합니다. -**컨테이너**가 (**컨테이너 이미지**로 부터) 시작되고 실행되면, 컨테이너는 파일이나 환경 변수를 생성하거나 변경할 수 있습니다. 이러한 변화는 오직 컨테이너에서만 존재하며, 그 기반이 되는 컨테이너 이미지에는 지속되지 않습니다 (즉 디스크에는 저장되지 않습니다). +**컨테이너**가 시작되어 실행 중이면(**컨테이너 이미지**로부터 시작됨) 파일, 환경 변수 등을 생성하거나 변경할 수 있습니다. 이러한 변경은 해당 컨테이너에만 존재하며, 기반이 되는 컨테이너 이미지에는 지속되지 않습니다(디스크에 저장되지 않습니다). -컨테이너 이미지는 **프로그램** 파일과 컨텐츠, 즉 `python`과 어떤 파일 `main.py`에 비교할 수 있습니다. +컨테이너 이미지는 **프로그램** 파일과 그 콘텐츠, 예를 들어 `python`과 어떤 파일 `main.py`에 비유할 수 있습니다. -그리고 (**컨테이너 이미지**와 대비해서) **컨테이너**는 이미지의 실제 실행 인스턴스로 **프로세스**에 비교할 수 있습니다. 사실, 컨테이너는 **프로세스 러닝**이 있을 때만 실행됩니다 (그리고 보통 하나의 프로세스 입니다). 컨테이너는 내부에서 실행되는 프로세스가 없으면 종료됩니다. +그리고 **컨테이너** 자체는(**컨테이너 이미지**와 달리) 이미지의 실제 실행 인스턴스로서 **프로세스**에 비유할 수 있습니다. 실제로 컨테이너는 **실행 중인 프로세스**가 있을 때만 실행됩니다(보통 단일 프로세스입니다). 컨테이너 내부에 실행 중인 프로세스가 없으면 컨테이너는 중지됩니다. -## 컨테이너 이미지 +## 컨테이너 이미지 { #container-images } -도커는 **컨테이너 이미지**와 **컨테이너**를 생성하고 관리하는데 주요 도구 중 하나가 되어왔습니다. +Docker는 **컨테이너 이미지**와 **컨테이너**를 생성하고 관리하는 주요 도구 중 하나입니다. -그리고 <a href="https://hub.docker.com/" class="external-link" target="_blank">도커 허브</a>에 다양한 도구, 환경, 데이터베이스, 그리고 어플리케이션에 대해 미리 만들어진 **공식 컨테이너 이미지**가 공개되어 있습니다. +또한 <a href="https://hub.docker.com/" class="external-link" target="_blank">Docker Hub</a>에는 다양한 도구, 환경, 데이터베이스, 애플리케이션을 위한 미리 만들어진 **공식 컨테이너 이미지**가 공개되어 있습니다. -예를 들어, 공식 <a href="https://hub.docker.com/_/python" class="external-link" target="_blank">파이썬 이미지</a>가 있습니다. +예를 들어, 공식 <a href="https://hub.docker.com/_/python" class="external-link" target="_blank">Python Image</a>가 있습니다. -또한 다른 대상, 예를 들면 데이터베이스를 위한 이미지들도 있습니다: +그리고 데이터베이스 등 다양한 용도의 다른 이미지도 많이 있습니다. 예를 들면: * <a href="https://hub.docker.com/_/postgres" class="external-link" target="_blank">PostgreSQL</a> * <a href="https://hub.docker.com/_/mysql" class="external-link" target="_blank">MySQL</a> * <a href="https://hub.docker.com/_/mongo" class="external-link" target="_blank">MongoDB</a> * <a href="https://hub.docker.com/_/redis" class="external-link" target="_blank">Redis</a> 등 -미리 만들어진 컨테이너 이미지를 사용하면 서로 다른 도구들을 **결합**하기 쉽습니다. 대부분의 경우에, **공식 이미지들**을 사용하고 환경 변수를 통해 설정할 수 있습니다. +미리 만들어진 컨테이너 이미지를 사용하면 서로 다른 도구를 **결합**하고 사용하기가 매우 쉽습니다. 예를 들어 새로운 데이터베이스를 시험해 볼 때도 그렇습니다. 대부분의 경우 **공식 이미지**를 사용하고, 환경 변수로 설정만 하면 됩니다. -이런 방법으로 대부분의 경우에 컨테이너와 도커에 대해 배울 수 있으며 다양한 도구와 요소들에 대한 지식을 재사용할 수 있습니다. +이렇게 하면 많은 경우 컨테이너와 Docker를 학습하고, 그 지식을 여러 다른 도구와 컴포넌트에 재사용할 수 있습니다. -따라서, 서로 다른 **다중 컨테이너**를 생성한 다음 이들을 연결할 수 있습니다. 예를 들어 데이터베이스, 파이썬 어플리케이션, 리액트 프론트엔드 어플리케이션을 사용하는 웹 서버에 대한 컨테이너를 만들어 이들의 내부 네트워크로 각 컨테이너를 연결할 수 있습니다. +따라서 데이터베이스, Python 애플리케이션, React 프론트엔드 애플리케이션이 있는 웹 서버 등 서로 다른 것들을 담은 **여러 컨테이너**를 실행하고 내부 네트워크를 통해 연결할 수 있습니다. -모든 컨테이너 관리 시스템(도커나 쿠버네티스)은 이러한 네트워킹 특성을 포함하고 있습니다. +Docker나 Kubernetes 같은 모든 컨테이너 관리 시스템에는 이러한 네트워킹 기능이 통합되어 있습니다. -## 컨테이너와 프로세스 +## 컨테이너와 프로세스 { #containers-and-processes } -**컨테이너 이미지**는 보통 **컨테이너**를 시작하기 위해 필요한 메타데이터와 디폴트 커맨드/프로그램과 그 프로그램에 전달하기 위한 파라미터들을 포함합니다. 이는 커맨드 라인에서 프로그램을 실행할 때 필요한 값들과 유사합니다. +**컨테이너 이미지**는 보통 **컨테이너**가 시작될 때 실행되어야 하는 기본 프로그램/명령과 해당 프로그램에 전달할 매개변수를 메타데이터에 포함합니다. 커맨드 라인에서 실행할 때와 매우 유사합니다. -**컨테이너**가 시작되면, 해당 커맨드/프로그램이 실행됩니다 (그러나 다른 커맨드/프로그램을 실행하도록 오버라이드 할 수 있습니다). +**컨테이너**가 시작되면 해당 명령/프로그램을 실행합니다(다만 오버라이드하여 다른 명령/프로그램을 실행하게 할 수도 있습니다). -컨테이너는 **메인 프로세스**(커맨드 또는 프로그램)이 실행되는 동안 실행됩니다. +컨테이너는 **메인 프로세스**(명령 또는 프로그램)가 실행되는 동안 실행됩니다. -컨테이너는 일반적으로 **단일 프로세스**를 가지고 있지만, 메인 프로세스의 서브 프로세스를 시작하는 것도 가능하며, 이 방법으로 하나의 컨테이너에 **다중 프로세스**를 가질 수 있습니다. +컨테이너는 보통 **단일 프로세스**를 가지지만, 메인 프로세스에서 서브프로세스를 시작할 수도 있으며, 그러면 같은 컨테이너에 **여러 프로세스**가 존재하게 됩니다. -그러나 **최소한 하나의 실행중인 프로세스**를 가지지 않고서는 실행중인 컨테이너를 가질 수 없습니다. 만약 메인 프로세스가 중단되면, 컨테이너도 중단됩니다. +하지만 **최소 하나의 실행 중인 프로세스** 없이 실행 중인 컨테이너를 가질 수는 없습니다. 메인 프로세스가 중지되면 컨테이너도 중지됩니다. -## FastAPI를 위한 도커 이미지 빌드하기 +## FastAPI를 위한 도커 이미지 빌드하기 { #build-a-docker-image-for-fastapi } -이제 무언가를 만들어 봅시다! 🚀 +좋습니다, 이제 무언가를 만들어 봅시다! 🚀 -**공식 파이썬** 이미지에 기반하여, FastAPI를 위한 **도커 이미지**를 **맨 처음부터** 생성하는 방법을 보이겠습니다. +**공식 Python** 이미지에 기반하여 FastAPI용 **Docker 이미지**를 **처음부터** 빌드하는 방법을 보여드리겠습니다. -**대부분의 경우**에 다음과 같은 것들을 하게 됩니다. 예를 들면: +이는 **대부분의 경우**에 하고 싶은 방식입니다. 예를 들면: -* **쿠버네티스** 또는 유사한 도구 사용하기 -* **라즈베리 파이**로 실행하기 -* 컨테이너 이미지를 실행할 클라우드 서비스 사용하기 등 +* **Kubernetes** 또는 유사한 도구를 사용할 때 +* **Raspberry Pi**에서 실행할 때 +* 컨테이너 이미지를 대신 실행해주는 클라우드 서비스를 사용할 때 등 -### 요구 패키지 +### 패키지 요구사항 { #package-requirements } -일반적으로는 어플리케이션의 특정 파일을 위한 **패키지 요구 조건**이 있을 것입니다. +보통 애플리케이션의 **패키지 요구사항**을 어떤 파일에 적어 둡니다. -그 요구 조건을 **설치**하는 방법은 여러분이 사용하는 도구에 따라 다를 것입니다. +이는 주로 그 요구사항을 **설치**하는 데 사용하는 도구에 따라 달라집니다. -가장 일반적인 방법은 패키지 이름과 버전이 줄 별로 기록된 `requirements.txt` 파일을 만드는 것입니다. +가장 일반적인 방법은 패키지 이름과 버전을 한 줄에 하나씩 적어 둔 `requirements.txt` 파일을 사용하는 것입니다. -버전의 범위를 설정하기 위해서는 [FastAPI 버전들에 대하여](versions.md){.internal-link target=_blank}에 쓰여진 것과 같은 아이디어를 사용합니다. +버전 범위를 설정할 때는 [FastAPI 버전들에 대하여](versions.md){.internal-link target=_blank}에서 읽은 것과 같은 아이디어를 사용하면 됩니다. -예를 들어, `requirements.txt` 파일은 다음과 같을 수 있습니다: +예를 들어 `requirements.txt`는 다음과 같을 수 있습니다: ``` -fastapi>=0.68.0,<0.69.0 -pydantic>=1.8.0,<2.0.0 -uvicorn>=0.15.0,<0.16.0 +fastapi[standard]>=0.113.0,<0.114.0 +pydantic>=2.7.0,<3.0.0 ``` -그리고 일반적으로 패키지 종속성은 `pip`로 설치합니다. 예를 들어: +그리고 보통 `pip`로 패키지 의존성을 설치합니다. 예를 들면: <div class="termy"> ```console $ pip install -r requirements.txt ---> 100% -Successfully installed fastapi pydantic uvicorn +Successfully installed fastapi pydantic ``` </div> /// info | 정보 -패키지 종속성을 정의하고 설치하기 위한 방법과 도구는 다양합니다. - -나중에 아래 세션에서 Poetry를 사용한 예시를 보이겠습니다. 👇 +패키지 의존성을 정의하고 설치하는 다른 형식과 도구도 있습니다. /// -### **FastAPI** 코드 생성하기 +### **FastAPI** 코드 생성하기 { #create-the-fastapi-code } -* `app` 디렉터리를 생성하고 이동합니다. -* 빈 파일 `__init__.py`을 생성합니다. -* 다음과 같은 `main.py`을 생성합니다: +* `app` 디렉터리를 만들고 들어갑니다. +* 빈 파일 `__init__.py`를 만듭니다. +* 다음 내용으로 `main.py` 파일을 만듭니다: ```Python from typing import Union @@ -165,79 +162,109 @@ def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` -### 도커파일 +### Dockerfile { #dockerfile } -이제 같은 프로젝트 디렉터리에 다음과 같은 파일 `Dockerfile`을 생성합니다: +이제 같은 프로젝트 디렉터리에 다음 내용으로 `Dockerfile` 파일을 만듭니다: ```{ .dockerfile .annotate } -# (1) +# (1)! FROM python:3.9 -# (2) +# (2)! WORKDIR /code -# (3) +# (3)! COPY ./requirements.txt /code/requirements.txt -# (4) +# (4)! RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -# (5) +# (5)! COPY ./app /code/app -# (6) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +# (6)! +CMD ["fastapi", "run", "app/main.py", "--port", "80"] ``` -1. 공식 파이썬 베이스 이미지에서 시작합니다. +1. 공식 Python 베이스 이미지에서 시작합니다. -2. 현재 워킹 디렉터리를 `/code`로 설정합니다. +2. 현재 작업 디렉터리를 `/code`로 설정합니다. - 여기에 `requirements.txt` 파일과 `app` 디렉터리를 위치시킬 것입니다. + 여기에 `requirements.txt` 파일과 `app` 디렉터리를 둘 것입니다. -3. 요구 조건과 파일을 `/code` 디렉터리로 복사합니다. +3. 요구사항 파일을 `/code` 디렉터리로 복사합니다. - 처음에는 **오직** 요구 조건이 필요한 파일만 복사하고, 이외의 코드는 그대로 둡니다. + 처음에는 요구사항 파일만 **단독으로** 복사하고, 나머지 코드는 복사하지 않습니다. - 이 파일이 **자주 바뀌지 않기 때문에**, 도커는 파일을 탐지하여 이 단계의 **캐시**를 사용하여 다음 단계에서도 캐시를 사용할 수 있도록 합니다. + 이 파일은 **자주 바뀌지 않기** 때문에 Docker는 이를 감지하여 이 단계에서 **캐시**를 사용하고, 다음 단계에서도 캐시를 사용할 수 있게 해줍니다. -4. 요구 조건 파일에 있는 패키지 종속성을 설치합니다. +4. 요구사항 파일에 있는 패키지 의존성을 설치합니다. - `--no-cache-dir` 옵션은 `pip`에게 다운로드한 패키지들을 로컬 환경에 저장하지 않도록 전달합니다. 이는 마치 같은 패키지를 설치하기 위해 오직 `pip`만 다시 실행하면 될 것 같지만, 컨테이너로 작업하는 경우 그렇지는 않습니다. + `--no-cache-dir` 옵션은 `pip`가 다운로드한 패키지를 로컬에 저장하지 않도록 합니다. 이는 `pip`가 같은 패키지를 설치하기 위해 다시 실행될 때만 의미가 있지만, 컨테이너 작업에서는 그렇지 않기 때문입니다. - /// note | 노트 + /// note | 참고 - `--no-cache-dir` 는 오직 `pip`와 관련되어 있으며, 도커나 컨테이너와는 무관합니다. + `--no-cache-dir`는 `pip`에만 관련되어 있으며 Docker나 컨테이너와는 관련이 없습니다. /// - `--upgrade` 옵션은 `pip`에게 설치된 패키지들을 업데이트하도록 합니다. + `--upgrade` 옵션은 이미 설치된 패키지가 있다면 `pip`가 이를 업그레이드하도록 합니다. - 이전 단계에서 파일을 복사한 것이 **도커 캐시**에 의해 탐지되기 때문에, 이 단계에서도 가능한 한 **도커 캐시**를 사용하게 됩니다. + 이전 단계에서 파일을 복사한 것이 **Docker 캐시**에 의해 감지될 수 있으므로, 이 단계에서도 가능하면 **Docker 캐시를 사용**합니다. - 이 단계에서 캐시를 사용하면 **매번** 모든 종속성을 다운로드 받고 설치할 필요가 없어, 개발 과정에서 이미지를 지속적으로 생성하는 데에 드는 **시간**을 많이 **절약**할 수 있습니다. + 이 단계에서 캐시를 사용하면 개발 중에 이미지를 반복해서 빌드할 때, 의존성을 **매번 다운로드하고 설치하는** 대신 많은 **시간**을 **절약**할 수 있습니다. -5. `/code` 디렉터리에 `./app` 디렉터리를 복사합니다. +5. `./app` 디렉터리를 `/code` 디렉터리 안으로 복사합니다. - **자주 변경되는** 모든 코드를 포함하고 있기 때문에, 도커 **캐시**는 이 단계나 **이후의 단계에서** 잘 사용되지 않습니다. + 이 디렉터리에는 **가장 자주 변경되는** 코드가 모두 포함되어 있으므로, Docker **캐시**는 이 단계나 **이후 단계들**에서는 쉽게 사용되지 않습니다. - 그러므로 컨테이너 이미지 빌드 시간을 최적화하기 위해 `Dockerfile`의 **거의 끝 부분**에 입력하는 것이 중요합니다. + 따라서 컨테이너 이미지 빌드 시간을 최적화하려면 `Dockerfile`의 **끝부분 근처**에 두는 것이 중요합니다. -6. `uvicorn` 서버를 실행하기 위해 **커맨드**를 설정합니다. +6. 내부적으로 Uvicorn을 사용하는 `fastapi run`을 사용하도록 **명령**을 설정합니다. - `CMD`는 문자열 리스트를 입력받고, 각 문자열은 커맨드 라인의 각 줄에 입력할 문자열입니다. + `CMD`는 문자열 리스트를 받으며, 각 문자열은 커맨드 라인에서 공백으로 구분해 입력하는 항목들입니다. - 이 커맨드는 **현재 워킹 디렉터리**에서 실행되며, 이는 위에서 `WORKDIR /code`로 설정한 `/code` 디렉터리와 같습니다. - - 프로그램이 `/code`에서 시작하고 그 속에 `./app` 디렉터리가 여러분의 코드와 함께 들어있기 때문에, **Uvicorn**은 이를 보고 `app`을 `app.main`으로부터 **불러 올** 것입니다. + 이 명령은 **현재 작업 디렉터리**에서 실행되며, 이는 위에서 `WORKDIR /code`로 설정한 `/code` 디렉터리와 같습니다. /// tip | 팁 -각 코드 라인을 코드의 숫자 버블을 클릭하여 리뷰할 수 있습니다. 👆 +코드의 각 숫자 버블을 클릭해 각 줄이 하는 일을 확인하세요. 👆 /// -이제 여러분은 다음과 같은 디렉터리 구조를 가지고 있을 것입니다: +/// warning | 경고 + +아래에서 설명하는 것처럼 `CMD` 지시어는 **항상** **exec form**을 사용해야 합니다. + +/// + +#### `CMD` 사용하기 - Exec Form { #use-cmd-exec-form } + +Docker 지시어 <a href="https://docs.docker.com/reference/dockerfile/#cmd" class="external-link" target="_blank">`CMD`</a>는 두 가지 형식으로 작성할 수 있습니다: + +✅ **Exec** form: + +```Dockerfile +# ✅ Do this +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +⛔️ **Shell** form: + +```Dockerfile +# ⛔️ Don't do this +CMD fastapi run app/main.py --port 80 +``` + +FastAPI가 정상적으로 종료(graceful shutdown)되고 [lifespan 이벤트](../advanced/events.md){.internal-link target=_blank}가 트리거되도록 하려면, 항상 **exec** form을 사용하세요. + +자세한 내용은 <a href="https://docs.docker.com/reference/dockerfile/#shell-and-exec-form" class="external-link" target="_blank">shell and exec form에 대한 Docker 문서</a>를 참고하세요. + +이는 `docker compose`를 사용할 때 꽤 눈에 띌 수 있습니다. 좀 더 기술적인 상세 내용은 Docker Compose FAQ 섹션을 참고하세요: <a href="https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop" class="external-link" target="_blank">Why do my services take 10 seconds to recreate or stop?</a>. + +#### 디렉터리 구조 { #directory-structure } + +이제 다음과 같은 디렉터리 구조가 되어야 합니다: ``` . @@ -248,51 +275,51 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] └── requirements.txt ``` -#### TLS 종료 프록시의 배후 +#### TLS 종료 프록시의 배후 { #behind-a-tls-termination-proxy } -만약 여러분이 컨테이너를 Nginx 또는 Traefik과 같은 TLS 종료 프록시 (로드 밸런서) 뒤에서 실행하고 있다면, `--proxy-headers` 옵션을 더하는 것이 좋습니다. 이 옵션은 Uvicorn에게 어플리케이션이 HTTPS 등의 뒤에서 실행되고 있으므로 프록시에서 전송된 헤더를 신뢰할 수 있다고 알립니다. +Nginx나 Traefik 같은 TLS 종료 프록시(로드 밸런서) 뒤에서 컨테이너를 실행하고 있다면 `--proxy-headers` 옵션을 추가하세요. 이 옵션은 (FastAPI CLI를 통해) Uvicorn에게 해당 프록시가 보낸 헤더를 신뢰하도록 하여, 애플리케이션이 HTTPS 뒤에서 실행 중임을 알게 합니다. ```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] ``` -#### 도커 캐시 +#### 도커 캐시 { #docker-cache } -이 `Dockerfile`에는 중요한 트릭이 있는데, 처음에는 **의존성이 있는 파일만** 복사하고, 나머지 코드는 그대로 둡니다. 왜 이런 방법을 써야하는지 설명하겠습니다. +이 `Dockerfile`에는 중요한 트릭이 있습니다. 먼저 **의존성 파일만** 복사하고, 나머지 코드는 복사하지 않는 것입니다. 왜 그런지 설명하겠습니다. ```Dockerfile COPY ./requirements.txt /code/requirements.txt ``` -도커와 다른 도구들은 컨테이너 이미지를 **증가하는 방식으로 빌드**합니다. `Dockerfile`의 맨 윗 부분부터 시작해, 레이어 위에 새로운 레이어를 더하는 방식으로, `Dockerfile`의 각 지시 사항으로 부터 생성된 어떤 파일이든 더해갑니다. +Docker와 다른 도구들은 `Dockerfile`의 위에서부터 시작해, 각 지시어가 만든 파일을 포함하며 **레이어를 하나씩 위에 쌓는 방식으로** 컨테이너 이미지를 **점진적으로** 빌드합니다. -도커 그리고 이와 유사한 도구들은 이미지 생성 시에 **내부 캐시**를 사용합니다. 만약 어떤 파일이 마지막으로 컨테이너 이미지를 빌드한 때로부터 바뀌지 않았다면, 파일을 다시 복사하여 새로운 레이어를 처음부터 생성하는 것이 아니라, 마지막에 생성했던 **같은 레이어를 재사용**합니다. +Docker와 유사한 도구들은 이미지를 빌드할 때 **내부 캐시**도 사용합니다. 어떤 파일이 마지막으로 컨테이너 이미지를 빌드했을 때부터 바뀌지 않았다면, 파일을 다시 복사하고 새 레이어를 처음부터 만드는 대신, 이전에 만든 **같은 레이어를 재사용**합니다. -단지 파일 복사를 지양하는 것으로 효율이 많이 향상되는 것은 아니지만, 그 단계에서 캐시를 사용했기 때문에, **다음 단계에서도 마찬가지로 캐시를 사용**할 수 있습니다. 예를 들어, 다음과 같은 의존성을 설치하는 지시 사항을 위한 캐시를 사용할 수 있습니다: +파일 복사를 피하는 것만으로 큰 개선이 생기지는 않을 수 있지만, 해당 단계에서 캐시를 사용했기 때문에 **다음 단계에서도 캐시를 사용할 수** 있습니다. 예를 들어 다음과 같이 의존성을 설치하는 지시어에서 캐시를 사용할 수 있습니다: ```Dockerfile RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt ``` -패키지를 포함하는 파일은 **자주 변경되지 않습니다**. 따라서 해당 파일만 복사하므로서, 도커는 그 단계의 **캐시를 사용**할 수 있습니다. +패키지 요구사항 파일은 **자주 변경되지 않습니다**. 따라서 그 파일만 복사하면 Docker는 그 단계에서 **캐시를 사용할 수** 있습니다. -그 다음으로, 도커는 **다음 단계에서** 의존성을 다운로드하고 설치하는 **캐시를 사용**할 수 있게 됩니다. 바로 이 과정에서 우리는 **많은 시간을 절약**하게 됩니다. ✨ ...그리고 기다리는 지루함도 피할 수 있습니다. 😪😆 +그리고 Docker는 그 다음 단계에서 의존성을 다운로드하고 설치할 때도 **캐시를 사용할 수** 있습니다. 바로 여기에서 **많은 시간을 절약**하게 됩니다. ✨ ...그리고 기다리며 지루해지는 것도 피할 수 있습니다. 😪😆 -패키지 의존성을 다운로드 받고 설치하는 데이는 **수 분이 걸릴 수 있지만**, **캐시**를 사용하면 최대 **수 초만에** 끝낼 수 있습니다. +패키지 의존성을 다운로드하고 설치하는 데에는 **몇 분**이 걸릴 수 있지만, **캐시**를 사용하면 많아야 **몇 초**면 끝납니다. -또한 여러분이 개발 과정에서 코드의 변경 사항이 반영되었는지 확인하기 위해 컨테이너 이미지를 계속해서 빌드하면, 절약된 시간은 축적되어 더욱 커질 것입니다. +또한 개발 중에 코드 변경 사항이 동작하는지 확인하기 위해 컨테이너 이미지를 계속 빌드하게 되므로, 이렇게 절약되는 시간은 누적되어 상당히 커집니다. -그리고 나서 `Dockerfile`의 거의 끝 부분에서, 모든 코드를 복사합니다. 이것이 **가장 빈번하게 변경**되는 부분이며, 대부분의 경우에 이 다음 단계에서는 캐시를 사용할 수 없기 때문에 가장 마지막에 둡니다. +그 다음 `Dockerfile`의 끝부분 근처에서 모든 코드를 복사합니다. 이 부분은 **가장 자주 변경되는** 부분이므로, 거의 항상 이 단계 이후에는 캐시를 사용할 수 없기 때문에 끝부분에 둡니다. ```Dockerfile COPY ./app /code/app ``` -### 도커 이미지 생성하기 +### 도커 이미지 생성하기 { #build-the-docker-image } -이제 모든 파일이 제자리에 있으니, 컨테이너 이미지를 빌드합니다. +이제 모든 파일이 제자리에 있으니 컨테이너 이미지를 빌드해봅시다. -* (여러분의 `Dockerfile`과 `app` 디렉터리가 위치한) 프로젝트 디렉터리로 이동합니다. +* 프로젝트 디렉터리로 이동합니다(`Dockerfile`이 있고 `app` 디렉터리를 포함하는 위치). * FastAPI 이미지를 빌드합니다: <div class="termy"> @@ -307,13 +334,13 @@ $ docker build -t myimage . /// tip | 팁 -맨 끝에 있는 `.` 에 주목합시다. 이는 `./`와 동등하며, 도커에게 컨테이너 이미지를 빌드하기 위한 디렉터리를 알려줍니다. +끝에 있는 `.`에 주목하세요. 이는 `./`와 동일하며, Docker에게 컨테이너 이미지를 빌드할 때 사용할 디렉터리를 알려줍니다. -이 경우에는 현재 디렉터리(`.`)와 같습니다. +이 경우 현재 디렉터리(`.`)입니다. /// -### 도커 컨테이너 시작하기 +### 도커 컨테이너 시작하기 { #start-the-docker-container } * 여러분의 이미지에 기반하여 컨테이너를 실행합니다: @@ -325,35 +352,35 @@ $ docker run -d --name mycontainer -p 80:80 myimage </div> -## 체크하기 +## 확인하기 { #check-it } -여러분의 도커 컨테이너 URL에서 실행 사항을 체크할 수 있습니다. 예를 들어: <a href="http://192.168.99.100/items/5?q=somequery" class="external-link" target="_blank">http://192.168.99.100/items/5?q=somequery</a> 또는 <a href="http://127.0.0.1/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1/items/5?q=somequery</a> (또는 동일하게, 여러분의 도커 호스트를 이용해서 체크할 수도 있습니다). +Docker 컨테이너의 URL에서 확인할 수 있어야 합니다. 예를 들어: <a href="http://192.168.99.100/items/5?q=somequery" class="external-link" target="_blank">http://192.168.99.100/items/5?q=somequery</a> 또는 <a href="http://127.0.0.1/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1/items/5?q=somequery</a>(또는 Docker 호스트를 사용해 동등하게 확인할 수 있습니다). -아래와 비슷한 것을 보게 될 것입니다: +아래와 같은 것을 보게 될 것입니다: ```JSON {"item_id": 5, "q": "somequery"} ``` -## 인터랙티브 API 문서 +## 인터랙티브 API 문서 { #interactive-api-docs } -이제 여러분은 <a href="http://192.168.99.100/docs" class="external-link" target="_blank">http://192.168.99.100/docs</a> 또는 <a href="http://127.0.0.1/docs" class="external-link" target="_blank">http://127.0.0.1/docs</a>로 이동할 수 있습니다(또는, 여러분의 도커 호스트를 이용할 수 있습니다). +이제 <a href="http://192.168.99.100/docs" class="external-link" target="_blank">http://192.168.99.100/docs</a> 또는 <a href="http://127.0.0.1/docs" class="external-link" target="_blank">http://127.0.0.1/docs</a>(또는 Docker 호스트를 사용해 동등하게 접근)로 이동할 수 있습니다. -여러분은 자동으로 생성된 인터랙티브 API(<a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>에서 제공된)를 볼 수 있습니다: +자동으로 생성된 인터랙티브 API 문서(<a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> 제공)를 볼 수 있습니다: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -## 대안 API 문서 +## 대안 API 문서 { #alternative-api-docs } -또한 여러분은 <a href="http://192.168.99.100/redoc" class="external-link" target="_blank">http://192.168.99.100/redoc</a> 또는 <a href="http://127.0.0.1/redoc" class="external-link" target="_blank">http://127.0.0.1/redoc</a>으로 이동할 수 있습니다(또는, 여러분의 도커 호스트를 이용할 수 있습니다). +또한 <a href="http://192.168.99.100/redoc" class="external-link" target="_blank">http://192.168.99.100/redoc</a> 또는 <a href="http://127.0.0.1/redoc" class="external-link" target="_blank">http://127.0.0.1/redoc</a>(또는 Docker 호스트를 사용해 동등하게 접근)로 이동할 수도 있습니다. -여러분은 자동으로 생성된 대안 문서(<a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>에서 제공된)를 볼 수 있습니다: +대안 자동 문서(<a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> 제공)를 볼 수 있습니다: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## 단일 파일 FastAPI로 도커 이미지 생성하기 +## 단일 파일 FastAPI로 도커 이미지 빌드하기 { #build-a-docker-image-with-a-single-file-fastapi } -만약 여러분의 FastAPI가 하나의 파일이라면, 예를 들어 `./app` 디렉터리 없이 `main.py` 파일만으로 이루어져 있다면, 파일 구조는 다음과 유사할 것입니다: +FastAPI가 단일 파일(예: `./app` 디렉터리 없이 `main.py`만 있는 경우)이라면, 파일 구조는 다음과 같을 수 있습니다: ``` . @@ -362,7 +389,7 @@ $ docker run -d --name mycontainer -p 80:80 myimage └── requirements.txt ``` -그러면 여러분들은 `Dockerfile` 내에 있는 파일을 복사하기 위해 그저 상응하는 경로를 바꾸기만 하면 됩니다: +그런 다음 `Dockerfile`에서 해당 파일을 복사하도록 경로만 맞게 변경하면 됩니다: ```{ .dockerfile .annotate hl_lines="10 13" } FROM python:3.9 @@ -373,359 +400,221 @@ COPY ./requirements.txt /code/requirements.txt RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -# (1) +# (1)! COPY ./main.py /code/ -# (2) -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] +# (2)! +CMD ["fastapi", "run", "main.py", "--port", "80"] ``` -1. `main.py` 파일을 `/code` 디렉터리로 곧바로 복사합니다(`./app` 디렉터리는 고려하지 않습니다). +1. `main.py` 파일을 `/code` 디렉터리로 직접 복사합니다(`./app` 디렉터리 없이). -2. Uvicorn을 실행해 `app` 객체를 (`app.main` 대신) `main`으로 부터 불러오도록 합니다. +2. 단일 파일 `main.py`에 있는 애플리케이션을 제공(serve)하기 위해 `fastapi run`을 사용합니다. -그 다음 Uvicorn 커맨드를 조정해서 FastAPI 객체를 불러오는데 `app.main` 대신에 새로운 모듈 `main`을 사용하도록 합니다. +`fastapi run`에 파일을 전달하면, 이것이 패키지의 일부가 아닌 단일 파일이라는 것을 자동으로 감지하고, 어떻게 임포트해서 FastAPI 앱을 제공할지 알아냅니다. 😎 -## 배포 개념 +## 배포 개념 { #deployment-concepts } -이제 컨테이너의 측면에서 [배포 개념](concepts.md){.internal-link target=_blank}에서 다루었던 것과 같은 배포 개념에 대해 이야기해 보겠습니다. +컨테이너 관점에서 같은 [배포 개념](concepts.md){.internal-link target=_blank}들을 다시 이야기해 봅시다. -컨테이너는 주로 어플리케이션을 빌드하고 배포하기 위한 과정을 단순화하는 도구이지만, **배포 개념**에 대한 특정한 접근법을 강요하지 않기 때문에 가능한 배포 전략에는 여러가지가 있습니다. +컨테이너는 주로 애플리케이션의 **빌드 및 배포** 과정을 단순화하는 도구이지만, 이러한 **배포 개념**을 처리하는 특정 접근 방식을 강제하지는 않으며, 가능한 전략은 여러 가지입니다. -**좋은 소식**은 서로 다른 전략들을 포괄하는 배포 개념이 있다는 점입니다. 🎉 +**좋은 소식**은 각 전략마다 모든 배포 개념을 다룰 수 있는 방법이 있다는 점입니다. 🎉 -컨테이너 측면에서 **배포 개념**을 리뷰해 보겠습니다: +컨테이너 관점에서 이 **배포 개념**들을 살펴봅시다: * HTTPS -* 구동하기 +* 시작 시 자동 실행 * 재시작 -* 복제 (실행 중인 프로세스 개수) +* 복제(실행 중인 프로세스 수) * 메모리 -* 시작하기 전 단계들 +* 시작 전 사전 단계 -## HTTPS +## HTTPS { #https } -만약 우리가 FastAPI 어플리케이션을 위한 **컨테이너 이미지**에만 집중한다면 (그리고 나중에 실행될 **컨테이너**에), HTTPS는 일반적으로 다른 도구에 의해 **외부적으로** 다루어질 것 입니다. +FastAPI 애플리케이션의 **컨테이너 이미지**(그리고 나중에 실행 중인 **컨테이너**)에만 집중한다면, HTTPS는 보통 다른 도구에 의해 **외부적으로** 처리됩니다. -**HTTPS**와 **인증서**의 **자동** 취득을 다루는 것은 다른 컨테이너가 될 수 있는데, 예를 들어 <a href="https://traefik.io/" class="external-link" target="_blank">Traefik</a>을 사용하는 것입니다. +예를 들어 <a href="https://traefik.io/" class="external-link" target="_blank">Traefik</a>을 사용하는 다른 컨테이너가 **HTTPS**와 **인증서**의 **자동** 획득을 처리할 수 있습니다. /// tip | 팁 -Traefik은 도커, 쿠버네티스, 그리고 다른 도구와 통합되어 있어 여러분의 컨테이너를 포함하는 HTTPS를 셋업하고 설정하는 것이 매우 쉽습니다. +Traefik은 Docker, Kubernetes 등과 통합되어 있어, 이를 사용해 컨테이너에 HTTPS를 설정하고 구성하기가 매우 쉽습니다. /// -대안적으로, HTTPS는 클라우드 제공자에 의해 서비스의 일환으로 다루어질 수도 있습니다 (이때도 어플리케이션은 여전히 컨테이너에서 실행될 것입니다). +또는 HTTPS를 클라우드 제공자가 서비스의 일부로 처리할 수도 있습니다(애플리케이션은 여전히 컨테이너에서 실행됩니다). -## 구동과 재시작 +## 시작 시 자동 실행과 재시작 { #running-on-startup-and-restarts } -여러분의 컨테이너를 **시작하고 실행하는** 데에 일반적으로 사용되는 도구는 따로 있습니다. +보통 컨테이너를 **시작하고 실행**하는 역할을 담당하는 다른 도구가 있습니다. -이는 **도커** 자체일 수도 있고, **도커 컴포즈**, **쿠버네티스**, **클라우드 서비스** 등이 될 수 있습니다. +직접 **Docker**일 수도 있고, **Docker Compose**, **Kubernetes**, **클라우드 서비스** 등일 수도 있습니다. -대부분 (또는 전체) 경우에, 컨테이너를 구동하거나 고장시에 재시작하도록 하는 간단한 옵션이 있습니다. 예를 들어, 도커에서는, 커맨드 라인 옵션 `--restart` 입니다. +대부분(또는 전부)의 경우, 시작 시 컨테이너를 실행하고 실패 시 재시작을 활성화하는 간단한 옵션이 있습니다. 예를 들어 Docker에서는 커맨드 라인 옵션 `--restart`입니다. -컨테이너를 사용하지 않고서는, 어플리케이션을 구동하고 재시작하는 것이 매우 번거롭고 어려울 수 있습니다. 하지만 **컨테이너를 사용한다면** 대부분의 경우에 이런 기능은 기본적으로 포함되어 있습니다. ✨ +컨테이너를 사용하지 않으면 애플리케이션을 시작 시 자동 실행하고 재시작까지 구성하는 것이 번거롭고 어렵습니다. 하지만 **컨테이너로 작업할 때**는 대부분의 경우 그 기능이 기본으로 포함되어 있습니다. ✨ -## 복제 - 프로세스 개수 +## 복제 - 프로세스 개수 { #replication-number-of-processes } -만약 여러분이 **쿠버네티스**와 머신 <abbr title="A group of machines that are configured to be connected and work together in some way.">클러스터</abbr>, 도커 스왐 모드, 노마드, 또는 다른 여러 머신 위에 분산 컨테이너를 관리하는 복잡한 시스템을 다루고 있다면, 여러분은 각 컨테이너에서 (워커와 함께 사용하는 Gunicorn 같은) **프로세스 매니저** 대신 **클러스터 레벨**에서 **복제를 다루**고 싶을 것입니다. +**Kubernetes**, Docker Swarm Mode, Nomad 등의 복잡한 시스템으로 여러 머신에 분산된 컨테이너를 관리하는 <abbr title="A group of machines that are configured to be connected and work together in some way.">cluster</abbr>를 사용한다면, 각 컨테이너에서(**워커를 사용하는 Uvicorn** 같은) **프로세스 매니저**를 쓰는 대신, **클러스터 레벨**에서 **복제를 처리**하고 싶을 가능성이 큽니다. -쿠버네티스와 같은 분산 컨테이너 관리 시스템 중 일부는 일반적으로 들어오는 요청에 대한 **로드 밸런싱**을 지원하면서 **컨테이너 복제**를 다루는 통합된 방법을 가지고 있습니다. 모두 **클러스터 레벨**에서 말이죠. +Kubernetes 같은 분산 컨테이너 관리 시스템은 보통 들어오는 요청에 대한 **로드 밸런싱**을 지원하면서도, **컨테이너 복제**를 처리하는 통합된 방법을 가지고 있습니다. 모두 **클러스터 레벨**에서요. -이런 경우에, 여러분은 [위에서 묘사된 것](#dockerfile)처럼 **처음부터 도커 이미지를** 빌드해서, 의존성을 설치하고, Uvicorn 워커를 관리하는 Gunicorn 대신 **단일 Uvicorn 프로세스**를 실행하고 싶을 것입니다. +그런 경우에는 [위에서 설명한 대로](#dockerfile) 의존성을 설치하고, 여러 Uvicorn 워커를 사용하는 대신 **단일 Uvicorn 프로세스**를 실행하는 **처음부터 만든 Docker 이미지**를 사용하는 것이 좋을 것입니다. -### 로드 밸런서 +### 로드 밸런서 { #load-balancer } -컨테이너로 작업할 때, 여러분은 일반적으로 **메인 포트의 상황을 감지하는** 요소를 가지고 있을 것입니다. 이는 **HTTPS**를 다루는 **TLS 종료 프록시**와 같은 다른 컨테이너일 수도 있고, 유사한 다른 도구일 수도 있습니다. +컨테이너를 사용할 때는 보통 **메인 포트에서 대기(listening)하는** 컴포넌트가 있습니다. **HTTPS**를 처리하기 위한 **TLS 종료 프록시** 역할을 하는 다른 컨테이너일 수도 있고, 유사한 도구일 수도 있습니다. -이 요소가 요청들의 **로드**를 읽어들이고 각 워커에게 (바라건대) **균형적으로** 분배한다면, 이 요소는 일반적으로 **로드 밸런서**라고 불립니다. +이 컴포넌트가 요청의 **부하(load)**를 받아 워커들에 (가능하면) **균형 있게** 분산한다면, 보통 **로드 밸런서**라고 부릅니다. /// tip | 팁 -HTTPS를 위해 사용된 **TLS 종료 프록시** 요소 또한 **로드 밸런서**가 될 수 있습니다. +HTTPS에 사용되는 동일한 **TLS 종료 프록시** 컴포넌트가 **로드 밸런서**이기도 한 경우가 많습니다. /// -또한 컨테이너로 작업할 때, 컨테이너를 시작하고 관리하기 위해 사용한 것과 동일한 시스템은 이미 해당 **로드 밸런서**로 부터 여러분의 앱에 해당하는 컨테이너로 **네트워크 통신**(예를 들어, HTTP 요청)을 전송하는 내부적인 도구를 가지고 있을 것입니다 (여기서도 로드 밸런서는 **TLS 종료 프록시**일 수 있습니다). +또한 컨테이너로 작업할 때, 이를 시작하고 관리하는 시스템은 이미 해당 **로드 밸런서**(또는 **TLS 종료 프록시**)에서 여러분의 앱이 있는 컨테이너로 **네트워크 통신**(예: HTTP 요청)을 전달하는 내부 도구를 가지고 있습니다. -### 하나의 로드 밸런서 - 다중 워커 컨테이너 +### 하나의 로드 밸런서 - 여러 워커 컨테이너 { #one-load-balancer-multiple-worker-containers } -**쿠버네티스**나 또는 다른 분산 컨테이너 관리 시스템으로 작업할 때, 시스템 내부의 네트워킹 메커니즘을 이용함으로써 메인 **포트**를 감지하고 있는 단일 **로드 밸런서**는 여러분의 앱에서 실행되고 있는 **여러개의 컨테이너**에 통신(요청들)을 전송할 수 있게 됩니다. +**Kubernetes** 같은 분산 컨테이너 관리 시스템에서는 내부 네트워킹 메커니즘을 통해, 메인 **포트**에서 대기하는 단일 **로드 밸런서**가 여러분의 앱을 실행하는 **여러 컨테이너**로 통신(요청)을 전달할 수 있습니다. -여러분의 앱에서 실행되고 있는 각각의 컨테이너는 일반적으로 **하나의 프로세스**만 가질 것입니다 (예를 들어, FastAPI 어플리케이션에서 실행되는 하나의 Uvicorn 프로세스처럼). 이 컨테이너들은 모두 같은 것을 실행하는 점에서 **동일한 컨테이너**이지만, 프로세스, 메모리 등은 공유하지 않습니다. 이 방식으로 여러분은 CPU의 **서로 다른 코어들** 또는 **서로 다른 머신들**을 **병렬화**하는 이점을 얻을 수 있습니다. +앱을 실행하는 각 컨테이너는 보통 **프로세스 하나만** 가집니다(예: FastAPI 애플리케이션을 실행하는 Uvicorn 프로세스). 모두 같은 것을 실행하는 **동일한 컨테이너**이지만, 각자 고유한 프로세스, 메모리 등을 가집니다. 이렇게 하면 CPU의 **서로 다른 코어** 또는 **서로 다른 머신**에서 **병렬화**의 이점을 얻을 수 있습니다. -또한 **로드 밸런서**가 있는 분산 컨테이너 시스템은 여러분의 앱에 있는 컨테이너 각각에 **차례대로 요청을 분산**시킬 것 입니다. 따라서 각 요청은 여러분의 앱에서 실행되는 여러개의 **복제된 컨테이너들** 중 하나에 의해 다루어질 것 입니다. +그리고 **로드 밸런서**가 있는 분산 컨테이너 시스템은 여러분의 앱을 실행하는 각 컨테이너에 **번갈아가며** 요청을 **분산**합니다. 따라서 각 요청은 여러분의 앱을 실행하는 여러 **복제된 컨테이너** 중 하나에서 처리될 수 있습니다. -그리고 일반적으로 **로드 밸런서**는 여러분의 클러스터에 있는 *다른* 앱으로 가는 요청들도 다룰 수 있으며 (예를 들어, 다른 도메인으로 가거나 다른 URL 경로 접두사를 가지는 경우), 이 통신들을 클러스터에 있는 *바로 그 다른* 어플리케이션으로 제대로 전송할 수 있습니다. +또한 보통 이 **로드 밸런서**는 클러스터 내 *다른* 앱으로 가는 요청(예: 다른 도메인, 또는 다른 URL 경로 접두사 아래로 가는 요청)도 처리할 수 있으며, 그 통신을 클러스터에서 실행 중인 *그 다른* 애플리케이션의 올바른 컨테이너로 전달할 수 있습니다. -### 단일 프로세스를 가지는 컨테이너 +### 컨테이너당 하나의 프로세스 { #one-process-per-container } -이 시나리오의 경우, 여러분은 이미 클러스터 레벨에서 복제를 다루고 있을 것이므로 **컨테이너 당 단일 (Uvicorn) 프로세스**를 가지고자 할 것입니다. +이 시나리오에서는 이미 클러스터 레벨에서 복제를 처리하고 있으므로, **컨테이너당 단일 (Uvicorn) 프로세스**를 두는 것이 좋을 가능성이 큽니다. -따라서, 여러분은 Gunicorn 이나 Uvicorn 워커, 또는 Uvicorn 워커를 사용하는 Uvicorn 매니저와 같은 프로세스 매니저를 가지고 싶어하지 **않을** 것입니다. 여러분은 컨테이너 당 **단일 Uvicorn 프로세스**를 가지고 싶어할 것입니다 (그러나 아마도 다중 컨테이너를 가질 것입니다). +따라서 이 경우 컨테이너에서 `--workers` 커맨드 라인 옵션 같은 방식으로 여러 워커를 두고 싶지는 **않을** 것입니다. 컨테이너당 **단일 Uvicorn 프로세스**만 두고(하지만 컨테이너는 여러 개일 수 있습니다) 싶을 것입니다. -이미 여러분이 클러스터 시스템을 관리하고 있으므로, (Uvicorn 워커를 관리하는 Gunicorn 이나 Uvicorn 처럼) 컨테이너 내에 다른 프로세스 매니저를 가지는 것은 **불필요한 복잡성**만 더하게 될 것입니다. +컨테이너 내부에 (여러 워커를 위한) 또 다른 프로세스 매니저를 두는 것은, 이미 클러스터 시스템에서 처리하고 있는 **불필요한 복잡성**만 추가할 가능성이 큽니다. -### 다중 프로세스를 가지는 컨테이너와 특수한 경우들 +### 여러 프로세스를 가진 컨테이너와 특수한 경우 { #containers-with-multiple-processes-and-special-cases } -당연한 말이지만, 여러분이 내부적으로 **Uvicorn 워커 프로세스들**를 시작하는 **Gunicorn 프로세스 매니저**를 가지는 단일 컨테이너를 원하는 **특수한 경우**도 있을 것입니다. +물론 컨테이너 하나에 여러 **Uvicorn 워커 프로세스**를 두고 싶을 수 있는 **특수한 경우**도 있습니다. -그런 경우에, 여러분들은 **Gunicorn**을 프로세스 매니저로 포함하는 **공식 도커 이미지**를 사용할 수 있습니다. 이 프로세스 매니저는 다중 **Uvicorn 워커 프로세스들**을 실행하며, 디폴트 세팅으로 현재 CPU 코어에 기반하여 자동으로 워커 개수를 조정합니다. 이 사항에 대해서는 아래의 [Gunicorn과 함께하는 공식 도커 이미지 - Uvicorn](#official-docker-image-with-gunicorn-uvicorn)에서 더 다루겠습니다. +그런 경우에는 `--workers` 커맨드 라인 옵션을 사용해 실행할 워커 수를 설정할 수 있습니다: -이런 경우에 해당하는 몇가지 예시가 있습니다: +```{ .dockerfile .annotate } +FROM python:3.9 -#### 단순한 앱 +WORKDIR /code -만약 여러분의 어플리케이션이 **충분히 단순**해서 (적어도 아직은) 프로세스 개수를 파인-튠 할 필요가 없거나 클러스터가 아닌 **단일 서버**에서 실행하고 있다면, 여러분은 컨테이너 내에 프로세스 매니저를 사용하거나 (공식 도커 이미지에서) 자동으로 설정되는 디폴트 값을 사용할 수 있습니다. +COPY ./requirements.txt /code/requirements.txt -#### 도커 구성 +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -여러분은 **도커 컴포즈**로 (클러스터가 아닌) **단일 서버로** 배포할 수 있으며, 이 경우에 공유된 네트워크와 **로드 밸런싱**을 포함하는 (도커 컴포즈로) 컨테이너의 복제를 관리하는 단순한 방법이 없을 수도 있습니다. +COPY ./app /code/app -그렇다면 여러분은 **프로세스 매니저**와 함께 내부에 **몇개의 워커 프로세스들**을 시작하는 **단일 컨테이너**를 필요로 할 수 있습니다. +# (1)! +CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"] +``` -#### Prometheus와 다른 이유들 +1. 여기서는 `--workers` 커맨드 라인 옵션으로 워커 수를 4로 설정합니다. -여러분은 **단일 프로세스**를 가지는 **다중 컨테이너** 대신 **다중 프로세스**를 가지는 **단일 컨테이너**를 채택하는 **다른 이유**가 있을 수 있습니다. +이런 방식이 의미가 있을 수 있는 예시는 다음과 같습니다: -예를 들어 (여러분의 장치 설정에 따라) Prometheus 익스포터와 같이 같은 컨테이너에 들어오는 **각 요청에 대해** 접근권한을 가지는 도구를 사용할 수 있습니다. +#### 단순한 앱 { #a-simple-app } -이 경우에 여러분이 **여러개의 컨테이너들**을 가지고 있다면, Prometheus가 **메트릭을 읽어 들일 때**, 디폴트로 **매번 하나의 컨테이너**(특정 리퀘스트를 관리하는 바로 그 컨테이너)로 부터 읽어들일 것입니다. 이는 모든 복제된 컨테이너에 대해 **축적된 메트릭들**을 읽어들이는 것과 대비됩니다. +애플리케이션이 **충분히 단순**해서 클러스터가 아닌 **단일 서버**에서 실행할 수 있다면, 컨테이너에 프로세스 매니저를 두고 싶을 수 있습니다. -그렇다면 이 경우에는 **다중 프로세스**를 가지는 **하나의 컨테이너**를 두어서 같은 컨테이너에서 모든 내부 프로세스에 대한 Prometheus 메트릭을 수집하는 로컬 도구(예를 들어 Prometheus 익스포터 같은)를 두어서 이 메그릭들을 하나의 컨테이너에 내에서 공유하는 방법이 더 단순할 것입니다. +#### Docker Compose { #docker-compose } + +**Docker Compose**로 클러스터가 아닌 **단일 서버**에 배포하는 경우, 공유 네트워크와 **로드 밸런싱**을 유지하면서(Docker Compose로) 컨테이너 복제를 관리하는 쉬운 방법이 없을 수 있습니다. + +그렇다면 **프로세스 매니저**가 컨테이너 내부에서 **여러 워커 프로세스**를 시작하는 **단일 컨테이너**를 원할 수 있습니다. --- -요점은, 이 중의 **어느것도** 여러분들이 반드시 따라야하는 **확정된 사실**이 아니라는 것입니다. 여러분은 이 아이디어들을 **여러분의 고유한 이용 사례를 평가**하는데 사용하고, 여러분의 시스템에 가장 적합한 접근법이 어떤 것인지 결정하며, 다음의 개념들을 관리하는 방법을 확인할 수 있습니다: +핵심은, 이것들 중 **어느 것도** 무조건 따라야 하는 **절대적인 규칙**은 아니라는 것입니다. 이 아이디어들을 사용해 **여러분의 사용 사례를 평가**하고, 여러분의 시스템에 가장 적합한 접근 방식을 결정하면서 다음 개념을 어떻게 관리할지 확인할 수 있습니다: * 보안 - HTTPS -* 구동하기 +* 시작 시 자동 실행 * 재시작 -* 복제 (실행 중인 프로세스 개수) +* 복제(실행 중인 프로세스 수) * 메모리 -* 시작하기 전 단계들 +* 시작 전 사전 단계 -## 메모리 +## 메모리 { #memory } -만약 여러분이 **컨테이너 당 단일 프로세스**를 실행한다면, 여러분은 각 컨테이너(복제된 경우에는 여러개의 컨테이너들)에 대해 잘 정의되고, 안정적이며, 제한된 용량의 메모리 소비량을 가지고 있을 것입니다. +**컨테이너당 단일 프로세스**를 실행하면, 각 컨테이너(복제된 경우 여러 개)마다 소비하는 메모리 양이 대체로 잘 정의되고 안정적이며 제한된 값이 됩니다. -그러면 여러분의 컨테이너 관리 시스템(예를 들어 **쿠버네티스**) 설정에서 앞서 정의된 것과 같은 메모리 제한과 요구사항을 설정할 수 있습니다. 이런 방법으로 **가용 머신**이 필요로하는 메모리와 클러스터에 있는 가용 머신들을 염두에 두고 **컨테이너를 복제**할 수 있습니다. +그런 다음 컨테이너 관리 시스템(예: **Kubernetes**) 설정에서 동일하게 메모리 제한과 요구사항을 설정할 수 있습니다. 그러면 클러스터에서 사용 가능한 머신에 있는 메모리와 컨테이너가 필요로 하는 메모리 양을 고려해 **컨테이너를 복제**할 수 있습니다. -만약 여러분의 어플리케이션이 **단순**하다면, 이것은 **문제가 되지 않을** 것이고, 고정된 메모리 제한을 구체화할 필요도 없을 것입니다. 하지만 여러분의 어플리케이션이 (예를 들어 **머신 러닝** 모델같이) **많은 메모리를 소요한다면**, 어플리케이션이 얼마나 많은 양의 메모리를 사용하는지 확인하고 **각 머신에서** 사용하는 **컨테이너의 수**를 조정할 필요가 있습니다 (그리고 필요에 따라 여러분의 클러스터에 머신을 추가할 수 있습니다). +애플리케이션이 **단순**하다면 이는 아마도 **문제가 되지 않을** 것이고, 엄격한 메모리 제한을 지정할 필요가 없을 수도 있습니다. 하지만 **많은 메모리를 사용한다면**(예: **머신 러닝** 모델), 얼마나 많은 메모리를 소비하는지 확인하고, **각 머신**에서 실행되는 **컨테이너 수**를 조정해야 합니다(필요하다면 클러스터에 머신을 더 추가할 수도 있습니다). -만약 여러분이 **컨테이너 당 여러개의 프로세스**를 실행한다면 (예를 들어 공식 도커 이미지 처럼), 여러분은 시작된 프로세스 개수가 가용한 것 보다 **더 많은 메모리를 소비**하지 않는지 확인해야 합니다. +**컨테이너당 여러 프로세스**를 실행한다면, 시작되는 프로세스 수가 사용 가능한 것보다 **더 많은 메모리를 소비하지** 않는지 확인해야 합니다. -## 시작하기 전 단계들과 컨테이너 +## 시작 전 단계와 컨테이너 { #previous-steps-before-starting-and-containers } -만약 여러분이 컨테이너(예를 들어 도커, 쿠버네티스)를 사용한다면, 여러분이 접근할 수 있는 주요 방법은 크게 두가지가 있습니다. +컨테이너(예: Docker, Kubernetes)를 사용한다면, 사용할 수 있는 주요 접근 방식은 두 가지입니다. -### 다중 컨테이너 +### 여러 컨테이너 { #multiple-containers } -만약 여러분이 **여러개의 컨테이너**를 가지고 있다면, 아마도 각각의 컨테이너는 **하나의 프로세스**를 가지고 있을 것입니다(예를 들어, **쿠버네티스** 클러스터에서). 그러면 여러분은 복제된 워커 컨테이너를 실행하기 **이전에**, 하나의 컨테이너에 있는 **이전의 단계들을** 수행하는 단일 프로세스를 가지는 **별도의 컨테이너들**을 가지고 싶을 것입니다. +**여러 컨테이너**가 있고 각 컨테이너가 보통 **단일 프로세스**를 실행한다면(예: **Kubernetes** 클러스터), 복제된 워커 컨테이너를 실행하기 **전에**, 단일 컨테이너에서 단일 프로세스로 **시작 전 사전 단계**를 수행하는 **별도의 컨테이너**를 두고 싶을 가능성이 큽니다. /// info | 정보 -만약 여러분이 쿠버네티스를 사용하고 있다면, 아마도 이는 <a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" class="external-link" target="_blank">Init Container</a>일 것입니다. +Kubernetes를 사용한다면, 이는 아마도 <a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" class="external-link" target="_blank">Init Container</a>일 것입니다. /// -만약 여러분의 이용 사례에서 이전 단계들을 **병렬적으로 여러번** 수행하는데에 문제가 없다면 (예를 들어 데이터베이스 이전을 실행하지 않고 데이터베이스가 준비되었는지 확인만 하는 경우), 메인 프로세스를 시작하기 전에 이 단계들을 각 컨테이너에 넣을 수 있습니다. +사용 사례에서 시작 전 사전 단계를 **여러 번 병렬로 실행**해도 문제가 없다면(예: 데이터베이스 마이그레이션을 실행하는 것이 아니라, 데이터베이스가 준비되었는지 확인만 하는 경우), 메인 프로세스를 시작하기 직전에 각 컨테이너에 그 단계를 넣을 수도 있습니다. -### 단일 컨테이너 +### 단일 컨테이너 { #single-container } -만약 여러분의 셋업이 **다중 프로세스**(또는 하나의 프로세스)를 시작하는 **하나의 컨테이너**를 가지는 단순한 셋업이라면, 사전 단계들을 앱을 포함하는 프로세스를 시작하기 직전에 같은 컨테이너에서 실행할 수 있습니다. 공식 도커 이미지는 이를 내부적으로 지원합니다. +**단일 컨테이너**에서 여러 **워커 프로세스**(또는 단일 프로세스)를 시작하는 단순한 셋업이라면, 앱이 있는 프로세스를 시작하기 직전에 같은 컨테이너에서 시작 전 사전 단계를 실행할 수 있습니다. -## Gunicorn과 함께하는 공식 도커 이미지 - Uvicorn +### 베이스 도커 이미지 { #base-docker-image } -앞 챕터에서 자세하게 설명된 것 처럼, Uvicorn 워커와 같이 실행되는 Gunicorn을 포함하는 공식 도커 이미지가 있습니다: [서버 워커 - Uvicorn과 함께하는 Gunicorn](server-workers.md){.internal-link target=_blank}. +과거에는 공식 FastAPI Docker 이미지가 있었습니다: <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>. 하지만 이제는 deprecated되었습니다. ⛔️ -이 이미지는 주로 위에서 설명된 상황에서 유용할 것입니다: [다중 프로세스를 가지는 컨테이너와 특수한 경우들](#containers-with-multiple-processes-and-special-cases). +아마도 이 베이스 도커 이미지(또는 유사한 다른 이미지)는 **사용하지 않는** 것이 좋습니다. -* <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>. +**Kubernetes**(또는 다른 도구)를 사용하고, 클러스터 레벨에서 여러 **컨테이너**로 **복제**를 이미 설정해 둔 경우라면, 위에서 설명한 대로 **처음부터 이미지를 빌드하는 것**이 더 낫습니다: [FastAPI를 위한 도커 이미지 빌드하기](#build-a-docker-image-for-fastapi). -/// warning | 경고 +그리고 여러 워커가 필요하다면, `--workers` 커맨드 라인 옵션을 간단히 사용하면 됩니다. -여러분이 이 베이스 이미지 또는 다른 유사한 이미지를 필요로 하지 **않을** 높은 가능성이 있으며, [위에서 설명된 것처럼: FastAPI를 위한 도커 이미지 빌드하기](#build-a-docker-image-for-fastapi) 처음부터 이미지를 빌드하는 것이 더 나을 수 있습니다. +/// note Technical Details | 기술 세부사항 + +이 Docker 이미지는 Uvicorn이 죽은 워커를 관리하고 재시작하는 기능을 지원하지 않던 시기에 만들어졌습니다. 그래서 Gunicorn과 Uvicorn을 함께 사용해야 했고, Gunicorn이 Uvicorn 워커 프로세스를 관리하고 재시작하도록 하기 위해 상당한 복잡성이 추가되었습니다. + +하지만 이제 Uvicorn(그리고 `fastapi` 명령)은 `--workers`를 지원하므로, 베이스 도커 이미지를 사용하는 대신 직접 이미지를 빌드하지 않을 이유가 없습니다(코드 양도 사실상 거의 같습니다 😅). /// -이 이미지는 가능한 CPU 코어에 기반한 **몇개의 워커 프로세스**를 설정하는 **자동-튜닝** 메커니즘을 포함하고 있습니다. +## 컨테이너 이미지 배포하기 { #deploy-the-container-image } -이 이미지는 **민감한 디폴트** 값을 가지고 있지만, 여러분들은 여전히 **환경 변수** 또는 설정 파일을 통해 설정값을 수정하고 업데이트 할 수 있습니다. - -또한 스크립트를 통해 <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker#pre_start_path" class="external-link" target="_blank">**시작하기 전 사전 단계**</a>를 실행하는 것을 지원합니다. - -/// tip | 팁 - -모든 설정과 옵션을 보려면, 도커 이미지 페이지로 이동합니다: <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>. - -/// - -### 공식 도커 이미지에 있는 프로세스 개수 - -이 이미지에 있는 **프로세스 개수**는 가용한 CPU **코어들**로 부터 **자동으로 계산**됩니다. - -이것이 의미하는 바는 이미지가 CPU로부터 **최대한의 성능**을 **쥐어짜낸다**는 것입니다. - -여러분은 이 설정 값을 **환경 변수**나 기타 방법들로 조정할 수 있습니다. - -그러나 프로세스의 개수가 컨테이너가 실행되고 있는 CPU에 의존한다는 것은 또한 **소요되는 메모리의 크기** 또한 이에 의존한다는 것을 의미합니다. - -그렇기 때문에, 만약 여러분의 어플리케이션이 많은 메모리를 요구하고 (예를 들어 머신러닝 모델처럼), 여러분의 서버가 CPU 코어 수는 많지만 **적은 메모리**를 가지고 있다면, 여러분의 컨테이너는 가용한 메모리보다 많은 메모리를 사용하려고 시도할 수 있으며, 결국 퍼포먼스를 크게 떨어뜨릴 수 있습니다(심지어 고장이 날 수도 있습니다). 🚨 - -### `Dockerfile` 생성하기 - -이 이미지에 기반해 `Dockerfile`을 생성하는 방법은 다음과 같습니다: - -```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app -``` - -### 더 큰 어플리케이션 - -만약 여러분이 [다중 파일을 가지는 더 큰 어플리케이션](../tutorial/bigger-applications.md){.internal-link target=_blank}을 생성하는 섹션을 따랐다면, 여러분의 `Dockerfile`은 대신 이렇게 생겼을 것입니다: - -```Dockerfile hl_lines="7" -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app/app -``` - -### 언제 사용할까 - -여러분들이 **쿠버네티스**(또는 유사한 다른 도구) 사용하거나 클러스터 레벨에서 다중 컨테이너를 이용해 이미 **사본**을 설정하고 있다면, 공식 베이스 이미지(또는 유사한 다른 이미지)를 사용하지 **않는** 것 좋습니다. 그런 경우에 여러분은 다음에 설명된 것 처럼 **처음부터 이미지를 빌드하는 것**이 더 낫습니다: [FastAPI를 위한 도커 이미지 빌드하기](#build-a-docker-image-for-fastapi). - -이 이미지는 위의 [다중 프로세스를 가지는 컨테이너와 특수한 경우들](#containers-with-multiple-processes-and-special-cases)에서 설명된 특수한 경우에 대해서만 주로 유용할 것입니다. 예를 들어, 만약 여러분의 어플리케이션이 **충분히 단순**해서 CPU에 기반한 디폴트 프로세스 개수를 설정하는 것이 잘 작동한다면, 클러스터 레벨에서 수동으로 사본을 설정할 필요가 없을 것이고, 여러분의 앱에서 하나 이상의 컨테이너를 실행하지도 않을 것입니다. 또는 만약에 여러분이 **도커 컴포즈**로 배포하거나, 단일 서버에서 실행하거나 하는 경우에도 마찬가지입니다. - -## 컨테이너 이미지 배포하기 - -컨테이너 (도커) 이미지를 완성한 뒤에 이를 배포하는 방법에는 여러가지 방법이 있습니다. +컨테이너(Docker) 이미지를 만든 후에는 이를 배포하는 여러 방법이 있습니다. 예를 들어: -* 단일 서버에서 **도커 컴포즈**로 배포하기 -* **쿠버네티스** 클러스터로 배포하기 -* 도커 스왐 모드 클러스터로 배포하기 -* 노마드 같은 다른 도구로 배포하기 -* 여러분의 컨테이너 이미지를 배포해주는 클라우드 서비스로 배포하기 +* 단일 서버에서 **Docker Compose**로 +* **Kubernetes** 클러스터로 +* Docker Swarm Mode 클러스터로 +* Nomad 같은 다른 도구로 +* 컨테이너 이미지를 받아 배포해주는 클라우드 서비스로 -## Poetry의 도커 이미지 +## `uv`를 사용하는 도커 이미지 { #docker-image-with-uv } -만약 여러분들이 프로젝트 의존성을 관리하기 위해 <a href="https://python-poetry.org/" class="external-link" target="_blank">Poetry</a>를 사용한다면, 도커의 멀티-스테이지 빌딩을 사용할 수 있습니다: +프로젝트를 설치하고 관리하기 위해 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>를 사용한다면, <a href="https://docs.astral.sh/uv/guides/integration/docker/" class="external-link" target="_blank">uv Docker guide</a>를 따를 수 있습니다. -```{ .dockerfile .annotate } -# (1) -FROM python:3.9 as requirements-stage +## 요약 { #recap } -# (2) -WORKDIR /tmp - -# (3) -RUN pip install poetry - -# (4) -COPY ./pyproject.toml ./poetry.lock* /tmp/ - -# (5) -RUN poetry export -f requirements.txt --output requirements.txt --without-hashes - -# (6) -FROM python:3.9 - -# (7) -WORKDIR /code - -# (8) -COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt - -# (9) -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt - -# (10) -COPY ./app /code/app - -# (11) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] -``` - -1. 첫 스테이지로, `requirements-stage`라고 이름 붙였습니다. - -2. `/tmp`를 현재의 워킹 디렉터리로 설정합니다. - - 이 위치에 우리는 `requirements.txt` 파일을 생성할 것입니다. - -3. 이 도커 스테이지에서 Poetry를 설치합니다. - -4. 파일 `pyproject.toml`와 `poetry.lock`를 `/tmp` 디렉터리로 복사합니다. - - `./poetry.lock*` (`*`로 끝나는) 파일을 사용하기 때문에, 파일이 아직 사용가능하지 않더라도 고장나지 않을 것입니다. - -5. `requirements.txt` 파일을 생성합니다. - -6. 이것이 마지막 스테이지로, 여기에 위치한 모든 것이 마지막 컨테이너 이미지에 포함될 것입니다. - -7. 현재의 워킹 디렉터리를 `/code`로 설정합니다. - -8. 파일 `requirements.txt`를 `/code` 디렉터리로 복사합니다. - - 이 파일은 오직 이전의 도커 스테이지에만 존재하며, 때문에 복사하기 위해서 `--from-requirements-stage` 옵션이 필요합니다. - -9. 생성된 `requirements.txt` 파일에 패키지 의존성을 설치합니다. - -10. `app` 디렉터리를 `/code` 디렉터리로 복사합니다. - -11. `uvicorn` 커맨드를 실행하여, `app.main`에서 불러온 `app` 객체를 사용하도록 합니다. - -/// tip | 팁 - -버블 숫자를 클릭해 각 줄이 하는 일을 알아볼 수 있습니다. - -/// - -**도커 스테이지**란 `Dockefile`의 일부로서 나중에 사용하기 위한 파일들을 생성하기 위한 **일시적인 컨테이너 이미지**로 작동합니다. - -첫 스테이지는 오직 **Poetry를 설치**하고 Poetry의 `pyproject.toml` 파일로부터 프로젝트 의존성을 위한 **`requirements.txt`를 생성**하기 위해 사용됩니다. - -이 `requirements.txt` 파일은 **다음 스테이지**에서 `pip`로 사용될 것입니다. - -마지막 컨테이너 이미지에는 **오직 마지막 스테이지만** 보존됩니다. 이전 스테이지(들)은 버려집니다. - -Poetry를 사용할 때 **도커 멀티-스테이지 빌드**를 사용하는 것이 좋은데, 여러분들의 프로젝트 의존성을 설치하기 위해 마지막 컨테이너 이미지에 **오직** `requirements.txt` 파일만 필요하지, Poetry와 그 의존성은 있을 필요가 없기 때문입니다. - -이 다음 (또한 마지막) 스테이지에서 여러분들은 이전에 설명된 것과 비슷한 방식으로 방식으로 이미지를 빌드할 수 있습니다. - -### TLS 종료 프록시의 배후 - Poetry - -이전에 언급한 것과 같이, 만약 여러분이 컨테이너를 Nginx 또는 Traefik과 같은 TLS 종료 프록시 (로드 밸런서) 뒤에서 실행하고 있다면, 커맨드에 `--proxy-headers` 옵션을 추가합니다: - -```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] -``` - -## 요약 - -컨테이너 시스템(예를 들어 **도커**나 **쿠버네티스**)을 사용하여 모든 **배포 개념**을 다루는 것은 꽤 간단합니다: +컨테이너 시스템(예: **Docker**, **Kubernetes**)을 사용하면 모든 **배포 개념**을 다루는 것이 상당히 단순해집니다: * HTTPS -* 구동하기 +* 시작 시 자동 실행 * 재시작 -* 복제 (실행 중인 프로세스 개수) +* 복제(실행 중인 프로세스 수) * 메모리 -* 시작하기 전 단계들 +* 시작 전 사전 단계 -대부분의 경우에서 여러분은 어떤 베이스 이미지도 사용하지 않고 공식 파이썬 도커 이미지에 기반해 **처음부터 컨테이너 이미지를 빌드**할 것입니다. +대부분의 경우 베이스 이미지는 사용하지 않고, 공식 Python Docker 이미지에 기반해 **처음부터 컨테이너 이미지를 빌드**하는 것이 좋습니다. -`Dockerfile`에 있는 지시 사항을 **순서대로** 다루고 **도커 캐시**를 사용하는 것으로 여러분은 **빌드 시간을 최소화**할 수 있으며, 이로써 생산성을 최대화할 수 있습니다 (그리고 지루함을 피할 수 있죠) 😎 - -특별한 경우에는, FastAPI를 위한 공식 도커 이미지를 사용할 수도 있습니다. 🤓 +`Dockerfile`에서 지시어의 **순서**와 **Docker 캐시**를 신경 쓰면 **빌드 시간을 최소화**해 생산성을 최대화할 수 있습니다(그리고 지루함도 피할 수 있습니다). 😎 diff --git a/docs/ko/docs/deployment/index.md b/docs/ko/docs/deployment/index.md index 87b05b68f..e0d237534 100644 --- a/docs/ko/docs/deployment/index.md +++ b/docs/ko/docs/deployment/index.md @@ -1,21 +1,23 @@ -# 배포하기 - 들어가면서 +# 배포 { #deployment } -**FastAPI**을 배포하는 것은 비교적 쉽습니다. +**FastAPI** 애플리케이션을 배포하는 것은 비교적 쉽습니다. -## 배포의 의미 +## 배포의 의미 { #what-does-deployment-mean } -**배포**란 애플리케이션을 **사용자가 사용**할 수 있도록 하는 데 필요한 단계를 수행하는 것을 의미합니다. +애플리케이션을 **배포**한다는 것은 **사용자가 사용**할 수 있도록 하는 데 필요한 단계를 수행하는 것을 의미합니다. -**웹 API**의 경우, 일반적으로 **사용자**가 중단이나 오류 없이 애플리케이션에 효율적으로 **접근**할 수 있도록 좋은 성능, 안정성 등을 제공하는 **서버 프로그램과** 함께 **원격 시스템**에 이를 설치하는 작업을 의미합니다. +**웹 API**의 경우, 일반적으로 **원격 머신**에 이를 설치하고, 좋은 성능, 안정성 등을 제공하는 **서버 프로그램**과 함께 구성하여 **사용자**가 중단이나 문제 없이 애플리케이션에 효율적으로 **접근**할 수 있게 하는 것을 포함합니다. -이는 지속적으로 코드를 변경하고, 지우고, 수정하고, 개발 서버를 중지했다가 다시 시작하는 등의 **개발** 단계와 대조됩니다. +이는 지속적으로 코드를 변경하고, 망가뜨리고 고치고, 개발 서버를 중지했다가 다시 시작하는 등의 **개발** 단계와 대조됩니다. -## 배포 전략 +## 배포 전략 { #deployment-strategies } -사용하는 도구나 특정 사례에 따라 여러 가지 방법이 있습니다. +구체적인 사용 사례와 사용하는 도구에 따라 여러 가지 방법이 있습니다. -배포도구들을 사용하여 직접 **서버에 배포**하거나, 배포작업의 일부를 수행하는 **클라우드 서비스** 또는 다른 방법을 사용할 수도 있습니다. +여러 도구를 조합해 직접 **서버를 배포**할 수도 있고, 작업의 일부를 대신해 주는 **클라우드 서비스**를 사용할 수도 있으며, 다른 가능한 선택지도 있습니다. -**FastAPI** 애플리케이션을 배포할 때 선택할 수 있는 몇 가지 주요 방법을 보여 드리겠습니다 (대부분 다른 유형의 웹 애플리케이션에도 적용됩니다). +예를 들어, FastAPI 뒤에 있는 저희 팀은 FastAPI로 작업하는 것과 같은 개발자 경험을 유지하면서, FastAPI 앱을 클라우드에 가능한 한 간소화된 방식으로 배포할 수 있도록 <a href="https://fastapicloud.com" class="external-link" target="_blank">**FastAPI Cloud**</a>를 만들었습니다. -다음 차례에 자세한 내용과 이를 위한 몇 가지 기술을 볼 수 있습니다. ✨ +**FastAPI** 애플리케이션을 배포할 때 아마 염두에 두어야 할 몇 가지 주요 개념을 보여드리겠습니다(대부분은 다른 유형의 웹 애플리케이션에도 적용됩니다). + +다음 섹션에서 염두에 둘 더 많은 세부사항과 이를 위한 몇 가지 기술을 볼 수 있습니다. ✨ diff --git a/docs/ko/docs/deployment/server-workers.md b/docs/ko/docs/deployment/server-workers.md index b40b25cd8..e98cfd114 100644 --- a/docs/ko/docs/deployment/server-workers.md +++ b/docs/ko/docs/deployment/server-workers.md @@ -1,130 +1,87 @@ -# 서버 워커 - 구니콘과 유비콘 +# 서버 워커 - 워커와 함께 사용하는 Uvicorn { #server-workers-uvicorn-with-workers } -전단계에서의 배포 개념들을 다시 확인해보겠습니다: +이전의 배포 개념들을 다시 확인해보겠습니다: * 보안 - HTTPS -* 서버 시작과 동시에 실행하기 +* 서버 시작 시 실행 * 재시작 -* **복제본 (실행 중인 프로세스의 숫자)** +* **복제(실행 중인 프로세스 수)** * 메모리 -* 시작하기 전의 여러 단계들 +* 시작하기 전의 이전 단계 -지금까지 문서의 모든 튜토리얼을 참고하여 **단일 프로세스**로 Uvicorn과 같은 **서버 프로그램**을 실행했을 것입니다. +지금까지 문서의 모든 튜토리얼을 참고하면서, `fastapi` 명령처럼 Uvicorn을 실행하는 **서버 프로그램**을 사용해 **단일 프로세스**로 실행해 왔을 가능성이 큽니다. -애플리케이션을 배포할 때 **다중 코어**를 활용하고 더 많은 요청을 처리할 수 있도록 **프로세스 복제본**이 필요합니다. +애플리케이션을 배포할 때는 **다중 코어**를 활용하고 더 많은 요청을 처리할 수 있도록 **프로세스 복제**를 하고 싶을 가능성이 큽니다. -전 과정이었던 [배포 개념들](concepts.md){.internal-link target=_blank}에서 본 것처럼 여러가지 방법이 존재합니다. +이전 장의 [배포 개념들](concepts.md){.internal-link target=_blank}에서 본 것처럼, 사용할 수 있는 전략이 여러 가지 있습니다. -지금부터 <a href="https://gunicorn.org/" class="external-link" target="_blank">**구니콘**</a>을 **유비콘 워커 프로세스**와 함께 사용하는 방법을 알려드리겠습니다. +여기서는 `fastapi` 명령을 사용하거나 `uvicorn` 명령을 직접 사용해서, **워커 프로세스**와 함께 **Uvicorn**을 사용하는 방법을 보여드리겠습니다. /// info | 정보 -만약 도커와 쿠버네티스 같은 컨테이너를 사용하고 있다면 다음 챕터 [FastAPI와 컨테이너 - 도커](docker.md){.internal-link target=_blank}에서 더 많은 정보를 얻을 수 있습니다. +Docker나 Kubernetes 같은 컨테이너를 사용하고 있다면, 다음 장인 [컨테이너에서의 FastAPI - 도커](docker.md){.internal-link target=_blank}에서 더 자세히 설명하겠습니다. -특히, 쿠버네티스에서 실행할 때는 구니콘을 사용하지 않고 대신 컨테이너당 하나의 유비콘 프로세스를 실행하는 것이 좋습니다. 이 장의 뒷부분에서 설명하겠습니다. +특히 **Kubernetes**에서 실행할 때는 워커를 사용하기보다는, 대신 **컨테이너당 단일 Uvicorn 프로세스 하나**를 실행하고 싶을 가능성이 크지만, 해당 내용은 그 장의 뒤에서 설명하겠습니다. /// -## 구니콘과 유비콘 워커 +## 여러 워커 { #multiple-workers } -**Gunicorn**은 **WSGI 표준**을 주로 사용하는 애플리케이션 서버입니다. 이것은 구니콘이 플라스크와 쟝고와 같은 애플리케이션을 제공할 수 있다는 것을 의미합니다. 구니콘 자체는 최신 **<a href="https://asgi.readthedocs.io/en/latest/" class="external-link" target="_blank">ASGI 표준</a>**을 사용하기 때문에 FastAPI와 호환되지 않습니다. +`--workers` 커맨드라인 옵션으로 여러 워커를 시작할 수 있습니다: -하지만 구니콘은 **프로세스 관리자**역할을 하고 사용자에게 특정 **워커 프로세스 클래스**를 알려줍니다. 그런 다음 구니콘은 해당 클래스를 사용하여 하나 이상의 **워커 프로세스**를 시작합니다. +//// tab | `fastapi` -그리고 **유비콘**은 **구니콘과 호환되는 워커 클래스**가 있습니다. - -이 조합을 사용하여 구니콘은 **프로세스 관리자** 역할을 하며 **포트**와 **IP**를 관찰하고, **유비콘 클래스**를 실행하는 워커 프로세스로 통신 정보를 **전송**합니다. - -그리고 나서 구니콘과 호환되는 **유비콘 워커** 클래스는 구니콘이 보낸 데이터를 FastAPI에서 사용하기 위한 ASGI 표준으로 변환하는 일을 담당합니다. - -## 구니콘과 유비콘 설치하기 +`fastapi` 명령을 사용한다면: <div class="termy"> ```console -$ pip install "uvicorn[standard]" gunicorn +$ <font color="#4E9A06">fastapi</font> run --workers 4 <u style="text-decoration-style:solid">main.py</u> ----> 100% + <span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting production server 🚀 + + Searching for package file structure from directories with + <font color="#3465A4">__init__.py</font> files + Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py + + <span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with the + following code: + + <u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> + + <span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font> + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000/docs</u></font> + + Logs: + + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font> <b>(</b>Press CTRL+C to + quit<b>)</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started parent process <b>[</b><font color="#34E2E2"><b>27365</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>27368</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>27369</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>27370</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>27367</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. ``` </div> -이 명령어는 유비콘 `standard` 추가 패키지(좋은 성능을 위한)와 구니콘을 설치할 것입니다. +//// -## 구니콘을 유비콘 워커와 함께 실행하기 +//// tab | `uvicorn` -설치 후 구니콘 실행하기: - -<div class="termy"> - -```console -$ gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 - -[19499] [INFO] Starting gunicorn 20.1.0 -[19499] [INFO] Listening at: http://0.0.0.0:80 (19499) -[19499] [INFO] Using worker: uvicorn.workers.UvicornWorker -[19511] [INFO] Booting worker with pid: 19511 -[19513] [INFO] Booting worker with pid: 19513 -[19514] [INFO] Booting worker with pid: 19514 -[19515] [INFO] Booting worker with pid: 19515 -[19511] [INFO] Started server process [19511] -[19511] [INFO] Waiting for application startup. -[19511] [INFO] Application startup complete. -[19513] [INFO] Started server process [19513] -[19513] [INFO] Waiting for application startup. -[19513] [INFO] Application startup complete. -[19514] [INFO] Started server process [19514] -[19514] [INFO] Waiting for application startup. -[19514] [INFO] Application startup complete. -[19515] [INFO] Started server process [19515] -[19515] [INFO] Waiting for application startup. -[19515] [INFO] Application startup complete. -``` - -</div> - -각 옵션이 무엇을 의미하는지 살펴봅시다: - -* 이것은 유비콘과 똑같은 문법입니다. `main`은 파이썬 모듈 네임 "`main`"을 의미하므로 `main.py`파일을 뜻합니다. 그리고 `app`은 **FastAPI** 어플리케이션이 들어 있는 변수의 이름입니다. - * `main:app`이 파이썬의 `import` 문법과 흡사한 면이 있다는 걸 알 수 있습니다: - - ```Python - from main import app - ``` - - * 곧, `main:app`안에 있는 콜론의 의미는 파이썬에서 `from main import app`에서의 `import`와 같습니다. -* `--workers`: 사용할 워커 프로세스의 개수이며 숫자만큼의 유비콘 워커를 실행합니다. 이 예제에서는 4개의 워커를 실행합니다. -* `--worker-class`: 워커 프로세스에서 사용하기 위한 구니콘과 호환되는 워커클래스. - * 이런식으로 구니콘이 import하여 사용할 수 있는 클래스를 전달해줍니다: - - ```Python - import uvicorn.workers.UvicornWorker - ``` - -* `--bind`: 구니콘이 관찰할 IP와 포트를 의미합니다. 콜론 (`:`)을 사용하여 IP와 포트를 구분합니다. - * 만약에 `--bind 0.0.0.0:80` (구니콘 옵션) 대신 유비콘을 직접 실행하고 싶다면 `--host 0.0.0.0`과 `--port 80`을 사용해야 합니다. - -출력에서 각 프로세스에 대한 **PID** (process ID)를 확인할 수 있습니다. (단순한 숫자입니다) - -출력 내용: - -* 구니콘 **프로세스 매니저**는 PID `19499`로 실행됩니다. (직접 실행할 경우 숫자가 다를 수 있습니다) -* 다음으로 `Listening at: http://0.0.0.0:80`을 시작합니다. -* 그런 다음 사용해야할 `uvicorn.workers.UvicornWorker`의 워커클래스를 탐지합니다. -* 그리고 PID `19511`, `19513`, `19514`, 그리고 `19515`를 가진 **4개의 워커**를 실행합니다. - - -또한 구니콘은 워커의 수를 유지하기 위해 **죽은 프로세스**를 관리하고 **재시작**하는 작업을 책임집니다. 이것은 이번 장 상단 목록의 **재시작** 개념을 부분적으로 도와주는 것입니다. - -그럼에도 불구하고 필요할 경우 외부에서 **구니콘을 재시작**하고, 혹은 **서버를 시작할 때 실행**할 수 있도록 하고 싶어할 것입니다. - -## 유비콘과 워커 - -유비콘은 몇 개의 **워커 프로세스**와 함께 실행할 수 있는 선택지가 있습니다. - -그럼에도 불구하고, 유비콘은 워커 프로세스를 다루는 데에 있어서 구니콘보다 더 제한적입니다. 따라서 이 수준(파이썬 수준)의 프로세스 관리자를 사용하려면 구니콘을 프로세스 관리자로 사용하는 것이 좋습니다. - -보통 이렇게 실행할 수 있습니다: +`uvicorn` 명령을 직접 사용하는 편이 좋다면: <div class="termy"> @@ -148,36 +105,35 @@ $ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 </div> -새로운 옵션인 `--workers`은 유비콘에게 4개의 워커 프로세스를 사용한다고 알려줍니다. +//// -각 프로세스의 **PID**를 확인할 수 있습니다. `27365`는 상위 프로세스(**프로세스 매니저**), 그리고 각각의 워커프로세스는 `27368`, `27369`, `27370`, 그리고 `27367`입니다. +여기서 새로운 옵션은 `--workers`뿐이며, Uvicorn에게 워커 프로세스 4개를 시작하라고 알려줍니다. -## 배포 개념들 +또한 각 프로세스의 **PID**도 확인할 수 있는데, 상위 프로세스(이것이 **프로세스 관리자**)의 PID는 `27365`이고, 각 워커 프로세스의 PID는 `27368`, `27369`, `27370`, `27367`입니다. -여기에서는 **유비콘 워커 프로세스**를 관리하는 **구니콘**(또는 유비콘)을 사용하여 애플리케이션을 **병렬화**하고, CPU **멀티 코어**의 장점을 활용하고, **더 많은 요청**을 처리할 수 있는 방법을 살펴보았습니다. +## 배포 개념들 { #deployment-concepts } -워커를 사용하는 것은 배포 개념 목록에서 주로 **복제본** 부분과 **재시작**에 약간 도움이 되지만 다른 배포 개념들도 다루어야 합니다: +여기서는 여러 **워커**를 사용해 애플리케이션 실행을 **병렬화**하고, CPU의 **다중 코어**를 활용하며, **더 많은 요청**을 제공할 수 있는 방법을 살펴봤습니다. + +위의 배포 개념 목록에서 워커를 사용하는 것은 주로 **복제** 부분에 도움이 되고, **재시작**에도 약간 도움이 되지만, 나머지 항목들도 여전히 신경 써야 합니다: * **보안 - HTTPS** -* **서버 시작과 동시에 실행하기** +* **서버 시작 시 실행** * ***재시작*** -* 복제본 (실행 중인 프로세스의 숫자) +* 복제(실행 중인 프로세스 수) * **메모리** -* **시작하기 전의 여러 단계들** +* **시작하기 전의 이전 단계** +## 컨테이너와 도커 { #containers-and-docker } -## 컨테이너와 도커 +다음 장인 [컨테이너에서의 FastAPI - 도커](docker.md){.internal-link target=_blank}에서는 다른 **배포 개념들**을 처리하기 위해 사용할 수 있는 몇 가지 전략을 설명하겠습니다. -다음 장인 [FastAPI와 컨테이너 - 도커](docker.md){.internal-link target=_blank}에서 다른 **배포 개념들**을 다루는 전략들을 알려드리겠습니다. +단일 Uvicorn 프로세스를 실행하기 위해, **처음부터 여러분만의 이미지를 직접 빌드**하는 방법을 보여드리겠습니다. 이는 간단한 과정이며, **Kubernetes** 같은 분산 컨테이너 관리 시스템을 사용할 때 아마도 이렇게 하고 싶을 것입니다. -또한 간단한 케이스에서 사용할 수 있는, **구니콘과 유비콘 워커**가 포함돼 있는 **공식 도커 이미지**와 함께 몇 가지 기본 구성을 보여드리겠습니다. +## 요약 { #recap } -그리고 단일 유비콘 프로세스(구니콘 없이)를 실행할 수 있도록 **사용자 자신의 이미지를 처음부터 구축**하는 방법도 보여드리겠습니다. 이는 간단한 과정이며, **쿠버네티스**와 같은 분산 컨테이너 관리 시스템을 사용할 때 수행할 작업입니다. +`fastapi` 또는 `uvicorn` 명령에서 `--workers` CLI 옵션을 사용해 여러 워커 프로세스를 실행하면, **멀티 코어 CPU**를 활용해 **여러 프로세스를 병렬로 실행**할 수 있습니다. -## 요약 +다른 배포 개념들을 직접 처리하면서 **자체 배포 시스템**을 구축하는 경우, 이러한 도구와 아이디어를 활용할 수 있습니다. -당신은 **구니콘**(또는 유비콘)을 유비콘 워커와 함께 프로세스 관리자로 사용하여 **멀티-코어 CPU**를 활용하는 **멀티 프로세스를 병렬로 실행**할 수 있습니다. - -다른 배포 개념을 직접 다루면서 **자신만의 배포 시스템**을 구성하는 경우 이러한 도구와 개념들을 활용할 수 있습니다. - -다음 장에서 컨테이너(예: 도커 및 쿠버네티스)와 함께하는 **FastAPI**에 대해 배워보세요. 이러한 툴에는 다른 **배포 개념**들을 간단히 해결할 수 있는 방법이 있습니다. ✨ +다음 장에서 컨테이너(예: Docker 및 Kubernetes)와 함께 사용하는 **FastAPI**에 대해 알아보세요. 해당 도구들이 다른 **배포 개념들**도 간단히 해결하는 방법이 있다는 것을 확인할 수 있습니다. ✨ diff --git a/docs/ko/docs/deployment/versions.md b/docs/ko/docs/deployment/versions.md index 559a892ab..173ba925c 100644 --- a/docs/ko/docs/deployment/versions.md +++ b/docs/ko/docs/deployment/versions.md @@ -1,94 +1,93 @@ -# FastAPI 버전들에 대하여 +# FastAPI 버전들에 대하여 { #about-fastapi-versions } -**FastAPI** 는 이미 많은 응용 프로그램과 시스템들을 만드는데 사용되고 있습니다. 그리고 100%의 테스트 정확성을 가지고 있습니다. 하지만 이것은 아직까지도 빠르게 발전하고 있습니다. +**FastAPI**는 이미 많은 애플리케이션과 시스템에서 프로덕션으로 사용되고 있습니다. 그리고 테스트 커버리지는 100%로 유지됩니다. 하지만 개발은 여전히 빠르게 진행되고 있습니다. -새로운 특징들이 빈번하게 추가되고, 오류들이 지속적으로 수정되고 있습니다. 그리고 코드가 계속적으로 향상되고 있습니다. +새로운 기능이 자주 추가되고, 버그가 규칙적으로 수정되며, 코드는 계속해서 지속적으로 개선되고 있습니다. -이것이 아직도 최신 버전이 `0.x.x`인 이유입니다. 이것은 각각의 버전들이 잠재적으로 변할 수 있다는 것을 보여줍니다. 이는 <a href="https://semver.org/" class="external-link" target="_blank">유의적 버전</a> 관습을 따릅니다. +그래서 현재 버전이 아직 `0.x.x`인 것입니다. 이는 각 버전이 잠재적으로 하위 호환성이 깨지는 변경을 포함할 수 있음을 반영합니다. 이는 <a href="https://semver.org/" class="external-link" target="_blank">Semantic Versioning</a> 관례를 따릅니다. -지금 바로 **FastAPI**로 응용 프로그램을 만들 수 있습니다. 이때 (아마 지금까지 그래 왔던 것처럼), 사용하는 버전이 코드와 잘 맞는지 확인해야합니다. +지금 바로 **FastAPI**로 프로덕션 애플리케이션을 만들 수 있습니다(그리고 아마도 한동안 그렇게 해오셨을 것입니다). 다만 나머지 코드와 함께 올바르게 동작하는 버전을 사용하고 있는지 확인하기만 하면 됩니다. -## `fastapi` 버전을 표시 +## `fastapi` 버전을 고정하기 { #pin-your-fastapi-version } -가장 먼저 해야할 것은 응용 프로그램이 잘 작동하는 가장 최신의 구체적인 **FastAPI** 버전을 표시하는 것입니다. +가장 먼저 해야 할 일은 여러분의 애플리케이션에서 올바르게 동작하는 것으로 알고 있는 **FastAPI**의 최신 구체 버전에 맞춰 사용 중인 버전을 "고정(pin)"하는 것입니다. -예를 들어, 응용 프로그램에 `0.45.0` 버전을 사용했다고 가정합니다. +예를 들어, 앱에서 `0.112.0` 버전을 사용하고 있다고 가정해 보겠습니다. -만약에 `requirements.txt` 파일을 사용했다면, 다음과 같이 버전을 명세할 수 있습니다: +`requirements.txt` 파일을 사용한다면 다음과 같이 버전을 지정할 수 있습니다: ```txt -fastapi==0.45.0 +fastapi[standard]==0.112.0 ``` -이것은 `0.45.0` 버전을 사용했다는 것을 의미합니다. +이는 정확히 `0.112.0` 버전을 사용한다는 의미입니다. -또는 다음과 같이 표시할 수 있습니다: +또는 다음과 같이 고정할 수도 있습니다: + +```txt +fastapi[standard]>=0.112.0,<0.113.0 +``` + +이는 `0.112.0` 이상이면서 `0.113.0` 미만의 버전을 사용한다는 의미입니다. 예를 들어 `0.112.2` 버전도 허용됩니다. + +`uv`, Poetry, Pipenv 등 다른 도구로 설치를 관리한다면, 모두 패키지의 특정 버전을 정의할 수 있는 방법을 제공합니다. + +## 이용 가능한 버전들 { #available-versions } + +사용 가능한 버전(예: 현재 최신 버전이 무엇인지 확인하기 위해)은 [Release Notes](../release-notes.md){.internal-link target=_blank}에서 확인할 수 있습니다. + +## 버전들에 대해 { #about-versions } + +Semantic Versioning 관례에 따르면, `1.0.0` 미만의 어떤 버전이든 잠재적으로 하위 호환성이 깨지는 변경을 추가할 수 있습니다. + +FastAPI는 또한 "PATCH" 버전 변경은 버그 수정과 하위 호환성이 깨지지 않는 변경을 위한 것이라는 관례를 따릅니다. + +/// tip | 팁 + +"PATCH"는 마지막 숫자입니다. 예를 들어 `0.2.3`에서 PATCH 버전은 `3`입니다. + +/// + +따라서 다음과 같이 버전을 고정할 수 있어야 합니다: ```txt fastapi>=0.45.0,<0.46.0 ``` -이것은 `0.45.0` 버전과 같거나 높으면서 `0.46.0` 버전 보다는 낮은 버전을 사용했다는 것을 의미합니다. 예를 들어, `0.45.2` 버전과 같은 경우는 해당 조건을 만족합니다. - -만약에 Poetry, Pipenv, 또는 그밖의 다양한 설치 도구를 사용한다면, 패키지에 구체적인 버전을 정의할 수 있는 방법을 가지고 있을 것입니다. - -## 이용가능한 버전들 - -[Release Notes](../release-notes.md){.internal-link target=_blank}를 통해 사용할 수 있는 버전들을 확인할 수 있습니다.(예를 들어, 가장 최신의 버전을 확인할 수 있습니다.) - - -## 버전들에 대해 - -유의적 버전 관습을 따라서, `1.0.0` 이하의 모든 버전들은 잠재적으로 급변할 수 있습니다. - -FastAPI는 오류를 수정하고, 일반적인 변경사항을 위해 "패치"버전의 관습을 따릅니다. +하위 호환성이 깨지는 변경과 새로운 기능은 "MINOR" 버전에 추가됩니다. /// tip | 팁 -여기서 말하는 "패치"란 버전의 마지막 숫자로, 예를 들어 `0.2.3` 버전에서 "패치"는 `3`을 의미합니다. +"MINOR"는 가운데 숫자입니다. 예를 들어 `0.2.3`에서 MINOR 버전은 `2`입니다. /// -따라서 다음과 같이 버전을 표시할 수 있습니다: +## FastAPI 버전 업그레이드하기 { #upgrading-the-fastapi-versions } + +앱에 테스트를 추가해야 합니다. + +**FastAPI**에서는 매우 쉽습니다(Starlette 덕분에). 문서를 확인해 보세요: [Testing](../tutorial/testing.md){.internal-link target=_blank} + +테스트를 갖춘 뒤에는 **FastAPI** 버전을 더 최신 버전으로 업그레이드하고, 테스트를 실행하여 모든 코드가 올바르게 동작하는지 확인하세요. + +모든 것이 동작하거나 필요한 변경을 한 뒤 모든 테스트가 통과한다면, `fastapi`를 그 새로운 최신 버전으로 고정할 수 있습니다. + +## Starlette에 대해 { #about-starlette } + +`starlette`의 버전은 고정하지 않는 것이 좋습니다. + +서로 다른 **FastAPI** 버전은 Starlette의 특정한 더 새로운 버전을 사용하게 됩니다. + +따라서 **FastAPI**가 올바른 Starlette 버전을 사용하도록 그냥 두면 됩니다. + +## Pydantic에 대해 { #about-pydantic } + +Pydantic은 자체 테스트에 **FastAPI**에 대한 테스트도 포함하고 있으므로, Pydantic의 새 버전(`1.0.0` 초과)은 항상 FastAPI와 호환됩니다. + +여러분에게 맞는 `1.0.0` 초과의 어떤 Pydantic 버전으로든 고정할 수 있습니다. + +예를 들어: ```txt -fastapi>=0.45.0,<0.46.0 -``` - -수정된 사항과 새로운 요소들이 "마이너" 버전에 추가되었습니다. - -/// tip | 팁 - -"마이너"란 버전 넘버의 가운데 숫자로, 예를 들어서 `0.2.3`의 "마이너" 버전은 `2`입니다. - -/// - -## FastAPI 버전의 업그레이드 - -응용 프로그램을 검사해야합니다. - -(Starlette 덕분에), **FastAPI** 를 이용하여 굉장히 쉽게 할 수 있습니다. [Testing](../tutorial/testing.md){.internal-link target=_blank}문서를 확인해 보십시오: - -검사를 해보고 난 후에, **FastAPI** 버전을 더 최신으로 업그레이드 할 수 있습니다. 그리고 코드들이 테스트에 정상적으로 작동하는지 확인을 해야합니다. - -만약에 모든 것이 정상 작동하거나 필요한 부분을 변경하고, 모든 검사를 통과한다면, 새로운 버전의 `fastapi`를 표시할 수 있습니다. - -## Starlette에 대해 - -`starlette`의 버전은 표시할 수 없습니다. - -서로다른 버전의 **FastAPI**가 구체적이고 새로운 버전의 Starlette을 사용할 것입니다. - -그러므로 **FastAPI**가 알맞은 Starlette 버전을 사용하도록 하십시오. - -## Pydantic에 대해 - -Pydantic은 **FastAPI** 를 위한 검사를 포함하고 있습니다. 따라서, 새로운 버전의 Pydantic(`1.0.0`이상)은 항상 FastAPI와 호환됩니다. - -작업을 하고 있는 `1.0.0` 이상의 모든 버전과 `2.0.0` 이하의 Pydantic 버전을 표시할 수 있습니다. - -예를 들어 다음과 같습니다: - -```txt -pydantic>=1.2.0,<2.0.0 +pydantic>=2.7.0,<3.0.0 ``` diff --git a/docs/ko/docs/environment-variables.md b/docs/ko/docs/environment-variables.md index 1e6af3ceb..dc231acb6 100644 --- a/docs/ko/docs/environment-variables.md +++ b/docs/ko/docs/environment-variables.md @@ -1,4 +1,4 @@ -# 환경 변수 +# 환경 변수 { #environment-variables } /// tip | 팁 @@ -6,11 +6,11 @@ /// -환경 변수는 파이썬 코드의 **바깥**인, **운영 체제**에 존재하는 변수입니다. 파이썬 코드나 다른 프로그램에서 읽을 수 있습니다. +환경 변수(또는 "**env var**"라고도 합니다)는 파이썬 코드의 **바깥**인, **운영 체제**에 존재하는 변수이며, 파이썬 코드(또는 다른 프로그램에서도)에서 읽을 수 있습니다. -환경 변수는 애플리케이션 **설정**을 처리하거나, 파이썬의 **설치** 과정의 일부로 유용합니다. +환경 변수는 애플리케이션 **설정**을 처리하거나, 파이썬의 **설치** 과정의 일부로 유용할 수 있습니다. -## 환경 변수를 만들고 사용하기 +## 환경 변수를 만들고 사용하기 { #create-and-use-env-vars } 파이썬 없이도, **셸 (터미널)** 에서 환경 변수를 **생성** 하고 사용할 수 있습니다. @@ -50,9 +50,9 @@ Hello Wade Wilson //// -## 파이썬에서 환경 변수 읽기 +## 파이썬에서 env var 읽기 { #read-env-vars-in-python } -파이썬 **바깥**인 터미널에서(다른 도구로도 가능) 환경 변수를 생성도 할 수도 있고, 이를 **파이썬에서 읽을 수 있습니다.** +파이썬 **바깥**인 터미널에서(또는 다른 어떤 방법으로든) 환경 변수를 만들고, 그런 다음 **파이썬에서 읽을 수 있습니다**. 예를 들어 다음과 같은 `main.py` 파일이 있다고 합시다: @@ -67,7 +67,7 @@ print(f"Hello {name} from Python") <a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> 의 두 번째 인자는 반환할 기본값입니다. -여기서는 `"World"`를 넣었기에 기본값으로써 사용됩니다. 넣지 않으면 `None` 이 기본값으로 사용됩니다. +제공하지 않으면 기본값은 `None`이며, 여기서는 사용할 기본값으로 `"World"`를 제공합니다. /// @@ -129,7 +129,7 @@ Hello Wade Wilson from Python 환경변수는 코드 바깥에서 설정될 수 있지만, 코드에서 읽을 수 있고, 나머지 파일과 함께 저장(`git`에 커밋)할 필요가 없으므로, 구성이나 **설정** 에 사용하는 것이 일반적입니다. -**특정 프로그램 호출**에 대해서만 사용할 수 있는 환경 변수를 만들 수도 있습니다. 해당 프로그램에서만 사용할 수 있고, 해당 프로그램이 실행되는 동안만 사용할 수 있습니다. +또한 **특정 프로그램 호출**에 대해서만 사용할 수 있는 환경 변수를 만들 수도 있는데, 해당 프로그램에서만 사용할 수 있고, 해당 프로그램이 실행되는 동안만 사용할 수 있습니다. 그렇게 하려면 프로그램 바로 앞, 같은 줄에 환경 변수를 만들어야 합니다: @@ -157,17 +157,17 @@ Hello World from Python /// -## 타입과 검증 +## 타입과 검증 { #types-and-validation } -이 환경변수들은 오직 **텍스트 문자열**로만 처리할 수 있습니다. 텍스트 문자열은 파이썬 외부에 있으며 다른 프로그램 및 나머지 시스템(Linux, Windows, macOS 등 다른 운영 체제)과 호환되어야 합니다. +이 환경변수들은 오직 **텍스트 문자열**로만 처리할 수 있습니다. 텍스트 문자열은 파이썬 외부에 있으며 다른 프로그램 및 나머지 시스템(그리고 Linux, Windows, macOS 같은 서로 다른 운영 체제에서도)과 호환되어야 합니다. 즉, 파이썬에서 환경 변수로부터 읽은 **모든 값**은 **`str`**이 되고, 다른 타입으로의 변환이나 검증은 코드에서 수행해야 합니다. -**애플리케이션 설정**을 처리하기 위한 환경 변수 사용에 대한 자세한 내용은 [고급 사용자 가이드 - 설정 및 환경 변수](./advanced/settings.md){.internal-link target=\_blank} 에서 확인할 수 있습니다. +**애플리케이션 설정**을 처리하기 위한 환경 변수 사용에 대한 자세한 내용은 [고급 사용자 가이드 - 설정 및 환경 변수](./advanced/settings.md){.internal-link target=_blank} 에서 확인할 수 있습니다. -## `PATH` 환경 변수 +## `PATH` 환경 변수 { #path-environment-variable } -**`PATH`**라고 불리는, **특별한** 환경변수가 있습니다. 운영체제(Linux, Windows, macOS 등)에서 실행할 프로그램을 찾기위해 사용됩니다. +**`PATH`**라고 불리는, **특별한** 환경변수가 있습니다. 운영체제(Linux, macOS, Windows)에서 실행할 프로그램을 찾기위해 사용됩니다. 변수 `PATH`의 값은 Linux와 macOS에서는 콜론 `:`, Windows에서는 세미콜론 `;`으로 구분된 디렉토리로 구성된 긴 문자열입니다. @@ -181,11 +181,11 @@ Hello World from Python 이는 시스템이 다음 디렉토리에서 프로그램을 찾아야 함을 의미합니다: -- `/usr/local/bin` -- `/usr/bin` -- `/bin` -- `/usr/sbin` -- `/sbin` +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` //// @@ -197,9 +197,9 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3 이는 시스템이 다음 디렉토리에서 프로그램을 찾아야 함을 의미합니다: -- `C:\Program Files\Python312\Scripts` -- `C:\Program Files\Python312` -- `C:\Windows\System32` +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` //// @@ -209,7 +209,7 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3 찾으면 **사용합니다**. 그렇지 않으면 **다른 디렉토리**에서 계속 찾습니다. -### 파이썬 설치와 `PATH` 업데이트 +### 파이썬 설치와 `PATH` 업데이트 { #installing-python-and-updating-the-path } 파이썬을 설치할 때, 아마 `PATH` 환경 변수를 업데이트 할 것이냐고 물어봤을 겁니다. @@ -285,13 +285,13 @@ $ C:\opt\custompython\bin\python //// -이 정보는 [가상 환경](virtual-environments.md){.internal-link target=\_blank} 에 대해 알아볼 때 유용할 것입니다. +이 정보는 [가상 환경](virtual-environments.md){.internal-link target=_blank} 에 대해 알아볼 때 유용할 것입니다. -## 결론 +## 결론 { #conclusion } -이 문서를 읽고 **환경 변수**가 무엇이고 파이썬에서 어떻게 사용하는지 기본적으로 이해하셨을 겁니다. +이 문서를 통해 **환경 변수**가 무엇이고 파이썬에서 어떻게 사용하는지 기본적으로 이해하셨을 겁니다. -또한 <a href="https://ko.wikipedia.org/wiki/환경_변수" class="external-link" target="_blank">환경 변수에 대한 위키피디아(한국어)</a>에서 이에 대해 자세히 알아볼 수 있습니다. +또한 <a href="https://en.wikipedia.org/wiki/Environment_variable" class="external-link" target="_blank">환경 변수에 대한 위키피디아</a>에서 이에 대해 자세히 알아볼 수 있습니다. 많은 경우에서, 환경 변수가 어떻게 유용하고 적용 가능한지 바로 명확하게 알 수는 없습니다. 하지만 개발할 때 다양한 시나리오에서 계속 나타나므로 이에 대해 아는 것이 좋습니다. diff --git a/docs/ko/docs/how-to/conditional-openapi.md b/docs/ko/docs/how-to/conditional-openapi.md index 79c7f0dd2..16e683366 100644 --- a/docs/ko/docs/how-to/conditional-openapi.md +++ b/docs/ko/docs/how-to/conditional-openapi.md @@ -1,46 +1,41 @@ -# 조건부적인 OpenAPI +# 조건부 OpenAPI { #conditional-openapi } -필요한 경우, 설정 및 환경 변수를 사용하여 환경에 따라 조건부로 OpenAPI를 구성하고 완전히 비활성화할 수도 있습니다. +필요한 경우, 설정 및 환경 변수를 사용하여 환경에 따라 OpenAPI를 조건부로 구성하고 완전히 비활성화할 수도 있습니다. -## 보안, API 및 docs에 대해서 +## 보안, API 및 docs에 대해서 { #about-security-apis-and-docs } 프로덕션에서, 문서화된 사용자 인터페이스(UI)를 숨기는 것이 API를 보호하는 방법이 *되어서는 안 됩니다*. -이는 API에 추가적인 보안을 제공하지 않으며, *경로 작업*은 여전히 동일한 위치에서 사용 할 수 있습니다. +이는 API에 추가적인 보안을 제공하지 않으며, *경로 처리*는 여전히 동일한 위치에서 사용 할 수 있습니다. 코드에 보안 결함이 있다면, 그 결함은 여전히 존재할 것입니다. -문서를 숨기는 것은 API와 상호작용하는 방법을 이해하기 어렵게 만들며, 프로덕션에서 디버깅을 더 어렵게 만들 수 있습니다. 이는 단순히 <a href="https://en.wikipedia.org/wiki/Security_through_obscurity" class="external-link" target="_blank">'모호성에 의한 보안'</a>의 한 형태로 간주될 수 있습니다. +문서를 숨기는 것은 API와 상호작용하는 방법을 이해하기 어렵게 만들며, 프로덕션에서 디버깅을 더 어렵게 만들 수 있습니다. 이는 단순히 <a href="https://en.wikipedia.org/wiki/Security_through_obscurity" class="external-link" target="_blank">Security through obscurity</a>의 한 형태로 간주될 수 있습니다. API를 보호하고 싶다면, 예를 들어 다음과 같은 더 나은 방법들이 있습니다: -* 요청 본문과 응답에 대해 잘 정의된 Pydantic 모델을 사용하도록 하세요. - +* 요청 본문과 응답에 대해 잘 정의된 Pydantic 모델이 있는지 확인하세요. * 종속성을 사용하여 필요한 권한과 역할을 구성하세요. - -* 평문 비밀번호를 절대 저장하지 말고, 오직 암호화된 비밀번호만 저장하세요. - -* Passlib과 JWT 토큰과 같은 잘 알려진 암호화 도구들을 구현하고 사용하세요. - +* 평문 비밀번호를 절대 저장하지 말고, 비밀번호 해시만 저장하세요. +* pwdlib와 JWT 토큰 등과 같은 잘 알려진 암호화 도구들을 구현하고 사용하세요. * 필요한 곳에 OAuth2 범위를 사용하여 더 세분화된 권한 제어를 추가하세요. +* ...등등. -* 등등.... +그럼에도 불구하고, 특정 환경(예: 프로덕션)에서 또는 환경 변수의 설정에 따라 API docs를 비활성화해야 하는 매우 특정한 사용 사례가 있을 수 있습니다. -그럼에도 불구하고, 특정 환경(예: 프로덕션)에서 또는 환경 변수의 설정에 따라 API 문서를 비활성화해야 하는 매우 특정한 사용 사례가 있을 수 있습니다. +## 설정 및 환경변수의 조건부 OpenAPI { #conditional-openapi-from-settings-and-env-vars } -## 설정 및 환경변수의 조건부 OpenAPI - -동일한 Pydantic 설정을 사용하여 생성된 OpenAPI 및 문서 UI를 쉽게 구성할 수 있습니다. +동일한 Pydantic 설정을 사용하여 생성된 OpenAPI 및 docs UI를 쉽게 구성할 수 있습니다. 예를 들어: -{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *} +{* ../../docs_src/conditional_openapi/tutorial001_py39.py hl[6,11] *} 여기서 `openapi_url` 설정을 기본값인 `"/openapi.json"`으로 선언합니다. 그런 뒤, 우리는 `FastAPI` 앱을 만들 때 그것을 사용합니다. -환경 변수 `OPENAPI_URL`을 빈 문자열로 설정하여 OpenAPI(문서 UI 포함)를 비활성화할 수도 있습니다. 예를 들어: +그런 다음 환경 변수 `OPENAPI_URL`을 빈 문자열로 설정하여 OpenAPI(UI docs 포함)를 비활성화할 수도 있습니다. 예를 들어: <div class="termy"> diff --git a/docs/ko/docs/how-to/configure-swagger-ui.md b/docs/ko/docs/how-to/configure-swagger-ui.md index 5a57342cf..174f976f6 100644 --- a/docs/ko/docs/how-to/configure-swagger-ui.md +++ b/docs/ko/docs/how-to/configure-swagger-ui.md @@ -1,4 +1,4 @@ -# Swagger UI 구성 +# Swagger UI 구성 { #configure-swagger-ui } 추가적인 <a href="https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/" class="external-link" target="_blank">Swagger UI 매개변수</a>를 구성할 수 있습니다. @@ -8,7 +8,7 @@ FastAPI는 이 구성을 **JSON** 형식으로 변환하여 JavaScript와 호환되도록 합니다. 이는 Swagger UI에서 필요로 하는 형식입니다. -## 구문 강조 비활성화 +## 구문 강조 비활성화 { #disable-syntax-highlighting } 예를 들어, Swagger UI에서 구문 강조 기능을 비활성화할 수 있습니다. @@ -18,41 +18,41 @@ FastAPI는 이 구성을 **JSON** 형식으로 변환하여 JavaScript와 호환 그러나 `syntaxHighlight`를 `False`로 설정하여 구문 강조 기능을 비활성화할 수 있습니다: -{* ../../docs_src/configure_swagger_ui/tutorial001.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial001_py39.py hl[3] *} ...그럼 Swagger UI에서 더 이상 구문 강조 기능이 표시되지 않습니다: <img src="/img/tutorial/extending-openapi/image03.png"> -## 테마 변경 +## 테마 변경 { #change-the-theme } 동일한 방식으로 `"syntaxHighlight.theme"` 키를 사용하여 구문 강조 테마를 설정할 수 있습니다 (중간에 점이 포함된 것을 참고하십시오). -{* ../../docs_src/configure_swagger_ui/tutorial002.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial002_py39.py hl[3] *} 이 설정은 구문 강조 색상 테마를 변경합니다: <img src="/img/tutorial/extending-openapi/image04.png"> -## 기본 Swagger UI 매개변수 변경 +## 기본 Swagger UI 매개변수 변경 { #change-default-swagger-ui-parameters } FastAPI는 대부분의 사용 사례에 적합한 몇 가지 기본 구성 매개변수를 포함하고 있습니다. 기본 구성에는 다음이 포함됩니다: -{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *} +{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} `swagger_ui_parameters` 인수에 다른 값을 설정하여 이러한 기본값 중 일부를 재정의할 수 있습니다. 예를 들어, `deepLinking`을 비활성화하려면 `swagger_ui_parameters`에 다음 설정을 전달할 수 있습니다: -{* ../../docs_src/configure_swagger_ui/tutorial003.py hl[3] *} +{* ../../docs_src/configure_swagger_ui/tutorial003_py39.py hl[3] *} -## 기타 Swagger UI 매개변수 +## 기타 Swagger UI 매개변수 { #other-swagger-ui-parameters } -사용할 수 있는 다른 모든 구성 옵션을 확인하려면, Swagger UI 매개변수에 대한 공식 <a href="https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/" class="external-link" target="_blank">문서</a>를 참조하십시오. +사용할 수 있는 다른 모든 구성 옵션을 확인하려면, Swagger UI 매개변수에 대한 공식 <a href="https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/" class="external-link" target="_blank">Swagger UI 매개변수 문서</a>를 참조하십시오. -## JavaScript 전용 설정 +## JavaScript 전용 설정 { #javascript-only-settings } Swagger UI는 **JavaScript 전용** 객체(예: JavaScript 함수)로 다른 구성을 허용하기도 합니다. @@ -67,4 +67,4 @@ presets: [ 이들은 문자열이 아닌 **JavaScript** 객체이므로 Python 코드에서 직접 전달할 수 없습니다. -이와 같은 JavaScript 전용 구성을 사용해야 하는 경우, 위의 방법 중 하나를 사용하여 모든 Swagger UI 경로 작업을 재정의하고 필요한 JavaScript를 수동으로 작성할 수 있습니다. +이와 같은 JavaScript 전용 구성을 사용해야 하는 경우, 위의 방법 중 하나를 사용할 수 있습니다. Swagger UI *경로 처리*를 모두 재정의하고 필요한 JavaScript를 수동으로 작성하세요. diff --git a/docs/ko/docs/index.md b/docs/ko/docs/index.md index b6b4765da..0c317f296 100644 --- a/docs/ko/docs/index.md +++ b/docs/ko/docs/index.md @@ -1,11 +1,11 @@ -# FastAPI +# FastAPI { #fastapi } <style> .md-content .md-typeset h1 { display: none; } </style> <p align="center"> - <a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> + <a href="https://fastapi.tiangolo.com/ko"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> </p> <p align="center"> <em>FastAPI 프레임워크, 고성능, 간편한 학습, 빠른 코드 작성, 준비된 프로덕션</em> @@ -27,7 +27,7 @@ --- -**문서**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a> +**문서**: <a href="https://fastapi.tiangolo.com/ko" target="_blank">https://fastapi.tiangolo.com</a> **소스 코드**: <a href="https://github.com/fastapi/fastapi" target="_blank">https://github.com/fastapi/fastapi</a> @@ -37,36 +37,41 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 주요 특징으로: -* **빠름**: (Starlette과 Pydantic 덕분에) **NodeJS** 및 **Go**와 대등할 정도로 매우 높은 성능. [사용 가능한 가장 빠른 파이썬 프레임워크 중 하나](#_11). - +* **빠름**: (Starlette과 Pydantic 덕분에) **NodeJS** 및 **Go**와 대등할 정도로 매우 높은 성능. [사용 가능한 가장 빠른 파이썬 프레임워크 중 하나](#performance). * **빠른 코드 작성**: 약 200%에서 300%까지 기능 개발 속도 증가. * * **적은 버그**: 사람(개발자)에 의한 에러 약 40% 감소. * * **직관적**: 훌륭한 편집기 지원. 모든 곳에서 <abbr title="also known as auto-complete, autocompletion, IntelliSense">자동완성</abbr>. 적은 디버깅 시간. * **쉬움**: 쉽게 사용하고 배우도록 설계. 적은 문서 읽기 시간. * **짧음**: 코드 중복 최소화. 각 매개변수 선언의 여러 기능. 적은 버그. * **견고함**: 준비된 프로덕션 용 코드를 얻으십시오. 자동 대화형 문서와 함께. -* **표준 기반**: API에 대한 (완전히 호환되는) 개방형 표준 기반: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (이전에 Swagger로 알려졌던) 및 <a href="http://json-schema.org/" class="external-link" target="_blank">JSON 스키마</a>. +* **표준 기반**: API에 대한 (완전히 호환되는) 개방형 표준 기반: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (이전에 Swagger로 알려졌던) 및 <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. <small>* 내부 개발팀의 프로덕션 애플리케이션을 빌드한 테스트에 근거한 측정</small> -## 골드 스폰서 +## 스폰서 { #sponsors } <!-- sponsors --> -{% if sponsors %} +### 키스톤 스폰서 { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor -%} + +### 골드 및 실버 스폰서 { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> {% endfor -%} {%- for sponsor in sponsors.silver -%} <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> {% endfor %} -{% endif %} <!-- /sponsors --> -<a href="https://fastapi.tiangolo.com/fastapi-people/#sponsors" class="external-link" target="_blank">다른 스폰서</a> +<a href="https://fastapi.tiangolo.com/ko/fastapi-people/#sponsors" class="external-link" target="_blank">다른 스폰서</a> -## 의견들 +## 의견들 { #opinions } "_[...] 저는 요즘 **FastAPI**를 많이 사용하고 있습니다. [...] 사실 우리 팀의 **마이크로소프트 ML 서비스** 전부를 바꿀 계획입니다. 그중 일부는 핵심 **Windows**와 몇몇의 **Office** 제품들이 통합되고 있습니다._" @@ -94,7 +99,7 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 "_솔직히, 당신이 만든 것은 매우 견고하고 세련되어 보입니다. 여러 면에서 **Hug**가 이렇게 되었으면 합니다 - 그걸 만든 누군가를 보는 것은 많은 영감을 줍니다._" -<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="http://www.hug.rest/" target="_blank">Hug</a> 제작자</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> +<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://github.com/hugapi/hug" target="_blank">Hug</a> 제작자</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> --- @@ -106,50 +111,54 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 --- -## **Typer**, FastAPI의 CLI +"_프로덕션 Python API를 만들고자 한다면, 저는 **FastAPI**를 강력히 추천합니다. **아름답게 설계**되었고, **사용이 간단**하며, **확장성이 매우 뛰어나**고, 우리의 API 우선 개발 전략에서 **핵심 구성 요소**가 되었으며 Virtual TAC Engineer 같은 많은 자동화와 서비스를 이끌고 있습니다._" + +<div style="text-align: right; margin-right: 10%;">Deon Pillsbury - <strong>Cisco</strong> <a href="https://www.linkedin.com/posts/deonpillsbury_cisco-cx-python-activity-6963242628536487936-trAp/" target="_blank"><small>(ref)</small></a></div> + +--- + +## FastAPI 미니 다큐멘터리 { #fastapi-mini-documentary } + +2025년 말에 공개된 <a href="https://www.youtube.com/watch?v=mpR8ngthqiE" class="external-link" target="_blank">FastAPI 미니 다큐멘터리</a>가 있습니다. 온라인에서 시청할 수 있습니다: + +<a href="https://www.youtube.com/watch?v=mpR8ngthqiE" target="_blank"><img src="https://fastapi.tiangolo.com/img/fastapi-documentary.jpg" alt="FastAPI Mini Documentary"></a> + +## **Typer**, CLI를 위한 FastAPI { #typer-the-fastapi-of-clis } <a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> 웹 API 대신 터미널에서 사용할 <abbr title="Command Line Interface">CLI</abbr> 앱을 만들고 있다면, <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>를 확인해 보십시오. -**Typer**는 FastAPI의 동생입니다. 그리고 **FastAPI의 CLI**가 되기 위해 생겼습니다. ⌨️ 🚀 +**Typer**는 FastAPI의 동생입니다. 그리고 **CLI를 위한 FastAPI**가 되기 위해 생겼습니다. ⌨️ 🚀 -## 요구사항 +## 요구사항 { #requirements } FastAPI는 거인들의 어깨 위에 서 있습니다: * 웹 부분을 위한 <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a>. * 데이터 부분을 위한 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a>. -## 설치 +## 설치 { #installation } + +<a href="https://fastapi.tiangolo.com/ko/virtual-environments/" class="external-link" target="_blank">가상 환경</a>을 생성하고 활성화한 다음 FastAPI를 설치하세요: <div class="termy"> ```console -$ pip install fastapi +$ pip install "fastapi[standard]" ---> 100% ``` </div> -프로덕션을 위해 <a href="http://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a> 또는 <a href="https://github.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>과 같은 ASGI 서버도 필요할 겁니다. +**Note**: 모든 터미널에서 동작하도록 `"fastapi[standard]"`를 따옴표로 감싸 넣었는지 확인하세요. -<div class="termy"> +## 예제 { #example } -```console -$ pip install "uvicorn[standard]" +### 만들기 { #create-it } ----> 100% -``` - -</div> - -## 예제 - -### 만들기 - -* `main.py` 파일을 만드십시오: +다음 내용으로 `main.py` 파일을 만드십시오: ```Python from typing import Union @@ -172,9 +181,9 @@ def read_item(item_id: int, q: Union[str, None] = None): <details markdown="1"> <summary>또는 <code>async def</code> 사용하기...</summary> -여러분의 코드가 `async` / `await`을 사용한다면, `async def`를 사용하십시오. +여러분의 코드가 `async` / `await`을 사용한다면, `async def`를 사용하십시오: -```Python hl_lines="9 14" +```Python hl_lines="9 14" from typing import Union from fastapi import FastAPI @@ -194,22 +203,35 @@ async def read_item(item_id: int, q: Union[str, None] = None): **Note**: -잘 모르겠다면, <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">문서에서 `async`와 `await`</a>에 관한 _"급하세요?"_ 섹션을 확인해 보십시오. +잘 모르겠다면, <a href="https://fastapi.tiangolo.com/ko/async/#in-a-hurry" target="_blank">문서에서 `async`와 `await`</a>에 관한 _"급하세요?"_ 섹션을 확인해 보십시오. </details> -### 실행하기 +### 실행하기 { #run-it } -서버를 실행하십시오: +다음 명령으로 서버를 실행하십시오: <div class="termy"> ```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. ``` @@ -217,17 +239,17 @@ INFO: Application startup complete. </div> <details markdown="1"> -<summary><code>uvicorn main:app --reload</code> 명령에 관하여...</summary> +<summary><code>fastapi dev main.py</code> 명령에 관하여...</summary> -명령 `uvicorn main:app`은 다음을 나타냅니다: +`fastapi dev` 명령은 `main.py` 파일을 읽고, 그 안의 **FastAPI** 앱을 감지한 다음, <a href="https://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a>을 사용해 서버를 시작합니다. -* `main`: `main.py` 파일 (파이썬 "모듈"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: 코드가 변경된 후 서버 재시작하기. 개발환경에서만 사용하십시오. +기본적으로 `fastapi dev`는 로컬 개발을 위해 auto-reload가 활성화된 상태로 시작됩니다. + +자세한 내용은 <a href="https://fastapi.tiangolo.com/ko/fastapi-cli/" target="_blank">FastAPI CLI 문서</a>에서 확인할 수 있습니다. </details> -### 확인하기 +### 확인하기 { #check-it } 브라우저로 <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>를 열어보십시오. @@ -241,10 +263,10 @@ INFO: Application startup complete. * _경로_ `/` 및 `/items/{item_id}`에서 HTTP 요청 받기. * 두 _경로_ 모두 `GET` <em>연산</em>(HTTP _메소드_ 로 알려진)을 받습니다. -* _경로_ `/items/{item_id}`는 _경로 매개변수_ `int`형 이어야 하는 `item_id`를 가지고 있습니다. -* _경로_ `/items/{item_id}`는 선택적인 `str`형 이어야 하는 _경로 매개변수_ `q`를 가지고 있습니다. +* _경로_ `/items/{item_id}`는 `int`형 이어야 하는 _경로 매개변수_ `item_id`를 가지고 있습니다. +* _경로_ `/items/{item_id}`는 선택적인 `str`형 _쿼리 매개변수_ `q`를 가지고 있습니다. -### 대화형 API 문서 +### 대화형 API 문서 { #interactive-api-docs } 이제 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>로 가보십시오. @@ -252,7 +274,7 @@ INFO: Application startup complete. ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### 대안 API 문서 +### 대안 API 문서 { #alternative-api-docs } 그리고 이제 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>로 가봅시다. @@ -260,13 +282,13 @@ INFO: Application startup complete. ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## 예제 심화 +## 예제 업그레이드 { #example-upgrade } -이제 `PUT` 요청에 있는 본문(Body)을 받기 위해 `main.py`를 수정해봅시다. +이제 `PUT` 요청에서 본문을 받기 위해 `main.py` 파일을 수정해봅시다. -Pydantic을 이용해 파이썬 표준 타입으로 본문을 선언합니다. +Pydantic 덕분에 표준 Python 타입을 사용해 본문을 선언합니다. -```Python hl_lines="4 9 10 11 12 25 26 27" +```Python hl_lines="4 9-12 25-27" from typing import Union from fastapi import FastAPI @@ -296,25 +318,25 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -서버가 자동으로 리로딩 할 수 있어야 합니다 (위에서 `uvicorn` 명령에 `--reload`을 추가 했기 때문입니다). +`fastapi dev` 서버는 자동으로 리로딩되어야 합니다. -### 대화형 API 문서 업그레이드 +### 대화형 API 문서 업그레이드 { #interactive-api-docs-upgrade } 이제 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>로 이동합니다. -* 대화형 API 문서가 새 본문과 함께 자동으로 업데이트 합니다: +* 대화형 API 문서는 새 본문을 포함해 자동으로 업데이트됩니다: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* "Try it out" 버튼을 클릭하면, 매개변수를 채울 수 있게 해주고 직접 API와 상호작용 할 수 있습니다: +* "Try it out" 버튼을 클릭하면, 매개변수를 채우고 API와 직접 상호작용할 수 있습니다: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* 그러고 나서 "Execute" 버튼을 누르면, 사용자 인터페이스는 API와 통신하고 매개변수를 전송하며 그 결과를 가져와서 화면에 표시합니다: +* 그런 다음 "Execute" 버튼을 클릭하면, 사용자 인터페이스가 API와 통신하고 매개변수를 전송한 뒤 결과를 받아 화면에 표시합니다: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### 대안 API 문서 업그레이드 +### 대안 API 문서 업그레이드 { #alternative-api-docs-upgrade } 그리고 이제, <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>로 이동합니다. @@ -322,7 +344,7 @@ def update_item(item_id: int, item: Item): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### 요약 +### 요약 { #recap } 요약하면, 여러분은 매개변수의 타입, 본문 등을 함수 매개변수로서 **한번에** 선언했습니다. @@ -351,8 +373,8 @@ item: Item * 타입 검사. * 데이터 검증: * 데이터가 유효하지 않을 때 자동으로 생성하는 명확한 에러. - * 중첩된 JSON 객체에 대한 유효성 검사. -* 입력 데이터 <abbr title="다음으로 알려진: 직렬화, 파싱, 마샬링">변환</abbr>: 네트워크에서 파이썬 데이터 및 타입으로 전송. 읽을 수 있는 것들: + * 깊이 중첩된 JSON 객체에 대한 유효성 검사. +* 입력 데이터 <abbr title="also known as: serialization, parsing, marshalling">변환</abbr>: 네트워크에서 파이썬 데이터 및 타입으로 전송. 읽을 수 있는 것들: * JSON. * 경로 매개변수. * 쿼리 매개변수. @@ -360,7 +382,7 @@ item: Item * 헤더. * 폼(Forms). * 파일. -* 출력 데이터 <abbr title="다음으로 알려진: 직렬화, 파싱, 마샬링">변환</abbr>: 파이썬 데이터 및 타입을 네트워크 데이터로 전환(JSON 형식으로): +* 출력 데이터 <abbr title="also known as: serialization, parsing, marshalling">변환</abbr>: 파이썬 데이터 및 타입을 네트워크 데이터로 전환(JSON 형식으로): * 파이썬 타입 변환 (`str`, `int`, `float`, `bool`, `list`, 등). * `datetime` 객체. * `UUID` 객체. @@ -377,13 +399,13 @@ item: Item * `GET` 및 `PUT` 요청에 `item_id`가 경로에 있는지 검증. * `GET` 및 `PUT` 요청에 `item_id`가 `int` 타입인지 검증. * 그렇지 않다면 클라이언트는 유용하고 명확한 에러를 볼 수 있습니다. -* `GET` 요청에 `q`라는 선택적인 쿼리 매개변수가 검사(`http://127.0.0.1:8000/items/foo?q=somequery`처럼). +* `GET` 요청에 `q`라는 선택적인 쿼리 매개변수가 있는지 검사(`http://127.0.0.1:8000/items/foo?q=somequery`처럼). * `q` 매개변수는 `= None`으로 선언되었기 때문에 선택사항입니다. * `None`이 없다면 필수사항입니다(`PUT`의 경우와 마찬가지로). * `/items/{item_id}`으로의 `PUT` 요청은 본문을 JSON으로 읽음: * `name`을 필수 속성으로 갖고 `str` 형인지 검사. - * `price`을 필수 속성으로 갖고 `float` 형인지 검사. - * 만약 주어진다면, `is_offer`를 선택 속성으로 갖고 `bool` 형인지 검사. + * `price`를 필수 속성으로 갖고 `float` 형이어야 하는지 검사. + * 만약 주어진다면, `is_offer`를 선택 속성으로 갖고 `bool` 형이어야 하는지 검사. * 이 모든 것은 깊이 중첩된 JSON 객체에도 적용됩니다. * JSON을 변환하거나 JSON으로 변환하는 것을 자동화. * 다음에서 사용할 수 있는 모든 것을 OpenAPI로 문서화: @@ -417,30 +439,88 @@ item: Item ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -더 많은 기능을 포함한 보다 완전한 예제의 경우, <a href="https://fastapi.tiangolo.com/tutorial/">튜토리얼 - 사용자 가이드</a>를 보십시오. +더 많은 기능을 포함한 보다 완전한 예제의 경우, <a href="https://fastapi.tiangolo.com/ko/tutorial/">튜토리얼 - 사용자 가이드</a>를 보십시오. **스포일러 주의**: 튜토리얼 - 사용자 가이드는: * 서로 다른 장소에서 **매개변수** 선언: **헤더**, **쿠키**, **폼 필드** 그리고 **파일**. * `maximum_length` 또는 `regex`처럼 **유효성 제약**하는 방법. -* 강력하고 사용하기 쉬운 **<abbr title="컴포넌트, 리소스, 제공자, 서비스, injectables라 알려진">의존성 주입</abbr>** 시스템. +* 강력하고 사용하기 쉬운 **<abbr title="also known as components, resources, providers, services, injectables">의존성 주입</abbr>** 시스템. * **OAuth2** 지원을 포함한 **JWT tokens** 및 **HTTP Basic**을 갖는 보안과 인증. * (Pydantic 덕분에) **깊은 중첩 JSON 모델**을 선언하는데 더 진보한 (하지만 마찬가지로 쉬운) 기술. +* <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a> 및 기타 라이브러리와의 **GraphQL** 통합. * (Starlette 덕분에) 많은 추가 기능: * **웹 소켓** - * **GraphQL** * HTTPX 및 `pytest`에 기반한 극히 쉬운 테스트 * **CORS** * **쿠키 세션** * ...기타 등등. -## 성능 +### 앱 배포하기(선택 사항) { #deploy-your-app-optional } -독립된 TechEmpower 벤치마크에서 Uvicorn에서 작동하는 FastAPI 어플리케이션이 <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">사용 가능한 가장 빠른 프레임워크 중 하나</a>로 Starlette와 Uvicorn(FastAPI에서 내부적으로 사용)에만 밑돌고 있습니다. (*) +선택적으로 FastAPI 앱을 <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>에 배포할 수 있습니다. 아직이라면 대기자 명단에 등록해 보세요. 🚀 -자세한 내용은 <a href="https://fastapi.tiangolo.com/benchmarks/" class="internal-link" target="_blank">벤치마크</a> 섹션을 보십시오. +이미 **FastAPI Cloud** 계정이 있다면(대기자 명단에서 초대해 드렸습니다 😉), 한 번의 명령으로 애플리케이션을 배포할 수 있습니다. -## 선택가능한 의존성 +배포하기 전에, 로그인되어 있는지 확인하세요: + +<div class="termy"> + +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +</div> + +그런 다음 앱을 배포하세요: + +<div class="termy"> + +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +</div> + +이게 전부입니다! 이제 해당 URL에서 앱에 접근할 수 있습니다. ✨ + +#### FastAPI Cloud 소개 { #about-fastapi-cloud } + +**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>**는 **FastAPI** 뒤에 있는 동일한 작성자와 팀이 만들었습니다. + +최소한의 노력으로 API를 **빌드**, **배포**, **접근**하는 과정을 간소화합니다. + +FastAPI로 앱을 빌드할 때의 동일한 **개발자 경험**을 클라우드에 **배포**하는 데까지 확장해 줍니다. 🎉 + +FastAPI Cloud는 *FastAPI and friends* 오픈 소스 프로젝트의 주요 스폰서이자 자금 제공자입니다. ✨ + +#### 다른 클라우드 제공자에 배포하기 { #deploy-to-other-cloud-providers } + +FastAPI는 오픈 소스이며 표준을 기반으로 합니다. 선택한 어떤 클라우드 제공자에도 FastAPI 앱을 배포할 수 있습니다. + +클라우드 제공자의 가이드를 따라 FastAPI 앱을 배포하세요. 🤓 + +## 성능 { #performance } + +독립된 TechEmpower 벤치마크에서 Uvicorn에서 작동하는 FastAPI 애플리케이션이 <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">사용 가능한 가장 빠른 Python 프레임워크 중 하나</a>로 Starlette와 Uvicorn(FastAPI에서 내부적으로 사용)에만 밑돌고 있습니다. (*) + +자세한 내용은 <a href="https://fastapi.tiangolo.com/ko/benchmarks/" class="internal-link" target="_blank">벤치마크</a> 섹션을 보십시오. + +## 의존성 { #dependencies } + +FastAPI는 Pydantic과 Starlette에 의존합니다. + +### `standard` 의존성 { #standard-dependencies } + +FastAPI를 `pip install "fastapi[standard]"`로 설치하면 `standard` 그룹의 선택적 의존성이 함께 설치됩니다. Pydantic이 사용하는: @@ -448,21 +528,38 @@ Pydantic이 사용하는: Starlette이 사용하는: -* <a href="https://www.python-httpx.org" target="_blank"><code>HTTPX</code></a> - `TestClient`를 사용하려면 필요. -* <a href="http://jinja.pocoo.org" target="_blank"><code>jinja2</code></a> - 기본 템플릿 설정을 사용하려면 필요. -* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - `request.form()`과 함께 <abbr title="HTTP 요청에서 파이썬 데이터로 가는 문자열 변환">"parsing"</abbr>의 지원을 원하면 필요. -* <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - `SessionMiddleware` 지원을 위해 필요. -* <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - Starlette의 `SchemaGenerator` 지원을 위해 필요 (FastAPI와 쓸때는 필요 없을 것입니다). -* <a href="https://graphene-python.org/" target="_blank"><code>graphene</code></a> - `GraphQLApp` 지원을 위해 필요. +* <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - `TestClient`를 사용하려면 필요. +* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - 기본 템플릿 설정을 사용하려면 필요. +* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - `request.form()`과 함께 form <abbr title="HTTP 요청에서 파이썬 데이터로 가는 문자열 변환">"parsing"</abbr> 지원을 원하면 필요. -FastAPI / Starlette이 사용하는: +FastAPI가 사용하는: -* <a href="http://www.uvicorn.dev" target="_blank"><code>uvicorn</code></a> - 애플리케이션을 로드하고 제공하는 서버. -* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - `ORJSONResponse`을 사용하려면 필요. +* <a href="https://www.uvicorn.dev" target="_blank"><code>uvicorn</code></a> - 애플리케이션을 로드하고 제공하는 서버를 위한 것입니다. 여기에는 고성능 서빙에 필요한 일부 의존성(예: `uvloop`)이 포함된 `uvicorn[standard]`가 포함됩니다. +* `fastapi-cli[standard]` - `fastapi` 명령을 제공하기 위한 것입니다. + * 여기에는 FastAPI 애플리케이션을 <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>에 배포할 수 있게 해주는 `fastapi-cloud-cli`가 포함됩니다. + +### `standard` 의존성 없이 { #without-standard-dependencies } + +`standard` 선택적 의존성을 포함하고 싶지 않다면, `pip install "fastapi[standard]"` 대신 `pip install fastapi`로 설치할 수 있습니다. + +### `fastapi-cloud-cli` 없이 { #without-fastapi-cloud-cli } + +표준 의존성과 함께 FastAPI를 설치하되 `fastapi-cloud-cli` 없이 설치하고 싶다면, `pip install "fastapi[standard-no-fastapi-cloud-cli]"`로 설치할 수 있습니다. + +### 추가 선택적 의존성 { #additional-optional-dependencies } + +추가로 설치하고 싶을 수 있는 의존성도 있습니다. + +추가 선택적 Pydantic 의존성: + +* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - 설정 관리를 위한 것입니다. +* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - Pydantic에서 사용할 추가 타입을 위한 것입니다. + +추가 선택적 FastAPI 의존성: + +* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - `ORJSONResponse`를 사용하려면 필요. * <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - `UJSONResponse`를 사용하려면 필요. -`pip install fastapi[all]`를 통해 이 모두를 설치 할 수 있습니다. - -## 라이센스 +## 라이센스 { #license } 이 프로젝트는 MIT 라이센스 조약에 따라 라이센스가 부여됩니다. diff --git a/docs/ko/docs/learn/index.md b/docs/ko/docs/learn/index.md index 7ac3a99b6..0b4d14ff4 100644 --- a/docs/ko/docs/learn/index.md +++ b/docs/ko/docs/learn/index.md @@ -1,5 +1,5 @@ -# 배우기 +# 배우기 { #learn } -여기 **FastAPI**를 배우기 위한 입문 자료와 자습서가 있습니다. +여기 **FastAPI**를 배우기 위한 입문 섹션과 자습서가 있습니다. -여러분은 FastAPI를 배우기 위해 **책**, **강의**, **공식 자료** 그리고 추천받은 방법을 고려할 수 있습니다. 😎 +여러분은 이것을 FastAPI를 배우기 위한 **책**, **강의**, **공식**이자 권장되는 방법으로 생각할 수 있습니다. 😎 diff --git a/docs/ko/docs/project-generation.md b/docs/ko/docs/project-generation.md index dd11fca70..73ea67d3e 100644 --- a/docs/ko/docs/project-generation.md +++ b/docs/ko/docs/project-generation.md @@ -1,4 +1,4 @@ -# Full Stack FastAPI 템플릿 +# Full Stack FastAPI 템플릿 { #full-stack-fastapi-template } 템플릿은 일반적으로 특정 설정과 함께 제공되지만, 유연하고 커스터마이징이 가능하게 디자인 되었습니다. 이 특성들은 여러분이 프로젝트의 요구사항에 맞춰 수정, 적용을 할 수 있게 해주고, 템플릿이 완벽한 시작점이 되게 해줍니다. 🏁 @@ -6,23 +6,23 @@ GitHub 저장소: <a href="https://github.com/tiangolo/full-stack-fastapi-template" class="external-link" target="_blank">Full Stack FastAPI 템플릿</a> -## Full Stack FastAPI 템플릿 - 기술 스택과 기능들 +## Full Stack FastAPI 템플릿 - 기술 스택과 기능들 { #full-stack-fastapi-template-technology-stack-and-features } -- ⚡ [**FastAPI**](https://fastapi.tiangolo.com): Python 백엔드 API. - - 🧰 [SQLModel](https://sqlmodel.tiangolo.com): Python SQL 데이터 상호작용을 위한 (ORM). - - 🔍 [Pydantic](https://docs.pydantic.dev): FastAPI에 의해 사용되는, 데이터 검증과 설정관리. - - 💾 [PostgreSQL](https://www.postgresql.org): SQL 데이터베이스. -- 🚀 [React](https://react.dev): 프론트엔드. - - 💃 TypeScript, hooks, [Vite](https://vitejs.dev) 및 기타 현대적인 프론트엔드 스택을 사용. - - 🎨 [Chakra UI](https://chakra-ui.com): 프론트엔드 컴포넌트. +- ⚡ Python 백엔드 API를 위한 [**FastAPI**](https://fastapi.tiangolo.com/ko). + - 🧰 Python SQL 데이터베이스 상호작용을 위한 [SQLModel](https://sqlmodel.tiangolo.com) (ORM). + - 🔍 FastAPI에 의해 사용되는, 데이터 검증과 설정 관리를 위한 [Pydantic](https://docs.pydantic.dev). + - 💾 SQL 데이터베이스로서의 [PostgreSQL](https://www.postgresql.org). +- 🚀 프론트엔드를 위한 [React](https://react.dev). + - 💃 TypeScript, hooks, Vite 및 기타 현대적인 프론트엔드 스택을 사용. + - 🎨 프론트엔드 컴포넌트를 위한 [Tailwind CSS](https://tailwindcss.com) 및 [shadcn/ui](https://ui.shadcn.com). - 🤖 자동으로 생성된 프론트엔드 클라이언트. - - 🧪 E2E 테스트를 위한 [Playwright](https://playwright.dev). + - 🧪 End-to-End 테스트를 위한 [Playwright](https://playwright.dev). - 🦇 다크 모드 지원. -- 🐋 [Docker Compose](https://www.docker.com): 개발 환경과 프로덕션(운영). +- 🐋 개발 환경과 프로덕션(운영)을 위한 [Docker Compose](https://www.docker.com). - 🔒 기본으로 지원되는 안전한 비밀번호 해싱. -- 🔑 JWT 토큰 인증. +- 🔑 JWT (JSON Web Token) 인증. - 📫 이메일 기반 비밀번호 복구. -- ✅ [Pytest]를 이용한 테스트(https://pytest.org). -- 📞 [Traefik](https://traefik.io): 리버스 프록시 / 로드 밸런서. +- ✅ [Pytest](https://pytest.org)를 이용한 테스트. +- 📞 리버스 프록시 / 로드 밸런서로서의 [Traefik](https://traefik.io). - 🚢 Docker Compose를 이용한 배포 지침: 자동 HTTPS 인증서를 처리하기 위한 프론트엔드 Traefik 프록시 설정 방법을 포함. - 🏭 GitHub Actions를 기반으로 CI (지속적인 통합) 및 CD (지속적인 배포). diff --git a/docs/ko/docs/python-types.md b/docs/ko/docs/python-types.md index 18d4b341e..dc264df80 100644 --- a/docs/ko/docs/python-types.md +++ b/docs/ko/docs/python-types.md @@ -1,313 +1,466 @@ -# 파이썬 타입 소개 +# 파이썬 타입 소개 { #python-types-intro } -파이썬은 선택적으로 "타입 힌트(type hints)"를 지원합니다. +파이썬은 선택적으로 "타입 힌트(type hints)"(“type annotations”라고도 함)를 지원합니다. -이러한 **타입 힌트**들은 변수의 <abbr title="예를 들면: str, int, float, bool">타입</abbr>을 선언할 수 있게 해주는 특수한 구문입니다. +이러한 **"타입 힌트"** 또는 애너테이션은 변수의 <abbr title="for example: str, int, float, bool">타입</abbr>을 선언할 수 있게 해주는 특수한 구문입니다. -변수의 타입을 지정하면 에디터와 툴이 더 많은 도움을 줄 수 있게 됩니다. +변수의 타입을 선언하면 에디터와 도구가 더 나은 지원을 제공할 수 있습니다. -이 문서는 파이썬 타입 힌트에 대한 **빠른 자습서 / 내용환기** 수준의 문서입니다. 여기서는 **FastAPI**를 쓰기 위한 최소한의 내용만을 다룹니다. +이 문서는 파이썬 타입 힌트에 대한 **빠른 자습서 / 내용 환기**입니다. **FastAPI**와 함께 사용하기 위해 필요한 최소한만 다룹니다... 실제로는 아주 조금만 있으면 됩니다. -**FastAPI**는 타입 힌트에 기반을 두고 있으며, 이는 많은 장점과 이익이 있습니다. +**FastAPI**는 모두 이러한 타입 힌트에 기반을 두고 있으며, 이는 많은 장점과 이점을 제공합니다. -비록 **FastAPI**를 쓰지 않는다고 하더라도, 조금이라도 알아두면 도움이 될 것입니다. +하지만 **FastAPI**를 전혀 사용하지 않더라도, 타입 힌트를 조금만 배워도 도움이 됩니다. /// note | 참고 -파이썬에 능숙하셔서 타입 힌트에 대해 모두 아신다면, 다음 챕터로 건너뛰세요. +파이썬에 능숙하고 타입 힌트에 대해 이미 모두 알고 있다면, 다음 장으로 건너뛰세요. /// -## 동기 부여 +## 동기 부여 { #motivation } -간단한 예제부터 시작해봅시다: +간단한 예제로 시작해봅시다: -{* ../../docs_src/python_types/tutorial001.py *} +{* ../../docs_src/python_types/tutorial001_py39.py *} - -이 프로그램을 실행한 결과값: +이 프로그램을 호출하면 다음이 출력됩니다: ``` John Doe ``` -함수는 아래와 같이 실행됩니다: +이 함수는 다음을 수행합니다: * `first_name`과 `last_name`를 받습니다. -* `title()`로 각 첫 문자를 대문자로 변환시킵니다. -* 두 단어를 중간에 공백을 두고 <abbr title="두 개를 하나로 차례차례 이어지게 하다">연결</abbr>합니다. +* `title()`로 각각의 첫 글자를 대문자로 변환합니다. +* 가운데에 공백을 두고 <abbr title="Puts them together, as one. With the contents of one after the other.">연결</abbr>합니다. -{* ../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} +### 수정하기 { #edit-it } -### 코드 수정 +매우 간단한 프로그램입니다. -이건 매우 간단한 프로그램입니다. +하지만 이제, 이것을 처음부터 작성한다고 상상해봅시다. -그런데 처음부터 작성한다고 생각을 해봅시다. +어느 시점엔 함수를 정의하기 시작했고, 매개변수도 준비해두었을 겁니다... -여러분은 매개변수를 준비했고, 함수를 정의하기 시작했을 겁니다. +그런데 "첫 글자를 대문자로 변환하는 그 메서드"를 호출해야 합니다. -이때 "첫 글자를 대문자로 바꾸는 함수"를 호출해야 합니다. +`upper`였나요? `uppercase`였나요? `first_uppercase`? `capitalize`? -`upper`였나? 아니면 `uppercase`? `first_uppercase`? `capitalize`? +그 다음, 개발자들의 오랜 친구인 에디터 자동완성을 시도합니다. -그때 개발자들의 오랜 친구, 에디터 자동완성을 시도해봅니다. +함수의 첫 번째 매개변수인 `first_name`을 입력하고, 점(`.`)을 찍은 다음, 완성을 트리거하기 위해 `Ctrl+Space`를 누릅니다. -당신은 `first_name`를 입력한 뒤 점(`.`)을 입력하고 자동완성을 켜기 위해서 `Ctrl+Space`를 눌렀습니다. - -하지만 슬프게도 아무런 도움이 되지 않습니다: +하지만, 슬프게도 쓸만한 게 아무것도 없습니다: <img src="/img/python-types/image01.png"> -### 타입 추가하기 +### 타입 추가하기 { #add-types } 이전 버전에서 한 줄만 수정해봅시다. -저희는 이 함수의 매개변수 부분: +함수의 매개변수인 정확히 이 부분을: ```Python first_name, last_name ``` -을 아래와 같이 바꿀 겁니다: +에서: ```Python first_name: str, last_name: str ``` +로 바꾸겠습니다. + 이게 다입니다. -이게 "타입 힌트"입니다: +이것들이 "타입 힌트"입니다: -{* ../../docs_src/python_types/tutorial002.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} - -타입힌트는 다음과 같이 기본 값을 선언하는 것과는 다릅니다: +이것은 다음처럼 기본값을 선언하는 것과는 다릅니다: ```Python first_name="john", last_name="doe" ``` -이는 다른 것입니다. +다른 것입니다. -등호(`=`) 대신 콜론(`:`)을 쓰고 있습니다. +등호(`=`)가 아니라 콜론(`:`)을 사용합니다. -일반적으로 타입힌트를 추가한다고 해서 특별하게 어떤 일이 일어나지도 않습니다. +그리고 보통 타입 힌트를 추가해도, 타입 힌트 없이 일어나는 일과 비교해 특별히 달라지는 것은 없습니다. -그렇지만 이제, 다시 함수를 만드는 도중이라고 생각해봅시다. 다만 이번엔 타입 힌트가 있습니다. +하지만 이제, 타입 힌트를 포함해 그 함수를 다시 만드는 중이라고 상상해봅시다. -같은 상황에서 `Ctrl+Space`로 자동완성을 작동시키면, +같은 지점에서 `Ctrl+Space`로 자동완성을 트리거하면 다음이 보입니다: <img src="/img/python-types/image02.png"> -아래와 같이 "그렇지!"하는 옵션이 나올때까지 스크롤을 내려서 볼 수 있습니다: +그러면 스크롤하며 옵션을 보다가, "기억나는" 것을 찾을 수 있습니다: <img src="/img/python-types/image03.png"> -## 더 큰 동기부여 +## 더 큰 동기부여 { #more-motivation } -아래 함수를 보면, 이미 타입 힌트가 적용되어 있는 걸 볼 수 있습니다: +이 함수를 확인해보세요. 이미 타입 힌트가 있습니다: -{* ../../docs_src/python_types/tutorial003.py hl[1] *} +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} - -편집기가 변수의 타입을 알고 있기 때문에, 자동완성 뿐 아니라 에러도 확인할 수 있습니다: +에디터가 변수의 타입을 알고 있기 때문에, 자동완성만 되는 게 아니라 오류 검사도 할 수 있습니다: <img src="/img/python-types/image04.png"> -이제 고쳐야하는 걸 알기 때문에, `age`를 `str(age)`과 같이 문자열로 바꾸게 됩니다: +이제 고쳐야 한다는 것을 알고, `age`를 `str(age)`로 문자열로 바꿉니다: -{* ../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} +## 타입 선언 { #declaring-types } -## 타입 선언 +방금 타입 힌트를 선언하는 주요 위치를 보았습니다. 함수 매개변수입니다. -방금 함수의 매개변수로써 타입 힌트를 선언하는 주요 장소를 보았습니다. +이것은 **FastAPI**와 함께 사용할 때도 주요 위치입니다. -이 위치는 여러분이 **FastAPI**와 함께 이를 사용하는 주요 장소입니다. - -### Simple 타입 +### Simple 타입 { #simple-types } `str`뿐 아니라 모든 파이썬 표준 타입을 선언할 수 있습니다. -예를 들면: +예를 들어 다음을 사용할 수 있습니다: * `int` * `float` * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} +### 타입 매개변수가 있는 Generic(제네릭) 타입 { #generic-types-with-type-parameters } -### 타입 매개변수를 활용한 Generic(제네릭) 타입 +`dict`, `list`, `set`, `tuple`처럼 다른 값을 담을 수 있는 데이터 구조가 있습니다. 그리고 내부 값에도 각자의 타입이 있을 수 있습니다. -`dict`, `list`, `set`, `tuple`과 같은 값을 저장할 수 있는 데이터 구조가 있고, 내부의 값은 각자의 타입을 가질 수도 있습니다. +이렇게 내부 타입을 가지는 타입을 "**generic**" 타입이라고 부릅니다. 그리고 내부 타입까지 포함해 선언할 수도 있습니다. -타입과 내부 타입을 선언하기 위해서는 파이썬 표준 모듈인 `typing`을 이용해야 합니다. +이런 타입과 내부 타입을 선언하려면 표준 파이썬 모듈 `typing`을 사용할 수 있습니다. 이 모듈은 이러한 타입 힌트를 지원하기 위해 존재합니다. -구체적으로는 아래 타입 힌트를 지원합니다. +#### 더 최신 버전의 Python { #newer-versions-of-python } -#### `List` +`typing`을 사용하는 문법은 Python 3.6부터 최신 버전까지, Python 3.9, Python 3.10 등을 포함한 모든 버전과 **호환**됩니다. -예를 들면, `str`의 `list`인 변수를 정의해봅시다. +파이썬이 발전함에 따라 **더 최신 버전**에서는 이러한 타입 애너테이션 지원이 개선되며, 많은 경우 타입 애너테이션을 선언하기 위해 `typing` 모듈을 import해서 사용할 필요조차 없게 됩니다. -`typing`에서 `List`(대문자 `L`)를 import 합니다. +프로젝트에서 더 최신 버전의 파이썬을 선택할 수 있다면, 그 추가적인 단순함을 활용할 수 있습니다. -{* ../../docs_src/python_types/tutorial006.py hl[1] *} +이 문서 전체에는 각 파이썬 버전과 호환되는 예제가 있습니다(차이가 있을 때). +예를 들어 "**Python 3.6+**"는 Python 3.6 이상(3.7, 3.8, 3.9, 3.10 등 포함)과 호환된다는 뜻입니다. 그리고 "**Python 3.9+**"는 Python 3.9 이상(3.10 등 포함)과 호환된다는 뜻입니다. -콜론(`:`) 문법을 이용하여 변수를 선언합니다. +**최신 버전의 Python**을 사용할 수 있다면, 최신 버전용 예제를 사용하세요. 예를 들어 "**Python 3.10+**"처럼, 가장 **좋고 가장 단순한 문법**을 갖게 됩니다. -타입으로는 `List`를 넣어줍니다. +#### List { #list } -이때 배열은 내부 타입을 포함하는 타입이기 때문에 대괄호 안에 넣어줍니다. +예를 들어, `str`의 `list`인 변수를 정의해봅시다. -{* ../../docs_src/python_types/tutorial006.py hl[4] *} +같은 콜론(`:`) 문법으로 변수를 선언합니다. +타입으로 `list`를 넣습니다. -/// tip | 팁 - -대괄호 안의 내부 타입은 "타입 매개변수(type paramters)"라고 합니다. - -이번 예제에서는 `str`이 `List`에 들어간 타입 매개변수 입니다. - -/// - -이는 "`items`은 `list`인데, 배열에 들어있는 아이템 각각은 `str`이다"라는 뜻입니다. - -이렇게 함으로써, 에디터는 배열에 들어있는 아이템을 처리할때도 도움을 줄 수 있게 됩니다: - -<img src="/img/python-types/image05.png"> - -타입이 없으면 이건 거의 불가능이나 다름 없습니다. - -변수 `item`은 `items`의 개별 요소라는 사실을 알아두세요. - -그리고 에디터는 계속 `str`라는 사실을 알고 도와줍니다. - -#### `Tuple`과 `Set` - -`tuple`과 `set`도 동일하게 선언할 수 있습니다. - -{* ../../docs_src/python_types/tutorial007.py hl[1,4] *} - - -이 뜻은 아래와 같습니다: - -* 변수 `items_t`는, 차례대로 `int`, `int`, `str`인 `tuple`이다. -* 변수 `items_s`는, 각 아이템이 `bytes`인 `set`이다. - -#### `Dict` - -`dict`를 선언하려면 컴마로 구분된 2개의 파라미터가 필요합니다. - -첫 번째 매개변수는 `dict`의 키(key)이고, - -두 번째 매개변수는 `dict`의 값(value)입니다. - -{* ../../docs_src/python_types/tutorial008.py hl[1,4] *} - - -이 뜻은 아래와 같습니다: - -* 변수 `prices`는 `dict`이다: - * `dict`의 키(key)는 `str`타입이다. (각 아이템의 이름(name)) - * `dict`의 값(value)는 `float`타입이다. (각 아이템의 가격(price)) - -#### `Optional` - -`str`과 같이 타입을 선언할 때 `Optional`을 쓸 수도 있는데, "선택적(Optional)"이기때문에 `None`도 될 수 있습니다: - -```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} -``` - -`Optional[str]`을 `str` 대신 쓰게 되면, 특정 값이 실제로는 `None`이 될 수도 있는데 항상 `str`이라고 가정하는 상황에서 에디터가 에러를 찾게 도와줄 수 있습니다. - -#### Generic(제네릭) 타입 - -이 타입은 대괄호 안에 매개변수를 가지며, 종류는: - -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Optional` -* ...등등 - -위와 같은 타입은 **Generic(제네릭) 타입** 혹은 **Generics(제네릭스)**라고 불립니다. - -### 타입으로서의 클래스 - -변수의 타입으로 클래스를 선언할 수도 있습니다. - -이름(name)을 가진 `Person` 클래스가 있다고 해봅시다. - -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} - - -그렇게 하면 변수를 `Person`이라고 선언할 수 있게 됩니다. - -{* ../../docs_src/python_types/tutorial010.py hl[6] *} - - -그리고 역시나 모든 에디터 도움을 받게 되겠죠. - -<img src="/img/python-types/image06.png"> - -## Pydantic 모델 - -<a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a>은 데이터 검증(Validation)을 위한 파이썬 라이브러리입니다. - -당신은 속성들을 포함한 클래스 형태로 "모양(shape)"을 선언할 수 있습니다. - -그리고 각 속성은 타입을 가지고 있습니다. - -이 클래스를 활용하여서 값을 가지고 있는 인스턴스를 만들게 되면, 필요한 경우에는 적당한 타입으로 변환까지 시키기도 하여 데이터가 포함된 객체를 반환합니다. - -그리고 결과 객체에 대해서는 에디터의 도움을 받을 수 있게 됩니다. - -Pydantic 공식 문서 예시: - -{* ../../docs_src/python_types/tutorial011.py *} +`list`는 내부 타입을 포함하는 타입이므로, 그 타입들을 대괄호 안에 넣습니다: +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} /// info | 정보 -Pydantic<에 대해 더 배우고 싶다면 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">공식 문서</a>를 참고하세요.</a> +대괄호 안의 내부 타입은 "type parameters"라고 부릅니다. + +이 경우 `str`이 `list`에 전달된 타입 매개변수입니다. /// -**FastAPI**는 모두 Pydantic을 기반으로 되어 있습니다. +이는 "변수 `items`는 `list`이고, 이 `list`의 각 아이템은 `str`이다"라는 뜻입니다. -이 모든 것이 실제로 어떻게 사용되는지에 대해서는 [자습서 - 사용자 안내서](tutorial/index.md){.internal-link target=_blank} 에서 더 많이 확인하실 수 있습니다. +이렇게 하면, 에디터는 리스트의 아이템을 처리하는 동안에도 지원을 제공할 수 있습니다: -## **FastAPI**에서의 타입 힌트 +<img src="/img/python-types/image05.png"> -**FastAPI**는 여러 부분에서 타입 힌트의 장점을 취하고 있습니다. +타입이 없으면, 이는 거의 불가능합니다. -**FastAPI**에서 타입 힌트와 함께 매개변수를 선언하면 장점은: +변수 `item`이 리스트 `items`의 요소 중 하나라는 점에 주목하세요. + +그리고 에디터는 여전히 이것이 `str`임을 알고, 그에 대한 지원을 제공합니다. + +#### Tuple과 Set { #tuple-and-set } + +`tuple`과 `set`도 동일하게 선언할 수 있습니다: + +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} + +이는 다음을 의미합니다: + +* 변수 `items_t`는 3개의 아이템을 가진 `tuple`이며, `int`, 또 다른 `int`, 그리고 `str`입니다. +* 변수 `items_s`는 `set`이며, 각 아이템의 타입은 `bytes`입니다. + +#### Dict { #dict } + +`dict`를 정의하려면, 쉼표로 구분된 2개의 타입 매개변수를 전달합니다. + +첫 번째 타입 매개변수는 `dict`의 키를 위한 것입니다. + +두 번째 타입 매개변수는 `dict`의 값을 위한 것입니다: + +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} + +이는 다음을 의미합니다: + +* 변수 `prices`는 `dict`입니다: + * 이 `dict`의 키는 `str` 타입입니다(예: 각 아이템의 이름). + * 이 `dict`의 값은 `float` 타입입니다(예: 각 아이템의 가격). + +#### Union { #union } + +변수가 **여러 타입 중 어떤 것이든** 될 수 있다고 선언할 수 있습니다. 예를 들어 `int` 또는 `str`입니다. + +Python 3.6 이상(3.10 포함)에서는 `typing`의 `Union` 타입을 사용하고, 대괄호 안에 허용할 수 있는 타입들을 넣을 수 있습니다. + +Python 3.10에는 가능한 타입들을 <abbr title='also called "bitwise or operator", but that meaning is not relevant here'>세로 막대(`|`)</abbr>로 구분해 넣을 수 있는 **새 문법**도 있습니다. + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../docs_src/python_types/tutorial008b_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial008b_py39.py!} +``` + +//// + +두 경우 모두 이는 `item`이 `int` 또는 `str`일 수 있다는 뜻입니다. + +#### `None`일 수도 있음 { #possibly-none } + +값이 `str` 같은 타입일 수도 있지만, `None`일 수도 있다고 선언할 수 있습니다. + +Python 3.6 이상(3.10 포함)에서는 `typing` 모듈에서 `Optional`을 import해서 사용하여 선언할 수 있습니다. + +```Python hl_lines="1 4" +{!../../docs_src/python_types/tutorial009_py39.py!} +``` + +그냥 `str` 대신 `Optional[str]`을 사용하면, 값이 항상 `str`이라고 가정하고 있지만 실제로는 `None`일 수도 있는 상황에서 에디터가 오류를 감지하도록 도와줍니다. + +`Optional[Something]`은 사실 `Union[Something, None]`의 축약이며, 서로 동등합니다. + +또한 이는 Python 3.10에서 `Something | None`을 사용할 수 있다는 의미이기도 합니다: + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../docs_src/python_types/tutorial009_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009_py39.py!} +``` + +//// + +//// tab | Python 3.9+ alternative + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009b_py39.py!} +``` + +//// + +#### `Union` 또는 `Optional` 사용하기 { #using-union-or-optional } + +Python 3.10 미만 버전을 사용한다면, 아주 **주관적인** 관점에서의 팁입니다: + +* 🚨 `Optional[SomeType]` 사용을 피하세요 +* 대신 ✨ **`Union[SomeType, None]`을 사용하세요** ✨. + +둘은 동등하고 내부적으로는 같은 것이지만, `Optional`이라는 단어가 값이 선택 사항인 것처럼 보일 수 있기 때문에 `Optional` 대신 `Union`을 권장합니다. 실제 의미는 값이 선택 사항이라는 뜻이 아니라, "값이 `None`일 수 있다"는 뜻이기 때문입니다. 선택 사항이 아니고 여전히 필수인 경우에도요. + +`Union[SomeType, None]`이 의미를 더 명확하게 드러낸다고 생각합니다. + +이건 단지 단어와 이름의 문제입니다. 하지만 그런 단어들이 여러분과 팀원이 코드에 대해 생각하는 방식에 영향을 줄 수 있습니다. + +예로, 이 함수를 봅시다: + +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} + +매개변수 `name`은 `Optional[str]`로 정의되어 있지만, **선택 사항이 아닙니다**. 매개변수 없이 함수를 호출할 수 없습니다: + +```Python +say_hi() # Oh, no, this throws an error! 😱 +``` + +기본값이 없기 때문에 `name` 매개변수는 **여전히 필수입니다**(*optional*이 아님). 그럼에도 `name`은 값으로 `None`을 허용합니다: + +```Python +say_hi(name=None) # This works, None is valid 🎉 +``` + +좋은 소식은 Python 3.10을 사용하면, 타입의 유니온을 정의하기 위해 간단히 `|`를 사용할 수 있어서 이런 걱정을 할 필요가 없다는 점입니다: + +{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *} + +그러면 `Optional`이나 `Union` 같은 이름에 대해 걱정할 필요도 없습니다. 😎 + +#### Generic(제네릭) 타입 { #generic-types } + +대괄호 안에 타입 매개변수를 받는 이러한 타입들은 **Generic types** 또는 **Generics**라고 부릅니다. 예를 들면: + +//// tab | Python 3.10+ + +대괄호와 내부 타입을 사용해, 동일한 내장 타입들을 제네릭으로 사용할 수 있습니다: + +* `list` +* `tuple` +* `set` +* `dict` + +그리고 이전 파이썬 버전과 마찬가지로 `typing` 모듈의 다음도 사용할 수 있습니다: + +* `Union` +* `Optional` +* ...그 밖의 것들. + +Python 3.10에서는 제네릭 `Union`과 `Optional`을 사용하는 대안으로, 타입 유니온을 선언하기 위해 <abbr title='also called "bitwise or operator", but that meaning is not relevant here'>세로 막대(`|`)</abbr>를 사용할 수 있는데, 훨씬 더 좋고 단순합니다. + +//// + +//// tab | Python 3.9+ + +대괄호와 내부 타입을 사용해, 동일한 내장 타입들을 제네릭으로 사용할 수 있습니다: + +* `list` +* `tuple` +* `set` +* `dict` + +그리고 `typing` 모듈의 제네릭들: + +* `Union` +* `Optional` +* ...그 밖의 것들. + +//// + +### 타입으로서의 클래스 { #classes-as-types } + +변수의 타입으로 클래스를 선언할 수도 있습니다. + +이름을 가진 `Person` 클래스가 있다고 해봅시다: + +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} + +그러면 `Person` 타입의 변수를 선언할 수 있습니다: + +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} + +그리고 다시, 에디터의 모든 지원을 받을 수 있습니다: + +<img src="/img/python-types/image06.png"> + +이는 "`one_person`은 `Person` 클래스의 **인스턴스**"라는 뜻입니다. + +"`one_person`은 `Person`이라는 **클래스**다"라는 뜻이 아닙니다. + +## Pydantic 모델 { #pydantic-models } + +<a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a>은 데이터 검증을 수행하는 파이썬 라이브러리입니다. + +속성을 가진 클래스 형태로 데이터의 "모양(shape)"을 선언합니다. + +그리고 각 속성은 타입을 가집니다. + +그 다음 그 클래스의 인스턴스를 몇 가지 값으로 생성하면, 값들을 검증하고, (그런 경우라면) 적절한 타입으로 변환한 뒤, 모든 데이터를 가진 객체를 제공합니다. + +그리고 그 결과 객체에 대해 에디터의 모든 지원을 받을 수 있습니다. + +Pydantic 공식 문서의 예시: + +{* ../../docs_src/python_types/tutorial011_py310.py *} + +/// info | 정보 + +<a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic에 대해 더 알아보려면 문서를 확인하세요</a>. + +/// + +**FastAPI**는 모두 Pydantic에 기반을 두고 있습니다. + +이 모든 것은 [자습서 - 사용자 안내서](tutorial/index.md){.internal-link target=_blank}에서 실제로 많이 보게 될 것입니다. + +/// tip | 팁 + +Pydantic은 기본값 없이 `Optional` 또는 `Union[Something, None]`을 사용할 때 특별한 동작이 있습니다. 이에 대해서는 Pydantic 문서의 <a href="https://docs.pydantic.dev/2.3/usage/models/#required-fields" class="external-link" target="_blank">Required Optional fields</a>에서 더 읽을 수 있습니다. + +/// + +## 메타데이터 애너테이션이 있는 타입 힌트 { #type-hints-with-metadata-annotations } + +파이썬에는 `Annotated`를 사용해 이러한 타입 힌트에 **추가 <abbr title="Data about the data, in this case, information about the type, e.g. a description.">메타데이터</abbr>**를 넣을 수 있는 기능도 있습니다. + +Python 3.9부터 `Annotated`는 표준 라이브러리의 일부이므로, `typing`에서 import할 수 있습니다. + +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} + +파이썬 자체는 이 `Annotated`로 아무것도 하지 않습니다. 그리고 에디터와 다른 도구들에게는 타입이 여전히 `str`입니다. + +하지만 `Annotated`의 이 공간을 사용해, 애플리케이션이 어떻게 동작하길 원하는지에 대한 추가 메타데이터를 **FastAPI**에 제공할 수 있습니다. + +기억해야 할 중요한 점은 `Annotated`에 전달하는 **첫 번째 *타입 매개변수***가 **실제 타입**이라는 것입니다. 나머지는 다른 도구를 위한 메타데이터일 뿐입니다. + +지금은 `Annotated`가 존재하며, 표준 파이썬이라는 것만 알면 됩니다. 😎 + +나중에 이것이 얼마나 **강력**할 수 있는지 보게 될 것입니다. + +/// tip | 팁 + +이것이 **표준 파이썬**이라는 사실은, 에디터에서 가능한 **최고의 개발자 경험**을 계속 얻을 수 있다는 뜻이기도 합니다. 사용하는 도구로 코드를 분석하고 리팩터링하는 등에서도요. ✨ + +또한 코드가 많은 다른 파이썬 도구 및 라이브러리와 매우 호환된다는 뜻이기도 합니다. 🚀 + +/// + +## **FastAPI**에서의 타입 힌트 { #type-hints-in-fastapi } + +**FastAPI**는 이러한 타입 힌트를 활용해 여러 가지를 합니다. + +**FastAPI**에서는 타입 힌트로 매개변수를 선언하면 다음을 얻습니다: * **에디터 도움**. * **타입 확인**. -...그리고 **FastAPI**는 같은 정의를 아래에도 적용합니다: +...그리고 **FastAPI**는 같은 선언을 다음에도 사용합니다: -* **요구사항 정의**: 요청 경로 매개변수, 쿼리 매개변수, 헤더, 바디, 의존성 등. -* **데이터 변환**: 요청에서 요구한 타입으로. -* **데이터 검증**: 각 요청마다: - * 데이터가 유효하지 않은 경우에는 **자동으로 에러**를 발생합니다. -* OpenAPI를 활용한 **API 문서화**: - * 자동으로 상호작용하는 유저 인터페이스에 쓰이게 됩니다. +* **요구사항 정의**: 요청 경로 매개변수, 쿼리 매개변수, 헤더, 바디, 의존성 등에서. +* **데이터 변환**: 요청에서 필요한 타입으로. +* **데이터 검증**: 각 요청에서: + * 데이터가 유효하지 않을 때 클라이언트에 반환되는 **자동 오류**를 생성합니다. +* OpenAPI를 사용해 API를 **문서화**: + * 자동 상호작용 문서 UI에서 사용됩니다. -위 내용이 다소 추상적일 수도 있지만, 걱정마세요. [자습서 - 사용자 안내서](tutorial/index.md){.internal-link target=_blank}에서 전부 확인 가능합니다. +이 모든 것이 다소 추상적으로 들릴 수도 있습니다. 걱정하지 마세요. [자습서 - 사용자 안내서](tutorial/index.md){.internal-link target=_blank}에서 실제로 확인하게 될 것입니다. -가장 중요한 건, 표준 파이썬 타입을 한 곳에서(클래스를 더하거나, 데코레이터 사용하는 대신) 사용함으로써 **FastAPI**가 당신을 위해 많은 일을 해준다는 사실이죠. +가장 중요한 점은 표준 파이썬 타입을 한 곳에서 사용함으로써(더 많은 클래스, 데코레이터 등을 추가하는 대신) **FastAPI**가 여러분을 위해 많은 일을 해준다는 사실입니다. /// info | 정보 -만약 모든 자습서를 다 보았음에도 타입에 대해서 더 보고자 방문한 경우에는 <a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank">`mypy`에서 제공하는 "cheat sheet"</a>이 좋은 자료가 될 겁니다. +자습서를 모두 끝내고 타입에 대해 더 알아보기 위해 다시 돌아왔다면, 좋은 자료로 <a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank">`mypy`의 "cheat sheet"</a>가 있습니다. /// diff --git a/docs/ko/docs/resources/index.md b/docs/ko/docs/resources/index.md index e804dd4d5..477b93a58 100644 --- a/docs/ko/docs/resources/index.md +++ b/docs/ko/docs/resources/index.md @@ -1,3 +1,3 @@ -# 리소스 +# 리소스 { #resources } -추가 리소스, 외부 링크, 기사 등. ✈️ +추가 리소스, 외부 링크 등. ✈️ diff --git a/docs/ko/docs/tutorial/background-tasks.md b/docs/ko/docs/tutorial/background-tasks.md index 9c4d57481..9e868f2fa 100644 --- a/docs/ko/docs/tutorial/background-tasks.md +++ b/docs/ko/docs/tutorial/background-tasks.md @@ -1,84 +1,86 @@ -# 백그라운드 작업 +# 백그라운드 작업 { #background-tasks } -FastAPI에서는 응답을 반환한 후에 실행할 백그라운드 작업을 정의할 수 있습니다. +FastAPI에서는 응답을 반환한 *후에* 실행할 백그라운드 작업을 정의할 수 있습니다. -백그라운드 작업은 클라이언트가 응답을 받기 위해 작업이 완료될 때까지 기다릴 필요가 없기 때문에 요청 후에 발생해야하는 작업에 매우 유용합니다. +백그라운드 작업은 요청 후에 발생해야 하지만, 클라이언트가 응답을 받기 전에 작업이 완료될 때까지 기다릴 필요가 없는 작업에 유용합니다. -이러한 작업에는 다음이 포함됩니다. +예를 들면 다음과 같습니다. -* 작업을 수행한 후 전송되는 이메일 알림 - * 이메일 서버에 연결하고 이메일을 전송하는 것은 (몇 초 정도) "느린" 경향이 있으므로, 응답은 즉시 반환하고 이메일 알림은 백그라운드에서 전송하는 게 가능합니다. +* 작업을 수행한 후 전송되는 이메일 알림: + * 이메일 서버에 연결하고 이메일을 전송하는 것은 (몇 초 정도) "느린" 경향이 있으므로, 응답은 즉시 반환하고 이메일 알림은 백그라운드에서 전송할 수 있습니다. * 데이터 처리: - * 예를 들어 처리에 오랜 시간이 걸리는 데이터를 받았을 때 "Accepted" (HTTP 202)을 반환하고, 백그라운드에서 데이터를 처리할 수 있습니다. + * 예를 들어 처리에 오랜 시간이 걸리는 프로세스를 거쳐야 하는 파일을 받았다면, "Accepted"(HTTP 202) 응답을 반환하고 백그라운드에서 파일을 처리할 수 있습니다. -## `백그라운드 작업` 사용 +## `BackgroundTasks` 사용 { #using-backgroundtasks } -먼저 아래와 같이 `BackgroundTasks`를 임포트하고, `BackgroundTasks`를 _경로 작동 함수_ 에서 매개변수로 가져오고 정의합니다. +먼저 `BackgroundTasks`를 임포트하고, `BackgroundTasks` 타입 선언으로 *경로 처리 함수*에 매개변수를 정의합니다: -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} -**FastAPI** 는 `BackgroundTasks` 개체를 생성하고, 매개 변수로 전달합니다. +**FastAPI**가 `BackgroundTasks` 타입의 객체를 생성하고 해당 매개변수로 전달합니다. -## 작업 함수 생성 +## 작업 함수 생성 { #create-a-task-function } -백그라운드 작업으로 실행할 함수를 정의합니다. +백그라운드 작업으로 실행할 함수를 생성합니다. -이것은 단순히 매개변수를 받을 수 있는 표준 함수일 뿐입니다. +이는 매개변수를 받을 수 있는 표준 함수일 뿐입니다. -**FastAPI**는 이것이 `async def` 함수이든, 일반 `def` 함수이든 내부적으로 이를 올바르게 처리합니다. +`async def` 함수일 수도, 일반 `def` 함수일 수도 있으며, **FastAPI**가 이를 올바르게 처리하는 방법을 알고 있습니다. -이 경우, 아래 작업은 파일에 쓰는 함수입니다. (이메일 보내기 시물레이션) +이 경우 작업 함수는 파일에 쓰기를 수행합니다(이메일 전송을 시뮬레이션). -그리고 이 작업은 `async`와 `await`를 사용하지 않으므로 일반 `def` 함수로 선언합니다. +그리고 쓰기 작업은 `async`와 `await`를 사용하지 않으므로, 일반 `def`로 함수를 정의합니다: -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} -## 백그라운드 작업 추가 +## 백그라운드 작업 추가 { #add-the-background-task } -_경로 작동 함수_ 내에서 작업 함수를 `.add_task()` 함수 통해 _백그라운드 작업_ 개체에 전달합니다. +*경로 처리 함수* 내부에서 `.add_task()` 메서드로 작업 함수를 *백그라운드 작업* 객체에 전달합니다: -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} -`.add_task()` 함수는 다음과 같은 인자를 받습니다 : +`.add_task()`는 다음 인자를 받습니다: -- 백그라운드에서 실행되는 작업 함수 (`write_notification`). -- 작업 함수에 순서대로 전달되어야 하는 일련의 인자 (`email`). -- 작업 함수에 전달되어야하는 모든 키워드 인자 (`message="some notification"`). +* 백그라운드에서 실행될 작업 함수(`write_notification`). +* 작업 함수에 순서대로 전달되어야 하는 인자 시퀀스(`email`). +* 작업 함수에 전달되어야 하는 키워드 인자(`message="some notification"`). -## 의존성 주입 +## 의존성 주입 { #dependency-injection } -`BackgroundTasks`를 의존성 주입 시스템과 함께 사용하면 _경로 작동 함수_, 종속성, 하위 종속성 등 여러 수준에서 BackgroundTasks 유형의 매개변수를 선언할 수 있습니다. +`BackgroundTasks`는 의존성 주입 시스템에서도 동작하며, *경로 처리 함수*, 의존성(dependable), 하위 의존성 등 여러 수준에서 `BackgroundTasks` 타입의 매개변수를 선언할 수 있습니다. -**FastAPI**는 각 경우에 수행할 작업과 동일한 개체를 내부적으로 재사용하기에, 모든 백그라운드 작업이 함께 병합되고 나중에 백그라운드에서 실행됩니다. +**FastAPI**는 각 경우에 무엇을 해야 하는지와 동일한 객체를 어떻게 재사용해야 하는지를 알고 있으므로, 모든 백그라운드 작업이 함께 병합되어 이후 백그라운드에서 실행됩니다: -{* ../../docs_src/background_tasks/tutorial002.py hl[13,15,22,25] *} -이 예제에서는 응답이 반환된 후에 `log.txt` 파일에 메시지가 기록됩니다. +{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} -요청에 쿼리가 있는 경우 백그라운드 작업의 로그에 기록됩니다. -그리고 _경로 작동 함수_ 에서 생성된 또 다른 백그라운드 작업은 경로 매개 변수를 활용하여 사용하여 메시지를 작성합니다. +이 예제에서는 응답이 전송된 *후에* 메시지가 `log.txt` 파일에 작성됩니다. -## 기술적 세부사항 +요청에 쿼리가 있었다면, 백그라운드 작업으로 로그에 작성됩니다. + +그 다음 *경로 처리 함수*에서 생성된 또 다른 백그라운드 작업이 `email` 경로 매개변수를 사용해 메시지를 작성합니다. + +## 기술적 세부사항 { #technical-details } `BackgroundTasks` 클래스는 <a href="https://www.starlette.dev/background/" class="external-link" target="_blank">`starlette.background`</a>에서 직접 가져옵니다. -`BackgroundTasks` 클래스는 FastAPI에서 직접 임포트하거나 포함하기 때문에 실수로 `BackgroundTask` (끝에 `s`가 없음)을 임포트하더라도 starlette.background에서 `BackgroundTask`를 가져오는 것을 방지할 수 있습니다. +FastAPI에 직접 임포트/포함되어 있으므로 `fastapi`에서 임포트할 수 있고, 실수로 `starlette.background`에서 대안인 `BackgroundTask`(끝에 `s`가 없음)를 임포트하는 것을 피할 수 있습니다. -(`BackgroundTask`가 아닌) `BackgroundTasks`를 사용하면, _경로 작동 함수_ 매개변수로 사용할 수 있게 되고 나머지는 **FastAPI**가 대신 처리하도록 할 수 있습니다. 이것은 `Request` 객체를 직접 사용하는 것과 같은 방식입니다. +`BackgroundTask`가 아닌 `BackgroundTasks`만 사용하면, 이를 *경로 처리 함수*의 매개변수로 사용할 수 있고 나머지는 **FastAPI**가 `Request` 객체를 직접 사용할 때처럼 대신 처리해 줍니다. -FastAPI에서 `BackgroundTask`를 단독으로 사용하는 것은 여전히 가능합니다. 하지만 객체를 코드에서 생성하고, 이 객체를 포함하는 Starlette `Response`를 반환해야 합니다. +FastAPI에서 `BackgroundTask`만 단독으로 사용하는 것도 가능하지만, 코드에서 객체를 생성하고 이를 포함하는 Starlette `Response`를 반환해야 합니다. -<a href="https://www.starlette.dev/background/" class="external-link" target="_blank">`Starlette의 공식 문서`</a>에서 백그라운드 작업에 대한 자세한 내용을 확인할 수 있습니다. +더 자세한 내용은 <a href="https://www.starlette.dev/background/" class="external-link" target="_blank">Starlette의 Background Tasks 공식 문서</a>에서 확인할 수 있습니다. -## 경고 +## 주의사항 { #caveat } -만약 무거운 백그라운드 작업을 수행해야하고 동일한 프로세스에서 실행할 필요가 없는 경우 (예: 메모리, 변수 등을 공유할 필요가 없음) <a href="https://docs.celeryq.dev" class="external-link" target="_blank">`Celery`</a>와 같은 큰 도구를 사용하면 도움이 될 수 있습니다. +무거운 백그라운드 계산을 수행해야 하고, 반드시 동일한 프로세스에서 실행할 필요가 없다면(예: 메모리, 변수 등을 공유할 필요가 없음) <a href="https://docs.celeryq.dev" class="external-link" target="_blank">Celery</a> 같은 더 큰 도구를 사용하는 것이 도움이 될 수 있습니다. -RabbitMQ 또는 Redis와 같은 메시지/작업 큐 시스템 보다 복잡한 구성이 필요한 경향이 있지만, 여러 작업 프로세스를 특히 여러 서버의 백그라운드에서 실행할 수 있습니다. +이들은 RabbitMQ나 Redis 같은 메시지/작업 큐 관리자 등 더 복잡한 설정을 필요로 하는 경향이 있지만, 여러 프로세스에서, 특히 여러 서버에서 백그라운드 작업을 실행할 수 있습니다. -그러나 동일한 FastAPI 앱에서 변수 및 개체에 접근해야햐는 작은 백그라운드 수행이 필요한 경우 (예 : 알림 이메일 보내기) 간단하게 `BackgroundTasks`를 사용해보세요. +하지만 동일한 **FastAPI** 앱의 변수와 객체에 접근해야 하거나(또는 이메일 알림 전송처럼) 작은 백그라운드 작업을 수행해야 한다면, `BackgroundTasks`를 간단히 사용하면 됩니다. -## 요약 +## 요약 { #recap } -백그라운드 작업을 추가하기 위해 _경로 작동 함수_ 에 매개변수로 `BackgroundTasks`를 가져오고 사용합니다. +*경로 처리 함수*와 의존성에서 매개변수로 `BackgroundTasks`를 임포트해 사용하여 백그라운드 작업을 추가합니다. diff --git a/docs/ko/docs/tutorial/body-fields.md b/docs/ko/docs/tutorial/body-fields.md index 4708e7099..c98734ab3 100644 --- a/docs/ko/docs/tutorial/body-fields.md +++ b/docs/ko/docs/tutorial/body-fields.md @@ -1,8 +1,8 @@ -# 본문 - 필드 +# 본문 - 필드 { #body-fields } -`Query`, `Path`와 `Body`를 사용해 *경로 작동 함수* 매개변수 내에서 추가적인 검증이나 메타데이터를 선언한 것처럼 Pydantic의 `Field`를 사용하여 모델 내에서 검증과 메타데이터를 선언할 수 있습니다. +`Query`, `Path`와 `Body`를 사용해 *경로 처리 함수* 매개변수 내에서 추가적인 검증이나 메타데이터를 선언한 것처럼 Pydantic의 `Field`를 사용하여 모델 내에서 검증과 메타데이터를 선언할 수 있습니다. -## `Field` 임포트 +## `Field` 임포트 { #import-field } 먼저 이를 임포트해야 합니다: @@ -14,7 +14,7 @@ /// -## 모델 어트리뷰트 선언 +## 모델 어트리뷰트 선언 { #declare-model-attributes } 그 다음 모델 어트리뷰트와 함께 `Field`를 사용할 수 있습니다: @@ -22,7 +22,7 @@ `Field`는 `Query`, `Path`와 `Body`와 같은 방식으로 동작하며, 모두 같은 매개변수들 등을 가집니다. -/// note | 기술적 세부사항 +/// note | 기술 세부사항 실제로 `Query`, `Path`등, 여러분이 앞으로 볼 다른 것들은 공통 클래스인 `Param` 클래스의 서브클래스 객체를 만드는데, 그 자체로 Pydantic의 `FieldInfo` 클래스의 서브클래스입니다. @@ -36,11 +36,11 @@ /// tip | 팁 -주목할 점은 타입, 기본 값 및 `Field`로 이루어진 각 모델 어트리뷰트가 `Path`, `Query`와 `Body`대신 `Field`를 사용하는 *경로 작동 함수*의 매개변수와 같은 구조를 가진다는 점 입니다. +주목할 점은 타입, 기본 값 및 `Field`로 이루어진 각 모델 어트리뷰트가 `Path`, `Query`와 `Body`대신 `Field`를 사용하는 *경로 처리 함수*의 매개변수와 같은 구조를 가진다는 점 입니다. /// -## 별도 정보 추가 +## 별도 정보 추가 { #add-extra-information } `Field`, `Query`, `Body`, 그 외 안에 별도 정보를 선언할 수 있습니다. 이는 생성된 JSON 스키마에 포함됩니다. @@ -53,7 +53,7 @@ /// -## 요약 +## 요약 { #recap } 모델 어트리뷰트를 위한 추가 검증과 메타데이터 선언하기 위해 Pydantic의 `Field` 를 사용할 수 있습니다. diff --git a/docs/ko/docs/tutorial/body-multiple-params.md b/docs/ko/docs/tutorial/body-multiple-params.md index edf892dfa..701351e63 100644 --- a/docs/ko/docs/tutorial/body-multiple-params.md +++ b/docs/ko/docs/tutorial/body-multiple-params.md @@ -1,26 +1,24 @@ -# 본문 - 다중 매개변수 +# 본문 - 다중 매개변수 { #body-multiple-parameters } -지금부터 `Path`와 `Query`를 어떻게 사용하는지 확인하겠습니다. +이제 `Path`와 `Query`를 어떻게 사용하는지 확인했으니, 요청 본문 선언에 대한 더 고급 사용법을 살펴보겠습니다. -요청 본문 선언에 대한 심화 사용법을 알아보겠습니다. +## `Path`, `Query` 및 본문 매개변수 혼합 { #mix-path-query-and-body-parameters } -## `Path`, `Query` 및 본문 매개변수 혼합 +먼저, 물론 `Path`, `Query` 및 요청 본문 매개변수 선언을 자유롭게 혼합해서 사용할 수 있고, **FastAPI**는 어떤 동작을 할지 압니다. -당연하게 `Path`, `Query` 및 요청 본문 매개변수 선언을 자유롭게 혼합해서 사용할 수 있고, **FastAPI**는 어떤 동작을 할지 압니다. +또한 기본 값을 `None`으로 설정해 본문 매개변수를 선택사항으로 선언할 수 있습니다: -또한, 기본 값을 `None`으로 설정해 본문 매개변수를 선택사항으로 선언할 수 있습니다. - -{* ../../docs_src/body_multiple_params/tutorial001.py hl[19:21] *} +{* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} /// note | 참고 -이 경우에는 본문으로 부터 가져온 ` item`은 기본값이 `None`이기 때문에, 선택사항이라는 점을 유의해야 합니다. +이 경우에는 본문에서 가져올 `item`이 선택사항이라는 점을 유의하세요. 기본값이 `None`이기 때문입니다. /// -## 다중 본문 매개변수 +## 다중 본문 매개변수 { #multiple-body-parameters } -이전 예제에서 보듯이, *경로 작동*은 아래와 같이 `Item` 속성을 가진 JSON 본문을 예상합니다: +이전 예제에서, *경로 처리*는 아래처럼 `Item`의 속성을 가진 JSON 본문을 예상합니다: ```JSON { @@ -33,11 +31,12 @@ 하지만, 다중 본문 매개변수 역시 선언할 수 있습니다. 예. `item`과 `user`: -{* ../../docs_src/body_multiple_params/tutorial002.py hl[22] *} +{* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *} -이 경우에, **FastAPI**는 이 함수 안에 한 개 이상의 본문 매개변수(Pydantic 모델인 두 매개변수)가 있다고 알 것입니다. -그래서, 본문의 매개변수 이름을 키(필드 명)로 사용할 수 있고, 다음과 같은 본문을 예측합니다: +이 경우에, **FastAPI**는 이 함수에 본문 매개변수가 1개보다 많다는 것을 알아챌 것입니다(두 매개변수가 Pydantic 모델입니다). + +그래서, 본문에서 매개변수 이름을 키(필드 이름)로 사용하고, 다음과 같은 본문을 예상합니다: ```JSON { @@ -56,29 +55,28 @@ /// note | 참고 -이전과 같이 `item`이 선언 되었더라도, 본문 내의 `item` 키가 있을 것이라고 예측합니다. +`item`이 이전과 같은 방식으로 선언되었더라도, 이제는 본문에서 `item` 키 안에 있을 것으로 예상된다는 점을 유의하세요. /// -FastAPI는 요청을 자동으로 변환해, 매개변수의 `item`과 `user`를 특별한 내용으로 받도록 할 것입니다. +**FastAPI**는 요청에서 자동으로 변환을 수행하여, 매개변수 `item`이 해당하는 내용을 받고 `user`도 마찬가지로 받도록 합니다. -복합 데이터의 검증을 수행하고 OpenAPI 스키마 및 자동 문서를 문서화합니다. +복합 데이터의 검증을 수행하고, OpenAPI 스키마 및 자동 문서에도 그에 맞게 문서화합니다. -## 본문 내의 단일 값 +## 본문 내의 단일 값 { #singular-values-in-body } -쿼리 및 경로 매개변수에 대한 추가 데이터를 정의하는 `Query`와 `Path`와 같이, **FastAPI**는 동등한 `Body`를 제공합니다. +쿼리 및 경로 매개변수에 대한 추가 데이터를 정의하는 `Query`와 `Path`가 있는 것과 같은 방식으로, **FastAPI**는 동등한 `Body`를 제공합니다. -예를 들어 이전의 모델을 확장하면, `item`과 `user`와 동일한 본문에 또 다른 `importance`라는 키를 갖도록 할 수있습니다. +예를 들어 이전 모델을 확장해서, `item`과 `user` 외에도 같은 본문에 `importance`라는 다른 키를 두고 싶을 수 있습니다. -단일 값을 그대로 선언한다면, **FastAPI**는 쿼리 매개변수로 가정할 것입니다. +단일 값이므로 그대로 선언하면, **FastAPI**는 이를 쿼리 매개변수라고 가정할 것입니다. -하지만, **FastAPI**의 `Body`를 사용해 다른 본문 키로 처리하도록 제어할 수 있습니다: +하지만 `Body`를 사용하여 다른 본문 키로 처리하도록 **FastAPI**에 지시할 수 있습니다: + +{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} -{* ../../docs_src/body_multiple_params/tutorial003.py hl[23] *} - -이 경우에는 **FastAPI**는 본문을 이와 같이 예측할 것입니다: - +이 경우에는 **FastAPI**가 다음과 같은 본문을 예상할 것입니다: ```JSON { @@ -96,58 +94,55 @@ FastAPI는 요청을 자동으로 변환해, 매개변수의 `item`과 `user`를 } ``` -다시 말해, 데이터 타입, 검증, 문서 등을 변환합니다. +다시 말해, 데이터 타입을 변환하고, 검증하고, 문서화하는 등의 작업을 수행합니다. -## 다중 본문 매개변수와 쿼리 +## 다중 본문 매개변수와 쿼리 { #multiple-body-params-and-query } -당연히, 필요할 때마다 추가적인 쿼리 매개변수를 선언할 수 있고, 이는 본문 매개변수에 추가됩니다. +물론, 필요할 때마다 어떤 본문 매개변수에 추가로 쿼리 매개변수도 선언할 수 있습니다. -기본적으로 단일 값은 쿼리 매개변수로 해석되므로, 명시적으로 `Query`를 추가할 필요가 없고, 아래처럼 할 수 있습니다: - -{* ../../docs_src/body_multiple_params/tutorial004.py hl[27] *} - -이렇게: +기본적으로 단일 값은 쿼리 매개변수로 해석되므로, 명시적으로 `Query`를 추가할 필요 없이 이렇게 하면 됩니다: ```Python -q: Optional[str] = None +q: Union[str, None] = None ``` +또는 Python 3.10 이상에서는: + +```Python +q: str | None = None +``` + +예를 들어: + +{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} + + /// info | 정보 -`Body` 또한 `Query`, `Path` 그리고 이후에 볼 다른 것들처럼 동일한 추가 검증과 메타데이터 매개변수를 갖고 있습니다. +`Body` 또한 `Query`, `Path` 그리고 이후에 볼 다른 것들과 마찬가지로 동일한 추가 검증과 메타데이터 매개변수를 모두 갖고 있습니다. /// -## 단일 본문 매개변수 삽입하기 +## 단일 본문 매개변수 삽입하기 { #embed-a-single-body-parameter } -Pydantic 모델 `Item`의 `item`을 본문 매개변수로 오직 한개만 갖고있다고 하겠습니다. +Pydantic 모델 `Item`에서 가져온 단일 `item` 본문 매개변수만 있다고 하겠습니다. -기본적으로 **FastAPI**는 직접 본문으로 예측할 것입니다. +기본적으로 **FastAPI**는 그 본문을 직접 예상합니다. -하지만, 만약 모델 내용에 `item `키를 가진 JSON으로 예측하길 원한다면, 추가적인 본문 매개변수를 선언한 것처럼 `Body`의 특별한 매개변수인 `embed`를 사용할 수 있습니다: - -{* ../../docs_src/body_multiple_params/tutorial005.py hl[17] *} - -아래 처럼: +하지만 추가 본문 매개변수를 선언할 때처럼, `item` 키를 가지고 그 안에 모델 내용이 들어 있는 JSON을 예상하게 하려면, `Body`의 특별한 매개변수 `embed`를 사용할 수 있습니다: ```Python -item: Item = Body(..., embed=True) +item: Item = Body(embed=True) ``` -이 경우에 **FastAPI**는 본문을 아래 대신에: +다음과 같이요: + +{* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} + + +이 경우 **FastAPI**는 다음과 같은 본문을 예상합니다: ```JSON hl_lines="2" -{ - "name": "Foo", - "description": "The pretender", - "price": 42.0, - "tax": 3.2 -} -``` - -아래 처럼 예측할 것 입니다: - -```JSON { "item": { "name": "Foo", @@ -158,12 +153,23 @@ item: Item = Body(..., embed=True) } ``` -## 정리 +다음 대신에: -요청이 단 한개의 본문을 가지고 있더라도, *경로 작동 함수*로 다중 본문 매개변수를 추가할 수 있습니다. +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` -하지만, **FastAPI**는 이를 처리하고, 함수에 올바른 데이터를 제공하며, *경로 작동*으로 올바른 스키마를 검증하고 문서화 합니다. +## 정리 { #recap } -또한, 단일 값을 본문의 일부로 받도록 선언할 수 있습니다. +요청은 본문을 하나만 가질 수 있지만, *경로 처리 함수*에 다중 본문 매개변수를 추가할 수 있습니다. -그리고 **FastAPI**는 단 한개의 매개변수가 선언 되더라도, 본문 내의 키로 삽입 시킬 수 있습니다. +하지만 **FastAPI**는 이를 처리하고, 함수에 올바른 데이터를 제공하며, *경로 처리*에서 올바른 스키마를 검증하고 문서화합니다. + +또한 단일 값을 본문의 일부로 받도록 선언할 수 있습니다. + +그리고 단 하나의 매개변수만 선언되어 있더라도, **FastAPI**에 본문을 키 안에 삽입하도록 지시할 수 있습니다. diff --git a/docs/ko/docs/tutorial/body-nested-models.md b/docs/ko/docs/tutorial/body-nested-models.md index ebd7b3ba6..4a8c1afc1 100644 --- a/docs/ko/docs/tutorial/body-nested-models.md +++ b/docs/ko/docs/tutorial/body-nested-models.md @@ -1,35 +1,26 @@ -# 본문 - 중첩 모델 +# 본문 - 중첩 모델 { #body-nested-models } -**FastAPI**를 이용하면 (Pydantic 덕분에) 단독으로 깊이 중첩된 모델을 정의, 검증, 문서화하며 사용할 수 있습니다. -## 리스트 필드 +**FastAPI**를 사용하면 (Pydantic 덕분에) 임의로 깊게 중첩된 모델을 정의, 검증, 문서화하고 사용할 수 있습니다. + +## 리스트 필드 { #list-fields } 어트리뷰트를 서브타입으로 정의할 수 있습니다. 예를 들어 파이썬 `list`는: -{* ../../docs_src/body_nested_models/tutorial001.py hl[14] *} +{* ../../docs_src/body_nested_models/tutorial001_py310.py hl[12] *} -이는 `tags`를 항목 리스트로 만듭니다. 각 항목의 타입을 선언하지 않더라도요. +이는 `tags`를 리스트로 만들지만, 리스트 요소의 타입을 선언하지는 않습니다. -## 타입 매개변수가 있는 리스트 필드 +## 타입 매개변수가 있는 리스트 필드 { #list-fields-with-type-parameter } -하지만 파이썬은 내부의 타입이나 "타입 매개변수"를 선언할 수 있는 특정 방법이 있습니다: +하지만 파이썬에는 내부 타입, 즉 "타입 매개변수"를 사용해 리스트를 선언하는 특정한 방법이 있습니다: -### typing의 `List` 임포트 +### 타입 매개변수로 `list` 선언 { #declare-a-list-with-a-type-parameter } -먼저, 파이썬 표준 `typing` 모듈에서 `List`를 임포트합니다: - -{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} - -### 타입 매개변수로 `List` 선언 - -`list`, `dict`, `tuple`과 같은 타입 매개변수(내부 타입)를 갖는 타입을 선언하려면: - -* `typing` 모듈에서 임포트 -* 대괄호를 사용하여 "타입 매개변수"로 내부 타입 전달: `[` 및 `]` +`list`, `dict`, `tuple`처럼 타입 매개변수(내부 타입)를 갖는 타입을 선언하려면, +대괄호 `[` 및 `]`를 사용해 내부 타입(들)을 "타입 매개변수"로 전달하세요. ```Python -from typing import List - -my_list: List[str] +my_list: list[str] ``` 이 모든 것은 타입 선언을 위한 표준 파이썬 문법입니다. @@ -38,45 +29,45 @@ my_list: List[str] 마찬가지로 예제에서 `tags`를 구체적으로 "문자열의 리스트"로 만들 수 있습니다: -{* ../../docs_src/body_nested_models/tutorial002.py hl[14] *} +{* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} -## 집합 타입 +## 집합 타입 { #set-types } -그런데 생각해보니 태그는 반복되면 안 되고, 고유한(Unique) 문자열이어야 할 것 같습니다. +그런데 생각해보니 태그는 반복되면 안 되고, 아마 고유한 문자열이어야 할 것입니다. -그리고 파이썬은 집합을 위한 특별한 데이터 타입 `set`이 있습니다. +그리고 파이썬에는 고유한 항목들의 집합을 위한 특별한 데이터 타입 `set`이 있습니다. -그렇다면 `Set`을 임포트 하고 `tags`를 `str`의 `set`으로 선언할 수 있습니다: +그렇다면 `tags`를 문자열의 집합으로 선언할 수 있습니다: -{* ../../docs_src/body_nested_models/tutorial003.py hl[1,14] *} +{* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} -덕분에 중복 데이터가 있는 요청을 수신하더라도 고유한 항목들의 집합으로 변환됩니다. +이렇게 하면 중복 데이터가 있는 요청을 받더라도 고유한 항목들의 집합으로 변환됩니다. -그리고 해당 데이터를 출력 할 때마다 소스에 중복이 있더라도 고유한 항목들의 집합으로 출력됩니다. +그리고 해당 데이터를 출력할 때마다, 소스에 중복이 있더라도 고유한 항목들의 집합으로 출력됩니다. 또한 그에 따라 주석이 생기고 문서화됩니다. -## 중첩 모델 +## 중첩 모델 { #nested-models } Pydantic 모델의 각 어트리뷰트는 타입을 갖습니다. -그런데 해당 타입 자체로 또다른 Pydantic 모델의 타입이 될 수 있습니다. +그런데 그 타입 자체가 또 다른 Pydantic 모델일 수 있습니다. -그러므로 특정한 어트리뷰트의 이름, 타입, 검증을 사용하여 깊게 중첩된 JSON "객체"를 선언할 수 있습니다. +따라서 특정한 어트리뷰트 이름, 타입, 검증을 사용하여 깊게 중첩된 JSON "객체"를 선언할 수 있습니다. -모든 것이 단독으로 중첩됩니다. +모든 것이 임의의 깊이로 중첩됩니다. -### 서브모델 정의 +### 서브모델 정의 { #define-a-submodel } -예를 들어, `Image` 모델을 선언할 수 있습니다: +예를 들어, `Image` 모델을 정의할 수 있습니다: -{* ../../docs_src/body_nested_models/tutorial004.py hl[9:11] *} +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} -### 서브모듈을 타입으로 사용 +### 서브모델을 타입으로 사용 { #use-the-submodel-as-a-type } -그리고 어트리뷰트의 타입으로 사용할 수 있습니다: +그리고 이를 어트리뷰트의 타입으로 사용할 수 있습니다: -{* ../../docs_src/body_nested_models/tutorial004.py hl[20] *} +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[18] *} 이는 **FastAPI**가 다음과 유사한 본문을 기대한다는 것을 의미합니다: @@ -94,32 +85,32 @@ Pydantic 모델의 각 어트리뷰트는 타입을 갖습니다. } ``` -다시 한번, **FastAPI**를 사용하여 해당 선언을 함으로써 얻는 것은: +다시 한번, **FastAPI**로 그 선언만 해도 얻는 것은: * 중첩 모델도 편집기 지원(자동완성 등) * 데이터 변환 * 데이터 검증 * 자동 문서화 -## 특별한 타입과 검증 +## 특별한 타입과 검증 { #special-types-and-validation } -`str`, `int`, `float` 등과 같은 단일 타입과는 별개로, `str`을 상속하는 더 복잡한 단일 타입을 사용할 수 있습니다. +`str`, `int`, `float` 등과 같은 일반적인 단일 타입과는 별개로, `str`을 상속하는 더 복잡한 단일 타입을 사용할 수 있습니다. -모든 옵션을 보려면, <a href="https://docs.pydantic.dev/latest/concepts/types/" class="external-link" target="_blank">Pydantic's exotic types</a> 문서를 확인하세요. 다음 장에서 몇가지 예제를 볼 수 있습니다. +사용할 수 있는 모든 옵션을 보려면 <a href="https://docs.pydantic.dev/latest/concepts/types/" class="external-link" target="_blank">Pydantic의 Type Overview</a>를 확인하세요. 다음 장에서 몇 가지 예제를 볼 수 있습니다. -예를 들어 `Image` 모델 안에 `url` 필드를 `str` 대신 Pydantic의 `HttpUrl`로 선언할 수 있습니다: +예를 들어 `Image` 모델에는 `url` 필드가 있으므로, 이를 `str` 대신 Pydantic의 `HttpUrl` 인스턴스로 선언할 수 있습니다: -{* ../../docs_src/body_nested_models/tutorial005.py hl[4,10] *} +{* ../../docs_src/body_nested_models/tutorial005_py310.py hl[2,8] *} -이 문자열이 유효한 URL인지 검사하고 JSON 스키마/OpenAPI로 문서화 됩니다. +이 문자열은 유효한 URL인지 검사되며, JSON Schema / OpenAPI에도 그에 맞게 문서화됩니다. -## 서브모델 리스트를 갖는 어트리뷰트 +## 서브모델 리스트를 갖는 어트리뷰트 { #attributes-with-lists-of-submodels } `list`, `set` 등의 서브타입으로 Pydantic 모델을 사용할 수도 있습니다: -{* ../../docs_src/body_nested_models/tutorial006.py hl[20] *} +{* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *} -아래와 같은 JSON 본문으로 예상(변환, 검증, 문서화 등을)합니다: +아래와 같은 JSON 본문을 예상(변환, 검증, 문서화 등)합니다: ```JSON hl_lines="11" { @@ -147,84 +138,84 @@ Pydantic 모델의 각 어트리뷰트는 타입을 갖습니다. /// info | 정보 -`images` 키가 어떻게 이미지 객체 리스트를 갖는지 주목하세요. +`images` 키가 이제 이미지 객체 리스트를 갖는지 주목하세요. /// -## 깊게 중첩된 모델 +## 깊게 중첩된 모델 { #deeply-nested-models } -단독으로 깊게 중첩된 모델을 정의할 수 있습니다: +임의로 깊게 중첩된 모델을 정의할 수 있습니다: -{* ../../docs_src/body_nested_models/tutorial007.py hl[9,14,20,23,27] *} +{* ../../docs_src/body_nested_models/tutorial007_py310.py hl[7,12,18,21,25] *} /// info | 정보 -`Offer`가 선택사항 `Image` 리스트를 차례로 갖는 `Item` 리스트를 어떻게 가지고 있는지 주목하세요 +`Offer`가 `Item`의 리스트를 가지고, 그 `Item`이 다시 선택 사항인 `Image` 리스트를 갖는지 주목하세요 /// -## 순수 리스트의 본문 +## 순수 리스트의 본문 { #bodies-of-pure-lists } -예상되는 JSON 본문의 최상위 값이 JSON `array`(파이썬 `list`)면, Pydantic 모델에서와 마찬가지로 함수의 매개변수에서 타입을 선언할 수 있습니다: +예상되는 JSON 본문의 최상위 값이 JSON `array`(파이썬 `list`)라면, Pydantic 모델에서와 마찬가지로 함수의 매개변수에서 타입을 선언할 수 있습니다: ```Python -images: List[Image] +images: list[Image] ``` 이를 아래처럼: -{* ../../docs_src/body_nested_models/tutorial008.py hl[15] *} +{* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *} -## 어디서나 편집기 지원 +## 어디서나 편집기 지원 { #editor-support-everywhere } -그리고 어디서나 편집기 지원을 받을수 있습니다. +그리고 어디서나 편집기 지원을 받을 수 있습니다. 리스트 내부 항목의 경우에도: <img src="/img/tutorial/body-nested-models/image01.png"> -Pydantic 모델 대신에 `dict`를 직접 사용하여 작업할 경우, 이러한 편집기 지원을 받을수 없습니다. +Pydantic 모델 대신 `dict`로 직접 작업한다면 이런 종류의 편집기 지원을 받을 수 없습니다. -하지만 수신한 딕셔너리가 자동으로 변환되고 출력도 자동으로 JSON으로 변환되므로 걱정할 필요는 없습니다. +하지만 그 부분에 대해서도 걱정할 필요는 없습니다. 들어오는 dict는 자동으로 변환되고, 출력도 자동으로 JSON으로 변환됩니다. -## 단독 `dict`의 본문 +## 임의의 `dict` 본문 { #bodies-of-arbitrary-dicts } -일부 타입의 키와 다른 타입의 값을 사용하여 `dict`로 본문을 선언할 수 있습니다. +또한 키는 어떤 타입이고 값은 다른 타입인 `dict`로 본문을 선언할 수 있습니다. -(Pydantic을 사용한 경우처럼) 유효한 필드/어트리뷰트 이름이 무엇인지 알 필요가 없습니다. +이렇게 하면 (Pydantic 모델을 사용하는 경우처럼) 유효한 필드/어트리뷰트 이름이 무엇인지 미리 알 필요가 없습니다. -아직 모르는 키를 받으려는 경우 유용합니다. +아직 모르는 키를 받으려는 경우에 유용합니다. --- -다른 유용한 경우는 다른 타입의 키를 가질 때입니다. 예. `int`. +또 다른 유용한 경우는 다른 타입(예: `int`)의 키를 갖고 싶을 때입니다. 여기서 그 경우를 볼 것입니다. -이 경우, `float` 값을 가진 `int` 키가 있는 모든 `dict`를 받아들입니다: +이 경우, `int` 키와 `float` 값을 가진 한 어떤 `dict`든 받아들입니다: -{* ../../docs_src/body_nested_models/tutorial009.py hl[15] *} +{* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *} /// tip | 팁 -JSON은 오직 `str`형 키만 지원한다는 것을 염두에 두세요. +JSON은 키로 `str`만 지원한다는 것을 염두에 두세요. -하지만 Pydantic은 자동 데이터 변환이 있습니다. +하지만 Pydantic은 자동 데이터 변환 기능이 있습니다. -즉, API 클라이언트가 문자열을 키로 보내더라도 해당 문자열이 순수한 정수를 포함하는한 Pydantic은 이를 변환하고 검증합니다. +즉, API 클라이언트는 키로 문자열만 보낼 수 있더라도, 해당 문자열이 순수한 정수를 포함하기만 하면 Pydantic이 이를 변환하고 검증합니다. -그러므로 `weights`로 받은 `dict`는 실제로 `int` 키와 `float` 값을 가집니다. +그리고 `weights`로 받는 `dict`는 실제로 `int` 키와 `float` 값을 갖게 됩니다. /// -## 요약 +## 요약 { #recap } -**FastAPI**를 사용하면 Pydantic 모델이 제공하는 최대 유연성을 확보하면서 코드를 간단하고 짧게, 그리고 우아하게 유지할 수 있습니다. +**FastAPI**를 사용하면 Pydantic 모델이 제공하는 최대 유연성을 확보하면서 코드를 간단하고 짧고 우아하게 유지할 수 있습니다. -물론 아래의 이점도 있습니다: +하지만 아래의 모든 이점도 있습니다: -* 편집기 지원 (자동완성이 어디서나!) -* 데이터 변환 (일명 파싱/직렬화) +* 편집기 지원(어디서나 자동완성!) +* 데이터 변환(일명 파싱/직렬화) * 데이터 검증 * 스키마 문서화 * 자동 문서 diff --git a/docs/ko/docs/tutorial/body.md b/docs/ko/docs/tutorial/body.md index b3914fa4b..1e66c60c2 100644 --- a/docs/ko/docs/tutorial/body.md +++ b/docs/ko/docs/tutorial/body.md @@ -1,10 +1,10 @@ -# 요청 본문 +# 요청 본문 { #request-body } 클라이언트(브라우저라고 해봅시다)로부터 여러분의 API로 데이터를 보내야 할 때, **요청 본문**으로 보냅니다. **요청** 본문은 클라이언트에서 API로 보내지는 데이터입니다. **응답** 본문은 API가 클라이언트로 보내는 데이터입니다. -여러분의 API는 대부분의 경우 **응답** 본문을 보내야 합니다. 하지만 클라이언트는 **요청** 본문을 매 번 보낼 필요가 없습니다. +여러분의 API는 대부분의 경우 **응답** 본문을 보내야 합니다. 하지만 클라이언트는 항상 **요청 본문**을 보낼 필요는 없고, 때로는 (쿼리 매개변수와 함께) 어떤 경로만 요청하고 본문은 보내지 않을 수도 있습니다. **요청** 본문을 선언하기 위해서 모든 강력함과 이점을 갖춘 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> 모델을 사용합니다. @@ -18,13 +18,13 @@ /// -## Pydantic의 `BaseModel` 임포트 +## Pydantic의 `BaseModel` 임포트 { #import-pydantics-basemodel } 먼저 `pydantic`에서 `BaseModel`를 임포트해야 합니다: {* ../../docs_src/body/tutorial001_py310.py hl[2] *} -## 여러분의 데이터 모델 만들기 +## 여러분의 데이터 모델 만들기 { #create-your-data-model } `BaseModel`를 상속받은 클래스로 여러분의 데이터 모델을 선언합니다. @@ -32,6 +32,7 @@ {* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} + 쿼리 매개변수를 선언할 때와 같이, 모델 어트리뷰트가 기본 값을 가지고 있어도 이는 필수가 아닙니다. 그외에는 필수입니다. 그저 `None`을 사용하여 선택적으로 만들 수 있습니다. 예를 들면, 위의 이 모델은 JSON "`object`" (혹은 파이썬 `dict`)을 다음과 같이 선언합니다: @@ -39,7 +40,7 @@ ```JSON { "name": "Foo", - "description": "선택적인 설명란", + "description": "An optional description", "price": 45.2, "tax": 3.5 } @@ -54,15 +55,15 @@ } ``` -## 매개변수로서 선언하기 +## 매개변수로서 선언하기 { #declare-it-as-a-parameter } -여러분의 *경로 작동*에 추가하기 위해, 경로 매개변수 그리고 쿼리 매개변수에서 선언했던 것과 같은 방식으로 선언하면 됩니다. +여러분의 *경로 처리*에 추가하기 위해, 경로 매개변수 그리고 쿼리 매개변수에서 선언했던 것과 같은 방식으로 선언하면 됩니다. {* ../../docs_src/body/tutorial001_py310.py hl[16] *} ...그리고 만들어낸 모델인 `Item`으로 타입을 선언합니다. -## 결과 +## 결과 { #results } 위에서의 단순한 파이썬 타입 선언으로, **FastAPI**는 다음과 같이 동작합니다: @@ -72,20 +73,20 @@ * 만약 데이터가 유효하지 않다면, 정확히 어떤 것이 그리고 어디에서 데이터가 잘 못 되었는지 지시하는 친절하고 명료한 에러를 반환할 것입니다. * 매개변수 `item`에 포함된 수신 데이터를 제공합니다. * 함수 내에서 매개변수를 `Item` 타입으로 선언했기 때문에, 모든 어트리뷰트와 그에 대한 타입에 대한 편집기 지원(완성 등)을 또한 받을 수 있습니다. -* 여러분의 모델을 위한 <a href="https://json-schema.org" class="external-link" target="_blank">JSON 스키마</a> 정의를 생성합니다. 여러분의 프로젝트에 적합하다면 여러분이 사용하고 싶은 곳 어디에서나 사용할 수 있습니다. -* 이러한 스키마는, 생성된 OpenAPI 스키마 일부가 될 것이며, 자동 문서화 <abbr title="사용자 인터페이스">UI</abbr>에 사용됩니다. +* 여러분의 모델을 위한 <a href="https://json-schema.org" class="external-link" target="_blank">JSON Schema</a> 정의를 생성합니다. 여러분의 프로젝트에 적합하다면 여러분이 사용하고 싶은 곳 어디에서나 사용할 수 있습니다. +* 이러한 스키마는, 생성된 OpenAPI 스키마 일부가 될 것이며, 자동 문서화 <abbr title="User Interfaces – 사용자 인터페이스">UIs</abbr>에 사용됩니다. -## 자동 문서화 +## 자동 문서화 { #automatic-docs } 모델의 JSON 스키마는 생성된 OpenAPI 스키마에 포함되며 대화형 API 문서에 표시됩니다: <img src="/img/tutorial/body/image01.png"> -이를 필요로 하는 각각의 *경로 작동*내부의 API 문서에도 사용됩니다: +이를 필요로 하는 각각의 *경로 처리* 내부의 API 문서에도 사용됩니다: <img src="/img/tutorial/body/image02.png"> -## 편집기 지원 +## 편집기 지원 { #editor-support } 편집기에서, 함수 내에서 타입 힌트와 완성을 어디서나 (만약 Pydantic model 대신에 `dict`을 받을 경우 나타나지 않을 수 있습니다) 받을 수 있습니다: @@ -97,13 +98,13 @@ 단순한 우연이 아닙니다. 프레임워크 전체가 이러한 디자인을 중심으로 설계되었습니다. -그 어떤 실행 전에, 모든 편집기에서 작동할 수 있도록 보장하기 위해 설계 단계에서 혹독하게 테스트되었습니다. +그 어떤 구현 전에, 모든 편집기에서 작동할 수 있도록 보장하기 위해 설계 단계에서 혹독하게 테스트되었습니다. 이를 지원하기 위해 Pydantic 자체에서 몇몇 변경점이 있었습니다. 이전 스크린샷은 <a href="https://code.visualstudio.com" class="external-link" target="_blank">Visual Studio Code</a>를 찍은 것입니다. -하지만 똑같은 편집기 지원을 <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a>에서 받을 수 있거나, 대부분의 다른 편집기에서도 받을 수 있습니다: +하지만 똑같은 편집기 지원을 <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a>와 대부분의 다른 파이썬 편집기에서도 받을 수 있습니다: <img src="/img/tutorial/body/image05.png"> @@ -113,21 +114,21 @@ 다음 사항을 포함해 Pydantic 모델에 대한 편집기 지원을 향상시킵니다: -* 자동 완성 -* 타입 확인 -* 리팩토링 -* 검색 -* 점검 +* auto-completion +* type checks +* refactoring +* searching +* inspections /// -## 모델 사용하기 +## 모델 사용하기 { #use-the-model } 함수 안에서 모델 객체의 모든 어트리뷰트에 직접 접근 가능합니다: -{* ../../docs_src/body/tutorial002_py310.py hl[19] *} +{* ../../docs_src/body/tutorial002_py310.py *} -## 요청 본문 + 경로 매개변수 +## 요청 본문 + 경로 매개변수 { #request-body-path-parameters } 경로 매개변수와 요청 본문을 동시에 선언할 수 있습니다. @@ -135,7 +136,8 @@ {* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} -## 요청 본문 + 경로 + 쿼리 매개변수 + +## 요청 본문 + 경로 + 쿼리 매개변수 { #request-body-path-query-parameters } **본문**, **경로** 그리고 **쿼리** 매개변수 모두 동시에 선언할 수도 있습니다. @@ -153,10 +155,12 @@ FastAPI는 `q`의 값이 필요없음을 알게 될 것입니다. 기본 값이 `= None`이기 때문입니다. -`Union[str, None]`에 있는 `Union`은 FastAPI에 의해 사용된 것이 아니지만, 편집기로 하여금 더 나은 지원과 에러 탐지를 지원할 것입니다. +Python 3.10+의 `str | None` 또는 Python 3.9+의 `Union[str, None]`에 있는 `Union`은 FastAPI가 `q` 값이 필수가 아님을 판단하기 위해 사용하지 않습니다. 기본 값이 `= None`이기 때문에 필수가 아님을 알게 됩니다. + +하지만 타입 어노테이션을 추가하면 편집기가 더 나은 지원을 제공하고 오류를 감지할 수 있습니다. /// -## Pydantic없이 +## Pydantic없이 { #without-pydantic } -만약 Pydantic 모델을 사용하고 싶지 않다면, **Body** 매개변수를 사용할 수도 있습니다. [Body - 다중 매개변수: 본문에 있는 유일한 값](body-multiple-params.md#_2){.internal-link target=_blank} 문서를 확인하세요. +만약 Pydantic 모델을 사용하고 싶지 않다면, **Body** 매개변수를 사용할 수도 있습니다. [Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank} 문서를 확인하세요. diff --git a/docs/ko/docs/tutorial/cookie-param-models.md b/docs/ko/docs/tutorial/cookie-param-models.md index e7eef0b1d..00238d1b7 100644 --- a/docs/ko/docs/tutorial/cookie-param-models.md +++ b/docs/ko/docs/tutorial/cookie-param-models.md @@ -1,8 +1,8 @@ -# 쿠키 매개변수 모델 +# 쿠키 매개변수 모델 { #cookie-parameter-models } 관련있는 **쿠키**들의 그룹이 있는 경우, **Pydantic 모델**을 생성하여 선언할 수 있습니다. 🍪 -이를 통해 **여러 위치**에서 **모델을 재사용** 할 수 있고 모든 매개변수에 대한 유효성 검사 및 메타데이터를 한 번에 선언할 수도 있습니다. 😍 +이를 통해 **여러 위치**에서 **모델을 재사용** 할 수 있고 모든 매개변수에 대한 유효성 검사 및 메타데이터를 한 번에 선언할 수도 있습니다. 😎 /// note | 참고 @@ -16,7 +16,7 @@ /// -## Pydantic 모델을 사용한 쿠키 +## Pydantic 모델을 사용한 쿠키 { #cookies-with-a-pydantic-model } **Pydantic 모델**에 필요한 **쿠키** 매개변수를 선언한 다음, 해당 매개변수를 `Cookie`로 선언합니다: @@ -24,7 +24,7 @@ **FastAPI**는 요청에서 받은 **쿠키**에서 **각 필드**에 대한 데이터를 **추출**하고 정의한 Pydantic 모델을 줍니다. -## 문서 확인하기 +## 문서 확인하기 { #check-the-docs } 문서 UI `/docs`에서 정의한 쿠키를 볼 수 있습니다: @@ -36,27 +36,27 @@ 명심하세요, 내부적으로 **브라우저는 쿠키를 특별한 방식으로 처리**하기 때문에 **자바스크립트**가 쉽게 쿠키를 건드릴 수 **없습니다**. -`/docs`에서 **API 문서 UI**로 이동하면 *경로 작업*에 대한 쿠키의 **문서**를 볼 수 있습니다. +`/docs`에서 **API 문서 UI**로 이동하면 *경로 처리*에 대한 쿠키의 **문서**를 볼 수 있습니다. 하지만 아무리 **데이터를 입력**하고 "실행(Execute)"을 클릭해도, 문서 UI는 **자바스크립트**로 작동하기 때문에 쿠키는 전송되지 않고, 아무 값도 쓰지 않은 것처럼 **오류** 메시지를 보게 됩니다. /// -## 추가 쿠키 금지하기 +## 추가 쿠키 금지하기 { #forbid-extra-cookies } 일부 특별한 사용 사례(흔하지는 않겠지만)에서는 수신하려는 쿠키를 **제한**할 수 있습니다. -이제 API는 자신의 <abbr title="농담입니다, 혹시나 해서요. 쿠키 동의와 관련해서 할 수 있는 것은 없지만, 이제 API조차도 잘못된 쿠키를 거부할 수 있다는 점이 재밌습니다. 쿠키 드세요. 🍪">쿠키 동의</abbr>를 제어할 수 있는 권한을 갖게 되었습니다. 🤪🍪 +이제 API는 자신의 <abbr title="This is a joke, just in case. It has nothing to do with cookie consents, but it's funny that even the API can now reject the poor cookies. Have a cookie. 🍪">cookie consent</abbr>를 제어할 수 있는 권한을 갖게 되었습니다. 🤪🍪 Pydantic의 모델 구성을 사용하여 추가(`extra`) 필드를 금지(`forbid`)할 수 있습니다: -{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} +{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *} 클라이언트가 **추가 쿠키**를 보내려고 시도하면, **오류** 응답을 받게 됩니다. -<abbr title="이건 또 다른 농담입니다. 제 말에 귀 기울이지 마세요. 커피랑 쿠키 좀 드세요. ☕">API가 거부</abbr>하는데도 동의를 얻기 위해 애쓰는 불쌍한 쿠키 배너(팝업)들. 🍪 +동의를 얻기 위해 애쓰는 불쌍한 쿠키 배너(팝업)들, <abbr title="This is another joke. Don't pay attention to me. Have some coffee for your cookie. ☕">API가 거부</abbr>하는데도. 🍪 -예를 들어, 클라이언트가 `good-list-please` 값으로 `santa_tracker` 쿠키를 보내려고 하면 클라이언트는 `santa_tracker` <abbr title="산타는 쿠키가 부족한 것을 못마땅해합니다. 🎅 알겠습니다, 쿠키 농담은 이제 없습니다.">쿠키가 허용되지 않는다</abbr>는 **오류** 응답을 받게 됩니다: +예를 들어, 클라이언트가 `good-list-please` 값으로 `santa_tracker` 쿠키를 보내려고 하면 클라이언트는 `santa_tracker` <abbr title="Santa disapproves the lack of cookies. 🎅 Okay, no more cookie jokes.">쿠키가 허용되지 않는다</abbr>는 **오류** 응답을 받게 됩니다: ```json { @@ -71,6 +71,6 @@ Pydantic의 모델 구성을 사용하여 추가(`extra`) 필드를 금지(`forb } ``` -## 요약 +## 요약 { #summary } -**Pydantic 모델**을 사용하여 **FastAPI**에서 <abbr title="가기 전에 마지막 쿠키를 드세요. 🍪">**쿠키**</abbr>를 선언할 수 있습니다. 😍 +**Pydantic 모델**을 사용하여 **FastAPI**에서 <abbr title="Have a last cookie before you go. 🍪">**쿠키**</abbr>를 선언할 수 있습니다. 😎 diff --git a/docs/ko/docs/tutorial/cookie-params.md b/docs/ko/docs/tutorial/cookie-params.md index fba756d49..0591a5e96 100644 --- a/docs/ko/docs/tutorial/cookie-params.md +++ b/docs/ko/docs/tutorial/cookie-params.md @@ -1,14 +1,14 @@ -# 쿠키 매개변수 +# 쿠키 매개변수 { #cookie-parameters } 쿠키 매개변수를 `Query`와 `Path` 매개변수들과 같은 방식으로 정의할 수 있습니다. -## `Cookie` 임포트 +## `Cookie` 임포트 { #import-cookie } 먼저 `Cookie`를 임포트합니다: {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[3] *} -## `Cookie` 매개변수 선언 +## `Cookie` 매개변수 선언 { #declare-cookie-parameters } 그런 다음, `Path`와 `Query`처럼 동일한 구조를 사용하는 쿠키 매개변수를 선언합니다. @@ -20,7 +20,7 @@ `Cookie`는 `Path` 및 `Query`의 "자매"클래스입니다. 이 역시 동일한 공통 `Param` 클래스를 상속합니다. -`Query`, `Path`, `Cookie` 그리고 다른 것들은 `fastapi`에서 임포트 할 때, 실제로는 특별한 클래스를 반환하는 함수임을 기억하세요. +하지만 `fastapi`에서 `Query`, `Path`, `Cookie` 그리고 다른 것들을 임포트할 때, 실제로는 특별한 클래스를 반환하는 함수임을 기억하세요. /// @@ -30,6 +30,16 @@ /// -## 요약 +/// info | 정보 -`Cookie`는 `Query`, `Path`와 동일한 패턴을 사용하여 선언합니다. +**브라우저는 쿠키를** 내부적으로 특별한 방식으로 처리하기 때문에, **JavaScript**가 쉽게 쿠키를 다루도록 허용하지 않는다는 점을 염두에 두세요. + +`/docs`의 **API docs UI**로 이동하면 *경로 처리*에 대한 쿠키 **문서**를 확인할 수 있습니다. + +하지만 **데이터를 채우고** "Execute"를 클릭하더라도, docs UI는 **JavaScript**로 동작하기 때문에 쿠키가 전송되지 않고, 아무 값도 입력하지 않은 것처럼 **오류** 메시지를 보게 될 것입니다. + +/// + +## 요약 { #recap } + +`Query`와 `Path`에서 사용하는 것과 동일한 공통 패턴으로, `Cookie`를 사용해 쿠키를 선언합니다. diff --git a/docs/ko/docs/tutorial/cors.md b/docs/ko/docs/tutorial/cors.md index 1ef5a7480..0f3948a3d 100644 --- a/docs/ko/docs/tutorial/cors.md +++ b/docs/ko/docs/tutorial/cors.md @@ -1,10 +1,10 @@ -# 교차 출처 리소스 공유 +# CORS (교차-출처 리소스 공유) { #cors-cross-origin-resource-sharing } -<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">CORS 또는 "교차-출처 리소스 공유"</a>란, 브라우저에서 동작하는 프론트엔드가 자바스크립트로 코드로 백엔드와 통신하고, 백엔드는 해당 프론트엔드와 다른 "출처"에 존재하는 상황을 의미합니다. +<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">CORS 또는 "Cross-Origin Resource Sharing"</a>란, 브라우저에서 실행되는 프론트엔드에 백엔드와 통신하는 JavaScript 코드가 있고, 백엔드가 프론트엔드와 다른 "출처(origin)"에 있는 상황을 의미합니다. -## 출처 +## 출처 { #origin } -출처란 프로토콜(`http` , `https`), 도메인(`myapp.com`, `localhost`, `localhost.tiangolo.com` ), 그리고 포트(`80`, `443`, `8080` )의 조합을 의미합니다. +출처란 프로토콜(`http`, `https`), 도메인(`myapp.com`, `localhost`, `localhost.tiangolo.com`), 그리고 포트(`80`, `443`, `8080`)의 조합을 의미합니다. 따라서, 아래는 모두 상이한 출처입니다: @@ -12,74 +12,78 @@ * `https://localhost` * `http://localhost:8080` -모두 `localhost` 에 있지만, 서로 다른 프로토콜과 포트를 사용하고 있으므로 다른 "출처"입니다. +모두 `localhost`에 있더라도, 서로 다른 프로토콜이나 포트를 사용하므로 서로 다른 "출처"입니다. -## 단계 +## 단계 { #steps } -브라우저 내 `http://localhost:8080`에서 동작하는 프론트엔드가 있고, 자바스크립트는 `http://localhost`를 통해 백엔드와 통신한다고 가정해봅시다(포트를 명시하지 않는 경우, 브라우저는 `80` 을 기본 포트로 간주합니다). +그러면 브라우저에서 `http://localhost:8080`으로 실행되는 프론트엔드가 있고, 그 JavaScript가 `http://localhost`에서 실행되는 백엔드와 통신하려고 한다고 해봅시다(포트를 명시하지 않았기 때문에, 브라우저는 기본 포트 `80`을 가정합니다). -그러면 브라우저는 백엔드에 HTTP `OPTIONS` 요청을 보내고, 백엔드에서 이 다른 출처(`http://localhost:8080`)와의 통신을 허가하는 적절한 헤더를 보내면, 브라우저는 프론트엔드의 자바스크립트가 백엔드에 요청을 보낼 수 있도록 합니다. +그러면 브라우저는 `:80`-백엔드에 HTTP `OPTIONS` 요청을 보내고, 백엔드가 이 다른 출처(`http://localhost:8080`)로부터의 통신을 허가하는 적절한 헤더를 보내면, `:8080`-브라우저는 프론트엔드의 JavaScript가 `:80`-백엔드에 요청을 보낼 수 있도록 합니다. -이를 위해, 백엔드는 "허용된 출처(allowed origins)" 목록을 가지고 있어야만 합니다. +이를 위해, `:80`-백엔드는 "허용된 출처(allowed origins)" 목록을 가지고 있어야 합니다. -이 경우, 프론트엔드가 제대로 동작하기 위해 `http://localhost:8080`을 목록에 포함해야 합니다. +이 경우, `:8080`-프론트엔드가 올바르게 동작하려면 목록에 `http://localhost:8080`이 포함되어야 합니다. -## 와일드카드 +## 와일드카드 { #wildcards } -모든 출처를 허용하기 위해 목록을 `"*"` ("와일드카드")로 선언하는 것도 가능합니다. +또한 목록을 `"*"`("와일드카드")로 선언해 모두 허용된다고 말할 수도 있습니다. -하지만 이것은 특정한 유형의 통신만을 허용하며, 쿠키 및 액세스 토큰과 사용되는 인증 헤더(Authoriztion header) 등이 포함된 경우와 같이 자격 증명(credentials)이 포함된 통신은 허용되지 않습니다. +하지만 그러면 자격 증명(credentials)이 포함된 모든 것을 제외하고 특정 유형의 통신만 허용하게 됩니다. 예: 쿠키, Bearer Token에 사용되는 것과 같은 Authorization 헤더 등. -따라서 모든 작업을 의도한대로 실행하기 위해, 허용되는 출처를 명시적으로 지정하는 것이 좋습니다. +따라서 모든 것이 올바르게 동작하게 하려면, 허용된 출처를 명시적으로 지정하는 것이 더 좋습니다. -## `CORSMiddleware` 사용 +## `CORSMiddleware` 사용 { #use-corsmiddleware } -`CORSMiddleware` 을 사용하여 **FastAPI** 응용 프로그램의 교차 출처 리소스 공유 환경을 설정할 수 있습니다. +`CORSMiddleware`를 사용하여 **FastAPI** 애플리케이션에서 이를 설정할 수 있습니다. -* `CORSMiddleware` 임포트. -* 허용되는 출처(문자열 형식)의 리스트 생성. -* FastAPI 응용 프로그램에 "미들웨어(middleware)"로 추가. +* `CORSMiddleware`를 임포트합니다. +* 허용된 출처(문자열)의 리스트를 생성합니다. +* **FastAPI** 애플리케이션에 "미들웨어(middleware)"로 추가합니다. -백엔드에서 다음의 사항을 허용할지에 대해 설정할 수도 있습니다: +또한 백엔드가 다음을 허용할지 여부도 지정할 수 있습니다: -* 자격증명 (인증 헤더, 쿠키 등). -* 특정한 HTTP 메소드(`POST`, `PUT`) 또는 와일드카드 `"*"` 를 사용한 모든 HTTP 메소드. -* 특정한 HTTP 헤더 또는 와일드카드 `"*"` 를 사용한 모든 HTTP 헤더. +* 자격 증명(Authorization 헤더, 쿠키 등). +* 특정 HTTP 메서드(`POST`, `PUT`) 또는 와일드카드 `"*"`를 사용한 모든 메서드. +* 특정 HTTP 헤더 또는 와일드카드 `"*"`를 사용한 모든 헤더. -{* ../../docs_src/cors/tutorial001.py hl[2,6:11,13:19] *} +{* ../../docs_src/cors/tutorial001_py39.py hl[2,6:11,13:19] *} -`CORSMiddleware` 에서 사용하는 기본 매개변수는 제한적이므로, 브라우저가 교차-도메인 상황에서 특정한 출처, 메소드, 헤더 등을 사용할 수 있도록 하려면 이들을 명시적으로 허용해야 합니다. -다음의 인자들이 지원됩니다: +`CORSMiddleware` 구현에서 사용하는 기본 매개변수는 기본적으로 제한적이므로, 브라우저가 Cross-Domain 컨텍스트에서 특정 출처, 메서드 또는 헤더를 사용할 수 있도록 하려면 이를 명시적으로 활성화해야 합니다. -* `allow_origins` - 교차-출처 요청을 보낼 수 있는 출처의 리스트입니다. 예) `['https://example.org', 'https://www.example.org']`. 모든 출처를 허용하기 위해 `['*']` 를 사용할 수 있습니다. -* `allow_origin_regex` - 교차-출처 요청을 보낼 수 있는 출처를 정규표현식 문자열로 나타냅니다. `'https://.*\.example\.org'`. -* `allow_methods` - 교차-출처 요청을 허용하는 HTTP 메소드의 리스트입니다. 기본값은 `['GET']` 입니다. `['*']` 을 사용하여 모든 표준 메소드들을 허용할 수 있습니다. -* `allow_headers` - 교차-출처를 지원하는 HTTP 요청 헤더의 리스트입니다. 기본값은 `[]` 입니다. 모든 헤더들을 허용하기 위해 `['*']` 를 사용할 수 있습니다. `Accept`, `Accept-Language`, `Content-Language` 그리고 `Content-Type` 헤더는 CORS 요청시 언제나 허용됩니다. -* `allow_credentials` - 교차-출처 요청시 쿠키 지원 여부를 설정합니다. 기본값은 `False` 입니다. 또한 해당 항목을 허용할 경우 `allow_origins` 는 `['*']` 로 설정할 수 없으며, 출처를 반드시 특정해야 합니다. -* `expose_headers` - 브라우저에 접근할 수 있어야 하는 모든 응답 헤더를 가리킵니다. 기본값은 `[]` 입니다. -* `max_age` - 브라우저가 CORS 응답을 캐시에 저장하는 최대 시간을 초 단위로 설정합니다. 기본값은 `600` 입니다. +다음 인자들이 지원됩니다: -미들웨어는 두가지 특정한 종류의 HTTP 요청에 응답합니다... +* `allow_origins` - 교차-출처 요청을 보낼 수 있도록 허용해야 하는 출처의 리스트입니다. 예: `['https://example.org', 'https://www.example.org']`. 모든 출처를 허용하려면 `['*']`를 사용할 수 있습니다. +* `allow_origin_regex` - 교차-출처 요청을 보낼 수 있도록 허용해야 하는 출처와 매칭할 정규표현식 문자열입니다. 예: `'https://.*\.example\.org'`. +* `allow_methods` - 교차-출처 요청에 허용되어야 하는 HTTP 메서드의 리스트입니다. 기본값은 `['GET']`입니다. 모든 표준 메서드를 허용하려면 `['*']`를 사용할 수 있습니다. +* `allow_headers` - 교차-출처 요청에 대해 지원되어야 하는 HTTP 요청 헤더의 리스트입니다. 기본값은 `[]`입니다. 모든 헤더를 허용하려면 `['*']`를 사용할 수 있습니다. `Accept`, `Accept-Language`, `Content-Language`, `Content-Type` 헤더는 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests" class="external-link" rel="noopener" target="_blank">단순 CORS 요청</a>에 대해 항상 허용됩니다. +* `allow_credentials` - 교차-출처 요청에 대해 쿠키를 지원해야 함을 나타냅니다. 기본값은 `False`입니다. -### CORS 사전 요청 + `allow_credentials`가 `True`로 설정된 경우 `allow_origins`, `allow_methods`, `allow_headers` 중 어느 것도 `['*']`로 설정할 수 없습니다. 모두 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#credentialed_requests_and_wildcards" class="external-link" rel="noopener" target="_blank">명시적으로 지정</a>되어야 합니다. -`Origin` 및 `Access-Control-Request-Method` 헤더와 함께 전송하는 모든 `OPTIONS` 요청입니다. +* `expose_headers` - 브라우저에서 접근 가능해야 하는 모든 응답 헤더를 나타냅니다. 기본값은 `[]`입니다. +* `max_age` - 브라우저가 CORS 응답을 캐시하는 최대 시간을 초 단위로 설정합니다. 기본값은 `600`입니다. -이 경우 미들웨어는 들어오는 요청을 가로채 적절한 CORS 헤더와, 정보 제공을 위한 `200` 또는 `400` 응답으로 응답합니다. +미들웨어는 두 가지 특정한 종류의 HTTP 요청에 응답합니다... -### 단순한 요청 +### CORS 사전 요청 { #cors-preflight-requests } -`Origin` 헤더를 가진 모든 요청. 이 경우 미들웨어는 요청을 정상적으로 전달하지만, 적절한 CORS 헤더를 응답에 포함시킵니다. +`Origin` 및 `Access-Control-Request-Method` 헤더가 있는 모든 `OPTIONS` 요청입니다. -## 더 많은 정보 +이 경우 미들웨어는 들어오는 요청을 가로채 적절한 CORS 헤더와 함께, 정보 제공 목적으로 `200` 또는 `400` 응답을 반환합니다. -<abbr title="교차-출처 리소스 공유">CORS</abbr>에 대한 더 많은 정보를 알고싶다면, <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/CORS" class="external-link" target="_blank">Mozilla CORS 문서</a>를 참고하기 바랍니다. +### 단순한 요청 { #simple-requests } -/// note | 기술적 세부 사항 +`Origin` 헤더가 있는 모든 요청입니다. 이 경우 미들웨어는 요청을 정상적으로 통과시키지만, 응답에 적절한 CORS 헤더를 포함합니다. -`from starlette.middleware.cors import CORSMiddleware` 역시 사용할 수 있습니다. +## 더 많은 정보 { #more-info } -**FastAPI**는 개발자인 당신의 편의를 위해 `fastapi.middleware` 에서 몇가지의 미들웨어를 제공합니다. 하지만 대부분의 미들웨어가 Stralette으로부터 직접 제공됩니다. +<abbr title="Cross-Origin Resource Sharing – 교차-출처 리소스 공유">CORS</abbr>에 대한 더 많은 정보는 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">Mozilla CORS 문서</a>를 참고하세요. + +/// note | 기술 세부사항 + +`from starlette.middleware.cors import CORSMiddleware`도 사용할 수 있습니다. + +**FastAPI**는 개발자인 여러분의 편의를 위해 `fastapi.middleware`에 여러 미들웨어를 제공합니다. 하지만 사용 가능한 미들웨어 대부분은 Starlette에서 직접 제공됩니다. /// diff --git a/docs/ko/docs/tutorial/debugging.md b/docs/ko/docs/tutorial/debugging.md index e42f1ba88..ca20acff6 100644 --- a/docs/ko/docs/tutorial/debugging.md +++ b/docs/ko/docs/tutorial/debugging.md @@ -1,14 +1,14 @@ -# 디버깅 +# 디버깅 { #debugging } 예를 들면 Visual Studio Code 또는 PyCharm을 사용하여 편집기에서 디버거를 연결할 수 있습니다. -## `uvicorn` 호출 +## `uvicorn` 호출 { #call-uvicorn } -FastAPI 애플리케이션에서 `uvicorn`을 직접 임포트하여 실행합니다 +FastAPI 애플리케이션에서 `uvicorn`을 직접 임포트하여 실행합니다: -{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} -### `__name__ == "__main__"` 에 대하여 +### `__name__ == "__main__"` 에 대하여 { #about-name-main } `__name__ == "__main__"`의 주요 목적은 다음과 같이 파일이 호출될 때 실행되는 일부 코드를 갖는 것입니다. @@ -26,7 +26,7 @@ $ python myapp.py from myapp import app ``` -#### 추가 세부사항 +#### 추가 세부사항 { #more-details } 파일 이름이 `myapp.py`라고 가정해 보겠습니다. @@ -62,7 +62,7 @@ from myapp import app # Some more code ``` -이 경우 `myapp.py` 내부의 자동 변수에는 값이 `"__main__"`인 변수 `__name__`이 없습니다. +이 경우 `myapp.py` 내부의 자동 변수 `__name__`에는 값이 `"__main__"`이 들어가지 않습니다. 따라서 다음 행 @@ -74,11 +74,11 @@ from myapp import app /// info | 정보 -자세한 내용은 <a href="https://docs.python.org/3/library/__main__.html" class="external-link" target="_blank">공식 Python 문서</a>를 확인하세요 +자세한 내용은 <a href="https://docs.python.org/3/library/__main__.html" class="external-link" target="_blank">공식 Python 문서</a>를 확인하세요. /// -## 디버거로 코드 실행 +## 디버거로 코드 실행 { #run-your-code-with-your-debugger } 코드에서 직접 Uvicorn 서버를 실행하고 있기 때문에 디버거에서 직접 Python 프로그램(FastAPI 애플리케이션)을 호출할 수 있습니다. @@ -101,7 +101,7 @@ from myapp import app Pycharm을 사용하는 경우 다음을 수행할 수 있습니다 -* "Run" 메뉴를 엽니다 +* "Run" 메뉴를 엽니다. * "Debug..." 옵션을 선택합니다. * 그러면 상황에 맞는 메뉴가 나타납니다. * 디버그할 파일을 선택합니다(이 경우 `main.py`). diff --git a/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md index 3e5cdcc8c..68bba669a 100644 --- a/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md @@ -1,30 +1,30 @@ -# 의존성으로서의 클래스 +# 의존성으로서의 클래스 { #classes-as-dependencies } -**의존성 주입** 시스템에 대해 자세히 살펴보기 전에 이전 예제를 업그레이드 해보겠습니다. +**의존성 주입** 시스템에 대해 더 깊이 살펴보기 전에, 이전 예제를 업그레이드해 보겠습니다. -## 이전 예제의 `딕셔너리` +## 이전 예제의 `dict` { #a-dict-from-the-previous-example } -이전 예제에서, 우리는 의존성(의존 가능한) 함수에서 `딕셔너리`객체를 반환하고 있었습니다: +이전 예제에서는 의존성("dependable")에서 `dict`를 반환하고 있었습니다: -{* ../../docs_src/dependencies/tutorial001.py hl[9] *} +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[9] *} -우리는 *경로 작동 함수*의 매개변수 `commons`에서 `딕셔너리` 객체를 얻습니다. +하지만 그러면 *경로 처리 함수*의 매개변수 `commons`에서 `dict`를 받게 됩니다. -그리고 우리는 에디터들이 `딕셔너리` 객체의 키나 밸류의 자료형을 알 수 없기 때문에 자동 완성과 같은 기능을 제공해 줄 수 없다는 것을 알고 있습니다. +그리고 에디터는 `dict`의 키와 값 타입을 알 수 없기 때문에 `dict`에 대해서는 (완성 기능 같은) 많은 지원을 제공할 수 없다는 것을 알고 있습니다. -더 나은 방법이 있을 것 같습니다... +더 나은 방법이 있습니다... -## 의존성으로 사용 가능한 것 +## 의존성이 되기 위한 조건 { #what-makes-a-dependency } -지금까지 함수로 선언된 의존성을 봐왔습니다. +지금까지는 함수로 선언된 의존성을 봤습니다. -아마도 더 일반적이기는 하겠지만 의존성을 선언하는 유일한 방법은 아닙니다. +하지만 그것만이 의존성을 선언하는 유일한 방법은 아닙니다(아마도 더 일반적이긴 하겠지만요). -핵심 요소는 의존성이 "호출 가능"해야 한다는 것입니다 +핵심 요소는 의존성이 "호출 가능(callable)"해야 한다는 것입니다. -파이썬에서의 "**호출 가능**"은 파이썬이 함수처럼 "호출"할 수 있는 모든 것입니다. +파이썬에서 "**호출 가능(callable)**"이란 파이썬이 함수처럼 "호출"할 수 있는 모든 것입니다. -따라서, 만약 당신이 `something`(함수가 아닐 수도 있음) 객체를 가지고 있고, +따라서 `something`(함수가 _아닐_ 수도 있습니다)이라는 객체가 있고, 다음처럼 "호출"(실행)할 수 있다면: ```Python something() @@ -36,11 +36,11 @@ something() something(some_argument, some_keyword_argument="foo") ``` -상기와 같은 방식으로 "호출(실행)" 할 수 있다면 "호출 가능"이 됩니다. +그것은 "호출 가능(callable)"입니다. -## 의존성으로서의 클래스 +## 의존성으로서의 클래스 { #classes-as-dependencies_1 } -파이썬 클래스의 인스턴스를 생성하기 위해 사용하는 것과 동일한 문법을 사용한다는 걸 알 수 있습니다. +파이썬 클래스의 인스턴스를 만들 때도 같은 문법을 사용한다는 것을 알 수 있을 겁니다. 예를 들어: @@ -53,125 +53,236 @@ class Cat: fluffy = Cat(name="Mr Fluffy") ``` -이 경우에 `fluffy`는 클래스 `Cat`의 인스턴스입니다. 그리고 우리는 `fluffy`를 만들기 위해서 `Cat`을 "호출"했습니다. +이 경우 `fluffy`는 클래스 `Cat`의 인스턴스입니다. -따라서, 파이썬 클래스는 **호출 가능**합니다. +그리고 `fluffy`를 만들기 위해 `Cat`을 "호출"하고 있습니다. -그래서 **FastAPI**에서는 파이썬 클래스를 의존성으로 사용할 수 있습니다. +따라서 파이썬 클래스도 **호출 가능(callable)**합니다. -FastAPI가 실질적으로 확인하는 것은 "호출 가능성"(함수, 클래스 또는 다른 모든 것)과 정의된 매개변수들입니다. +그러면 **FastAPI**에서는 파이썬 클래스를 의존성으로 사용할 수 있습니다. -"호출 가능"한 것을 의존성으로서 **FastAPI**에 전달하면, 그 "호출 가능"한 것의 매개변수들을 분석한 후 이를 *경로 작동 함수*를 위한 매개변수와 동일한 방식으로 처리합니다. 하위-의존성 또한 같은 방식으로 처리합니다. +FastAPI가 실제로 확인하는 것은 그것이 "호출 가능(callable)"(함수, 클래스, 또는 다른 무엇이든)한지와 정의된 매개변수들입니다. -매개변수가 없는 "호출 가능"한 것 역시 매개변수가 없는 *경로 작동 함수*와 동일한 방식으로 적용됩니다. +**FastAPI**에서 "호출 가능(callable)"한 것을 의존성으로 넘기면, 그 "호출 가능(callable)"한 것의 매개변수들을 분석하고 *경로 처리 함수*의 매개변수와 동일한 방식으로 처리합니다. 하위 의존성도 포함해서요. -그래서, 우리는 위 예제에서의 `common_paramenters` 의존성을 클래스 `CommonQueryParams`로 바꿀 수 있습니다. +이것은 매개변수가 전혀 없는 callable에도 적용됩니다. 매개변수가 없는 *경로 처리 함수*에 적용되는 것과 동일합니다. -{* ../../docs_src/dependencies/tutorial002.py hl[11:15] *} +그러면 위의 의존성("dependable") `common_parameters`를 클래스 `CommonQueryParams`로 바꿀 수 있습니다: -클래스의 인스턴스를 생성하는 데 사용되는 `__init__` 메서드에 주목하기 바랍니다: +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[11:15] *} -{* ../../docs_src/dependencies/tutorial002.py hl[12] *} +클래스의 인스턴스를 만들 때 사용하는 `__init__` 메서드에 주목하세요: -...이전 `common_parameters`와 동일한 매개변수를 가집니다: +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[12] *} -{* ../../docs_src/dependencies/tutorial001.py hl[9] *} +...이전의 `common_parameters`와 동일한 매개변수를 가지고 있습니다: -이 매개변수들은 **FastAPI**가 의존성을 "해결"하기 위해 사용할 것입니다 +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8] *} -함수와 클래스 두 가지 방식 모두, 아래 요소를 갖습니다: +이 매개변수들이 **FastAPI**가 의존성을 "해결"하는 데 사용할 것들입니다. -* `문자열`이면서 선택사항인 쿼리 매개변수 `q`. -* 기본값이 `0`이면서 `정수형`인 쿼리 매개변수 `skip` -* 기본값이 `100`이면서 `정수형`인 쿼리 매개변수 `limit` +두 경우 모두 다음을 갖게 됩니다: -두 가지 방식 모두, 데이터는 변환, 검증되고 OpenAPI 스키마에 문서화됩니다. +* `str`인 선택적 쿼리 매개변수 `q`. +* 기본값이 `0`인 `int` 쿼리 매개변수 `skip`. +* 기본값이 `100`인 `int` 쿼리 매개변수 `limit`. -## 사용해봅시다! +두 경우 모두 데이터는 변환되고, 검증되며, OpenAPI 스키마에 문서화되는 등 여러 처리가 적용됩니다. -이제 아래의 클래스를 이용해서 의존성을 정의할 수 있습니다. +## 사용하기 { #use-it } -{* ../../docs_src/dependencies/tutorial002.py hl[19] *} +이제 이 클래스를 사용해 의존성을 선언할 수 있습니다. -**FastAPI**는 `CommonQueryParams` 클래스를 호출합니다. 이것은 해당 클래스의 "인스턴스"를 생성하고 그 인스턴스는 함수의 매개변수 `commons`로 전달됩니다. +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[19] *} -## 타입 힌팅 vs `Depends` +**FastAPI**는 `CommonQueryParams` 클래스를 호출합니다. 그러면 해당 클래스의 "인스턴스"가 생성되고, 그 인스턴스가 함수의 매개변수 `commons`로 전달됩니다. -위 코드에서 `CommonQueryParams`를 두 번 작성한 방식에 주목하십시오: +## 타입 어노테이션 vs `Depends` { #type-annotation-vs-depends } + +위 코드에서 `CommonQueryParams`를 두 번 작성하는 방식에 주목하세요: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` -마지막 `CommonQueryParams` 변수를 보면: +//// + +마지막 `CommonQueryParams`는, 다음에서: ```Python -... = Depends(CommonQueryParams) +... Depends(CommonQueryParams) ``` -... **FastAPI**가 실제로 어떤 것이 의존성인지 알기 위해서 사용하는 방법입니다. -FastAPI는 선언된 매개변수들을 추출할 것이고 실제로 이 변수들을 호출할 것입니다. +...**FastAPI**가 실제로 무엇이 의존성인지 알기 위해 사용하는 것입니다. + +FastAPI는 여기에서 선언된 매개변수들을 추출하고, 실제로 이것을 호출합니다. --- -이 경우에, 첫번째 `CommonQueryParams` 변수를 보면: +이 경우 첫 번째 `CommonQueryParams`는 다음에서: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, ... +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// ```Python commons: CommonQueryParams ... ``` -... **FastAPI**는 `CommonQueryParams` 변수에 어떠한 특별한 의미도 부여하지 않습니다. FastAPI는 이 변수를 데이터 변환, 검증 등에 활용하지 않습니다. (활용하려면 `= Depends(CommonQueryParams)`를 사용해야 합니다.) +//// -사실 아래와 같이 작성해도 무관합니다: +...**FastAPI**에 특별한 의미가 없습니다. FastAPI는 이를 데이터 변환, 검증 등에 사용하지 않습니다(그런 용도로는 `Depends(CommonQueryParams)`를 사용하고 있기 때문입니다). + +실제로는 이렇게만 작성해도 됩니다: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[Any, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// ```Python commons = Depends(CommonQueryParams) ``` -..전체적인 코드는 아래와 같습니다: +//// -{* ../../docs_src/dependencies/tutorial003.py hl[19] *} +...다음과 같이요: -그러나 자료형을 선언하면 에디터가 매개변수 `commons`로 전달될 것이 무엇인지 알게 되고, 이를 통해 코드 완성, 자료형 확인 등에 도움이 될 수 있으므로 권장됩니다. +{* ../../docs_src/dependencies/tutorial003_an_py310.py hl[19] *} -<!-- <img src="/img/tutorial/dependencies/image02.png"> --> +하지만 타입을 선언하는 것을 권장합니다. 그러면 에디터가 매개변수 `commons`에 무엇이 전달되는지 알 수 있고, 코드 완성, 타입 체크 등에서 도움을 받을 수 있습니다: -## 코드 단축 +<img src="/img/tutorial/dependencies/image02.png"> -그러나 여기 `CommonQueryParams`를 두 번이나 작성하는, 코드 반복이 있다는 것을 알 수 있습니다: +## 단축 { #shortcut } + +하지만 `CommonQueryParams`를 두 번 작성하는 코드 반복이 있다는 것을 볼 수 있습니다: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` -**FastAPI**는 *특히* 의존성이 **FastAPI**가 클래스 자체의 인스턴스를 생성하기 위해 "호출"하는 클래스인 경우, 조금 더 쉬운 방법을 제공합니다. +//// -이러한 특정한 경우에는 아래처럼 사용할 수 있습니다: +**FastAPI**는 이런 경우를 위한 단축 방법을 제공합니다. 이때 의존성은 *특히* **FastAPI**가 "호출"해서 클래스 자체의 인스턴스를 만들도록 하는 클래스입니다. -이렇게 쓰는 것 대신: +이 특정한 경우에는 다음과 같이 할 수 있습니다: + +다음처럼 작성하는 대신: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` -...이렇게 쓸 수 있습니다.: +//// + +...이렇게 작성합니다: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends()] +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// ```Python commons: CommonQueryParams = Depends() ``` -의존성을 매개변수의 타입으로 선언하는 경우 `Depends(CommonQueryParams)`처럼 클래스 이름 전체를 *다시* 작성하는 대신, 매개변수를 넣지 않은 `Depends()`의 형태로 사용할 수 있습니다. +//// -아래에 같은 예제가 있습니다: +의존성을 매개변수의 타입으로 선언하고, `Depends(CommonQueryParams)` 안에 클래스 전체를 *다시* 작성하는 대신 매개변수 없이 `Depends()`를 사용합니다. -{* ../../docs_src/dependencies/tutorial004.py hl[19] *} +그러면 같은 예제는 다음처럼 보일 겁니다: -...이렇게 코드를 단축하여도 **FastAPI**는 무엇을 해야하는지 알고 있습니다. +{* ../../docs_src/dependencies/tutorial004_an_py310.py hl[19] *} + +...그리고 **FastAPI**는 무엇을 해야 하는지 알게 됩니다. /// tip | 팁 -만약 이것이 도움이 되기보다 더 헷갈리게 만든다면, 잊어버리십시오. 이것이 반드시 필요한 것은 아닙니다. +도움이 되기보다 더 헷갈린다면, 무시하세요. 이건 *필수*가 아닙니다. -이것은 단지 손쉬운 방법일 뿐입니다. 왜냐하면 **FastAPI**는 코드 반복을 최소화할 수 있는 방법을 고민하기 때문입니다. +그저 단축 방법일 뿐입니다. **FastAPI**는 코드 반복을 최소화하도록 도와주는 것을 중요하게 생각하기 때문입니다. /// diff --git a/docs/ko/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/ko/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index 4a3854cef..39c78c078 100644 --- a/docs/ko/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/ko/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -1,4 +1,4 @@ -# 경로 작동 데코레이터에서의 의존성 +# 경로 작동 데코레이터에서의 의존성 { #dependencies-in-path-operation-decorators } 몇몇 경우에는, *경로 작동 함수* 안에서 의존성의 반환 값이 필요하지 않습니다. @@ -8,7 +8,7 @@ 그런 경우에, `Depends`를 사용하여 *경로 작동 함수*의 매개변수로 선언하는 것보다 *경로 작동 데코레이터*에 `dependencies`의 `list`를 추가할 수 있습니다. -## *경로 작동 데코레이터*에 `dependencies` 추가하기 +## *경로 작동 데코레이터*에 `dependencies` 추가하기 { #add-dependencies-to-the-path-operation-decorator } *경로 작동 데코레이터*는 `dependencies`라는 선택적인 인자를 받습니다. @@ -22,7 +22,7 @@ 일부 편집기에서는 사용되지 않는 함수 매개변수를 검사하고 오류로 표시합니다. -*경로 작동 데코레이터*에서 `dependencies`를 사용하면 편집기/도구 오류를 피하며 실행되도록 할 수 있습니다. +*경로 작동 데코레이터*에서 이러한 `dependencies`를 사용하면 편집기/도구 오류를 피하면서도 실행되도록 할 수 있습니다. 또한 코드에서 사용되지 않는 매개변수를 보고 불필요하다고 생각할 수 있는 새로운 개발자의 혼란을 방지하는데 도움이 될 수 있습니다. @@ -36,23 +36,23 @@ /// -## 의존성 오류와 값 반환하기 +## 의존성 오류와 값 반환하기 { #dependencies-errors-and-return-values } 평소에 사용하던대로 같은 의존성 *함수*를 사용할 수 있습니다. -### 의존성 요구사항 +### 의존성 요구사항 { #dependency-requirements } (헤더같은) 요청 요구사항이나 하위-의존성을 선언할 수 있습니다: {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[8,13] *} -### 오류 발생시키기 +### 오류 발생시키기 { #raise-exceptions } -다음 의존성은 기존 의존성과 동일하게 예외를 `raise`를 일으킬 수 있습니다: +다음 의존성은 기존 의존성과 동일하게 예외를 `raise`할 수 있습니다: {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[10,15] *} -### 값 반환하기 +### 값 반환하기 { #return-values } 값을 반환하거나, 그러지 않을 수 있으며 값은 사용되지 않습니다. @@ -60,10 +60,10 @@ {* ../../docs_src/dependencies/tutorial006_an_py39.py hl[11,16] *} -## *경로 작동* 모음에 대한 의존성 +## *경로 작동* 모음에 대한 의존성 { #dependencies-for-a-group-of-path-operations } 나중에 여러 파일을 가지고 있을 수 있는 더 큰 애플리케이션을 구조화하는 법([더 큰 애플리케이션 - 여러 파일들](../../tutorial/bigger-applications.md){.internal-link target=_blank})을 읽을 때, *경로 작동* 모음에 대한 단일 `dependencies` 매개변수를 선언하는 법에 대해서 배우게 될 것입니다. -## 전역 의존성 +## 전역 의존성 { #global-dependencies } 다음으로 각 *경로 작동*에 적용되도록 `FastAPI` 애플리케이션 전체에 의존성을 추가하는 법을 볼 것입니다. diff --git a/docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md index ff174937d..9bf6c0693 100644 --- a/docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/ko/docs/tutorial/dependencies/dependencies-with-yield.md @@ -1,6 +1,6 @@ -# yield를 사용하는 의존성 +# `yield`를 사용하는 의존성 { #dependencies-with-yield } -FastAPI는 <abbr title='때로는 "종료 코드", "정리 코드", "종료 처리 코드", "닫기 코드", "컨텍스트 관리자 종료 코드" 등으로도 불립니다'>작업 완료 후 추가 단계를 수행하는</abbr> 의존성을 지원합니다. +FastAPI는 <abbr title='sometimes also called "exit code", "cleanup code", "teardown code", "closing code", "context manager exit code", etc. – 때로는 "exit code", "cleanup code", "teardown code", "closing code", "context manager exit code" 등으로도 불립니다'>작업 완료 후 추가 단계를 수행하는</abbr> 의존성을 지원합니다. 이를 구현하려면 `return` 대신 `yield`를 사용하고, 추가로 실행할 단계 (코드)를 그 뒤에 작성하세요. @@ -23,21 +23,21 @@ FastAPI는 <abbr title='때로는 "종료 코드", "정리 코드", "종료 처 /// -## `yield`를 사용하는 데이터베이스 의존성 +## `yield`를 사용하는 데이터베이스 의존성 { #a-database-dependency-with-yield } 예를 들어, 이 기능을 사용하면 데이터베이스 세션을 생성하고 작업이 끝난 후에 세션을 종료할 수 있습니다. 응답을 생성하기 전에는 `yield`문을 포함하여 그 이전의 코드만이 실행됩니다: -{* ../../docs_src/dependencies/tutorial007.py hl[2:4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[2:4] *} -yield된 값은 *경로 작업* 및 다른 의존성들에 주입되는 값 입니다: +yield된 값은 *경로 처리* 및 다른 의존성들에 주입되는 값 입니다: -{* ../../docs_src/dependencies/tutorial007.py hl[4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[4] *} -`yield`문 다음의 코드는 응답을 생성한 후 보내기 전에 실행됩니다: +`yield`문 다음의 코드는 응답을 생성한 후 실행됩니다: -{* ../../docs_src/dependencies/tutorial007.py hl[5:6] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[5:6] *} /// tip | 팁 @@ -47,19 +47,19 @@ yield된 값은 *경로 작업* 및 다른 의존성들에 주입되는 값 입 /// -## `yield`와 `try`를 사용하는 의존성 +## `yield`와 `try`를 사용하는 의존성 { #a-dependency-with-yield-and-try } `yield`를 사용하는 의존성에서 `try` 블록을 사용한다면, 의존성을 사용하는 도중 발생한 모든 예외를 받을 수 있습니다. -예를 들어, 다른 의존성이나 *경로 작업*의 중간에 데이터베이스 트랜잭션 "롤백"이 발생하거나 다른 오류가 발생한다면, 해당 예외를 의존성에서 받을 수 있습니다. +예를 들어, 다른 의존성이나 *경로 처리*의 중간에 데이터베이스 트랜잭션 "롤백"이 발생하거나 다른 오류가 발생한다면, 해당 예외를 의존성에서 받을 수 있습니다. 따라서, 의존성 내에서 `except SomeException`을 사용하여 특정 예외를 처리할 수 있습니다. 마찬가지로, `finally`를 사용하여 예외 발생 여부와 관계 없이 종료 단계까 실행되도록 할 수 있습니다. -{* ../../docs_src/dependencies/tutorial007.py hl[3,5] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[3,5] *} -## `yield`를 사용하는 하위 의존성 +## `yield`를 사용하는 하위 의존성 { #sub-dependencies-with-yield } 모든 크기와 형태의 하위 의존성과 하위 의존성의 "트리"도 가질 수 있으며, 이들 모두가 `yield`를 사용할 수 있습니다. @@ -87,21 +87,23 @@ yield된 값은 *경로 작업* 및 다른 의존성들에 주입되는 값 입 /// note | 기술 세부사항 -파이썬의 <a href=“https://docs.python.org/3/library/contextlib.html” class=“external-link” target=“_blank”>Context Managers</a> 덕분에 이 기능이 작동합니다. +파이썬의 <a href="https://docs.python.org/3/library/contextlib.html" class="external-link" target="_blank">Context Managers</a> 덕분에 이 기능이 작동합니다. -**FastAPI**는 이를 내부적으로 컨텍스트 관리자를 사용하여 구현합니다. +**FastAPI**는 이를 내부적으로 사용하여 이를 달성합니다. /// -## `yield`와 `HTTPException`를 사용하는 의존성 +## `yield`와 `HTTPException`를 사용하는 의존성 { #dependencies-with-yield-and-httpexception } -`yield`와 `try` 블록이 있는 의존성을 사용하여 예외를 처리할 수 있다는 것을 알게 되었습니다. +`yield`를 사용하는 의존성에서 `try` 블록을 사용해 코드를 실행하고, 그 다음 `finally` 뒤에 종료 코드를 실행할 수 있다는 것을 보았습니다. -같은 방식으로, `yield` 이후의 종료 코드에서 `HTTPException`이나 유사한 예외를 발생시킬 수 있습니다. +또한 `except`를 사용해 발생한 예외를 잡고 그에 대해 무언가를 할 수도 있습니다. + +예를 들어, `HTTPException` 같은 다른 예외를 발생시킬 수 있습니다. /// tip | 팁 -이는 다소 고급 기술이며, 대부분의 경우 경로 연산 함수 등 나머지 애플리케이션 코드 내부에서 예외 (`HTTPException` 포함)를 발생시킬 수 있으므로 실제로는 필요하지 않을 것입니다. +이는 다소 고급 기술이며, 대부분의 경우 실제로는 필요하지 않을 것입니다. 예를 들어, *경로 처리 함수* 등 나머지 애플리케이션 코드 내부에서 예외 (`HTTPException` 포함)를 발생시킬 수 있기 때문입니다. 하지만 필요한 경우 사용할 수 있습니다. 🤓 @@ -109,27 +111,27 @@ yield된 값은 *경로 작업* 및 다른 의존성들에 주입되는 값 입 {* ../../docs_src/dependencies/tutorial008b_an_py39.py hl[18:22,31] *} -예외를 처리하고(또는 추가로 다른 `HTTPException`을 발생시키기 위해) 사용할 수 있는 또 다른 방법은 [사용자 정의 예외 처리기](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}를 생성하는 것 입니다. +예외를 잡고 그에 기반해 사용자 정의 응답을 생성하려면, [사용자 정의 예외 처리기](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}를 생성하세요. -## `yield`와 `except`를 사용하는 의존성 +## `yield`와 `except`를 사용하는 의존성 { #dependencies-with-yield-and-except } -`yield`를 사용하는 의존성에서 `except`를 사용하여 예외를 포착하고 예외를 다시 발생시키지 않거나 (또는 새 예외를 발생시키지 않으면), FastAPI는 해당 예외가 발생했는지 알 수 없습니다. 이는 일반적인 Python 방식과 동일합니다: +`yield`를 사용하는 의존성에서 `except`를 사용하여 예외를 포착하고 예외를 다시 발생시키지 않거나 (또는 새 예외를 발생시키지 않으면), FastAPI는 일반적인 Python에서와 마찬가지로 예외가 있었다는 것을 알아차릴 수 없습니다: {* ../../docs_src/dependencies/tutorial008c_an_py39.py hl[15:16] *} -이 경우, `HTTPException`이나 유사한 예외를 발생시키지 않기 때문에 클라이언트는 HTTP 500 Internal Server Error 응답을 보게 되지만, 서버는 어떤 오류가 발생했는지에 대한 **로그**나 다른 표시를 전혀 가지지 않게 됩니다. 😱 +이 경우, `HTTPException`이나 유사한 예외를 발생시키지 않기 때문에 클라이언트는 마땅히 *HTTP 500 Internal Server Error* 응답을 보게 되지만, 서버에는 어떤 오류였는지에 대한 **로그**나 다른 표시가 **전혀 남지 않게 됩니다**. 😱 -### `yield`와 `except`를 사용하는 의존성에서 항상 `raise` 하기 +### `yield`와 `except`를 사용하는 의존성에서 항상 `raise` 하기 { #always-raise-in-dependencies-with-yield-and-except } -`yield`가 있는 의존성에서 예외를 잡았을 때는 `HTTPException`이나 유사한 예외를 새로 발생시키지 않는 한, 반드시 원래의 예외를 다시 발생시켜야 합니다. +`yield`가 있는 의존성에서 예외를 잡았을 때, 다른 `HTTPException`이나 유사한 예외를 발생시키는 것이 아니라면, **원래 예외를 다시 발생시켜야 합니다**. `raise`를 사용하여 동일한 예외를 다시 발생시킬 수 있습니다: {* ../../docs_src/dependencies/tutorial008d_an_py39.py hl[17] *} -이제 클라이언트는 동일한 *HTTP 500 Internal Server Error* 오류 응답을 받게 되지만, 서버 로그에는 사용자 정의 예외인 `InternalError"가 기록됩니다. 😎 +이제 클라이언트는 동일한 *HTTP 500 Internal Server Error* 응답을 받게 되지만, 서버 로그에는 사용자 정의 `InternalError`가 기록됩니다. 😎 -## `yield`를 사용하는 의존성의 실행 순서 +## `yield`를 사용하는 의존성의 실행 순서 { #execution-of-dependencies-with-yield } 실행 순서는 아래 다이어그램과 거의 비슷합니다. 시간은 위에서 아래로 흐릅니다. 그리고 각 열은 상호 작용하거나 코드를 실행하는 부분 중 하나입니다. @@ -170,7 +172,7 @@ participant tasks as Background tasks /// info | 정보 -클라이언트에 **하나의 응답** 만 전송됩니다. 이는 오류 응답 중 하나일 수도 있고,*경로 작업*에서 생성된 응답일 수도 있습니다. +클라이언트에는 **하나의 응답**만 전송됩니다. 이는 오류 응답 중 하나일 수도 있고, *경로 처리*에서 생성된 응답일 수도 있습니다. 이러한 응답 중 하나가 전송된 후에는 다른 응답을 보낼 수 없습니다. @@ -178,55 +180,67 @@ participant tasks as Background tasks /// tip | 팁 -이 다이어그램은 `HTTPException`을 보여주지만, `yield`를 사용하는 의존성에서 처리한 예외나 [사용자 정의 예외처리기](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}.를 사용하여 처리한 다른 예외도 발생시킬 수 있습니다. - -어떤 예외가 발생하든, `HTTPException`을 포함하여 yield를 사용하는 의존성으로 전달됩니다. 대부분의 경우 예외를 다시 발생시키거나 새로운 예외를 발생시켜야 합니다. +*경로 처리 함수*의 코드에서 어떤 예외를 발생시키면 `HTTPException`을 포함해 `yield`를 사용하는 의존성으로 전달됩니다. 대부분의 경우 해당 예외(또는 새 예외)를 `yield`를 사용하는 의존성에서 다시 발생시켜, 제대로 처리되도록 해야 합니다. /// -## `yield`, `HTTPException`, `except` 및 백그라운드 작업을 사용하는 의존성 +## 조기 종료와 `scope` { #early-exit-and-scope } -/// warning | 경고 +일반적으로 `yield`를 사용하는 의존성의 종료 코드는 클라이언트로 **응답이 전송된 후에** 실행됩니다. -이러한 기술적 세부 사항은 대부분 필요하지 않으므로 이 섹션을 건너뛰고 아래에서 계속 진행해도 됩니다. +하지만 *경로 처리 함수*에서 반환한 뒤에는 더 이상 해당 의존성이 필요 없다는 것을 알고 있다면, `Depends(scope="function")`을 사용하여 FastAPI에 *경로 처리 함수*가 반환된 후, 하지만 **응답이 전송되기 전에** 의존성을 종료(닫기)해야 한다고 알려줄 수 있습니다. -이러한 세부 정보는 주로 FastAPI 0.106.0 이전 버전에서 `yield`가 있는 의존성의 리소스를 백그라운드 작업에서 사용했던 경우메 유용합니다. +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} -/// +`Depends()`는 다음이 될 수 있는 `scope` 매개변수를 받습니다: -### `yield`와 `except`를 사용하는 의존성, 기술 세부사항 +* `"function"`: 요청을 처리하는 *경로 처리 함수* 전에 의존성을 시작하고, *경로 처리 함수*가 끝난 후, 하지만 응답이 클라이언트로 전송되기 **전에** 의존성을 종료합니다. 즉, 의존성 함수는 *경로 처리 **함수***를 **둘러싸며** 실행됩니다. +* `"request"`: 요청을 처리하는 *경로 처리 함수* 전에 의존성을 시작하고(`"function"`을 사용할 때와 유사), 응답이 클라이언트로 전송된 **후에** 종료합니다. 즉, 의존성 함수는 **요청**과 응답 사이클을 **둘러싸며** 실행됩니다. -FastAPI 0.110.0 이전에는 `yield`가 포함된 의존성을 사용한 후 해당 의존성에서 `except`가 포함된 예외를 캡처하고 다시 예외를 발생시키지 않으면 예외가 자동으로 예외 핸들러 또는 내부 서버 오류 핸들러로 발생/전달되었습니다. +지정하지 않고 의존성이 `yield`를 사용한다면, 기본 `scope`는 `"request"`입니다. -이는 처리기 없이 전달된 예외(내부 서버 오류)에서 처리되지 않은 메모리 소비를 수정하고 일반 파이썬 코드의 동작과 일치하도록 하기 위해 0.110.0 버전에서 변경되었습니다. +### 하위 의존성을 위한 `scope` { #scope-for-sub-dependencies } -### 백그라운드 작업과 `yield`를 사용하는 의존성, 기술 세부사항 +`scope="request"`(기본값)로 의존성을 선언하면, 모든 하위 의존성도 `scope`가 `"request"`여야 합니다. -FastAPI 0.106.0 이전에는 `yield` 이후에 예외를 발생시키는 것이 불가능했습니다. `yield`가 있는 의존성 종료 코드는 응답이 전송된 이후에 실행되었기 때문에, [예외 처리기](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}가 이미 실행된 상태였습니다. +하지만 `scope`가 `"function"`인 의존성은 `scope`가 `"function"`인 의존성과 `"request"`인 의존성을 모두 의존성으로 가질 수 있습니다. -이는 주로 백그라운드 작업 내에서 의존성에서 "yield된" 동일한 객체를 사용할 수 있도록 하기 위해 이런 방식으로 설계되었습니다. 종료 코드는 백그라운드 작업이 완료된 후에 실행되었기 때문입니다 +이는 어떤 의존성이든, 종료 코드에서 하위 의존성을 계속 사용해야 할 수도 있으므로, 하위 의존성보다 먼저 종료 코드를 실행할 수 있어야 하기 때문입니다. -하지만 이렇게 하면 리소스를 불필요하게 양보한 의존성(예: 데이터베이스 연결)에서 보유하면서 응답이 네트워크를 통해 이동할 때까지 기다리는 것을 의미하기 때문에 FastAPI 0.106.0에서 변경되었습니다. +```mermaid +sequenceDiagram -/// tip | 팁 +participant client as Client +participant dep_req as Dep scope="request" +participant dep_func as Dep scope="function" +participant operation as Path Operation -또한 백그라운드 작업은 일반적으로 자체 리소스(예: 자체 데이터베이스 연결)를 사용하여 별도로 처리해야 하는 독립적인 로직 집합입니다. + client ->> dep_req: Start request + Note over dep_req: Run code up to yield + dep_req ->> dep_func: Pass dependency + Note over dep_func: Run code up to yield + dep_func ->> operation: Run path operation with dependency + operation ->> dep_func: Return from path operation + Note over dep_func: Run code after yield + Note over dep_func: ✅ Dependency closed + dep_func ->> client: Send response to client + Note over client: Response sent + Note over dep_req: Run code after yield + Note over dep_req: ✅ Dependency closed +``` -따라서 이렇게 하면 코드가 더 깔끔해집니다. +## `yield`, `HTTPException`, `except` 및 백그라운드 작업을 사용하는 의존성 { #dependencies-with-yield-httpexception-except-and-background-tasks } -/// +`yield`를 사용하는 의존성은 시간이 지나면서 서로 다른 사용 사례를 다루고 일부 문제를 수정하기 위해 발전해 왔습니다. -만약 이전에 이러한 동작에 의존했다면, 이제는 백그라운드 작업 내부에서 백그라운드 작업을 위한 리소스를 생성하고, `yield`가 있는 의존성의 리소스에 의존하지 않는 데이터만 내부적으로 사용해야합니다. +FastAPI의 여러 버전에서 무엇이 바뀌었는지 보고 싶다면, 고급 가이드의 [고급 의존성 - `yield`, `HTTPException`, `except` 및 백그라운드 작업을 사용하는 의존성](../../advanced/advanced-dependencies.md#dependencies-with-yield-httpexception-except-and-background-tasks){.internal-link target=_blank}에서 더 자세히 읽을 수 있습니다. +## 컨텍스트 관리자 { #context-managers } -예를 들어, 동일한 데이터베이스 세션을 사용하는 대신, 백그라운드 작업 내부에서 새로운 데이터베이스 세션을 생성하고 이 새로운 세션을 사용하여 데이터베이스에서 객체를 가져와야 합니다. 그리고 데이터베이스 객체를 백그라운드 작업 함수의 매개변수로 직접 전달하는 대신, 해당 객체의 ID를 전달한 다음 백그라운드 작업 함수 내부에서 객체를 다시 가져와야 합니다 - -## 컨텍스트 관리자 - -### "컨텍스트 관리자"란? +### "컨텍스트 관리자"란 { #what-are-context-managers } "컨텍스트 관리자"는 Python에서 `with` 문에서 사용할 수 있는 모든 객체를 의미합니다. -예를 들어, <a href="https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files" class="external-link" target="_blank"> `with`를 사용하여 파일을 읽을 수 있습니다</a>: +예를 들어, <a href="https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files" class="external-link" target="_blank">`with`를 사용하여 파일을 읽을 수 있습니다</a>: ```Python with open("./somefile.txt") as f: @@ -240,7 +254,7 @@ with open("./somefile.txt") as f: `yield`가 있는 의존성을 생성하면 **FastAPI**는 내부적으로 이를 위한 컨텍스트 매니저를 생성하고 다른 관련 도구들과 결합합니다. -### `yield`를 사용하는 의존성에서 컨텍스트 관리자 사용하기 +### `yield`를 사용하는 의존성에서 컨텍스트 관리자 사용하기 { #using-context-managers-in-dependencies-with-yield } /// warning | 경고 @@ -255,7 +269,7 @@ Python에서는 다음을 통해 컨텍스트 관리자를 생성할 수 있습 **FastAPI**의 `yield`가 있는 의존성 내에서 `with` 또는 `async with`문을 사용하여 이들을 활용할 수 있습니다: -{* ../../docs_src/dependencies/tutorial010.py hl[1:9,13] *} +{* ../../docs_src/dependencies/tutorial010_py39.py hl[1:9,13] *} /// tip | 팁 diff --git a/docs/ko/docs/tutorial/dependencies/global-dependencies.md b/docs/ko/docs/tutorial/dependencies/global-dependencies.md index 0d0e7684d..68e4295da 100644 --- a/docs/ko/docs/tutorial/dependencies/global-dependencies.md +++ b/docs/ko/docs/tutorial/dependencies/global-dependencies.md @@ -1,15 +1,16 @@ -# 전역 의존성 +# 전역 의존성 { #global-dependencies } -몇몇 애플리케이션에서는 애플리케이션 전체에 의존성을 추가하고 싶을 수 있습니다. +몇몇 유형의 애플리케이션에서는 애플리케이션 전체에 의존성을 추가하고 싶을 수 있습니다. -[*경로 작동 데코레이터*에 `dependencies` 추가하기](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}와 유사한 방법으로 `FastAPI` 애플리케이션에 그것들을 추가할 수 있습니다. +[*경로 처리 데코레이터*에 `dependencies` 추가하기](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}와 유사한 방법으로 `FastAPI` 애플리케케이션에 그것들을 추가할 수 있습니다. -그런 경우에, 애플리케이션의 모든 *경로 작동*에 적용될 것입니다: +그런 경우에, 애플리케이션의 모든 *경로 처리*에 적용될 것입니다: -{* ../../docs_src/dependencies/tutorial012_an_py39.py hl[16] *} +{* ../../docs_src/dependencies/tutorial012_an_py39.py hl[17] *} -그리고 [*경로 작동 데코레이터*에 `dependencies` 추가하기](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}에 대한 아이디어는 여전히 적용되지만 여기에서는 앱에 있는 모든 *경로 작동*에 적용됩니다. -## *경로 작동* 모음에 대한 의존성 +그리고 [*경로 처리 데코레이터*에 `dependencies` 추가하기](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} 섹션의 모든 아이디어는 여전히 적용되지만, 이 경우에는 앱의 모든 *경로 처리*에 적용됩니다. -이후에 여러 파일들을 가지는 더 큰 애플리케이션을 구조화하는 법([더 큰 애플리케이션 - 여러 파일들](../../tutorial/bigger-applications.md){.internal-link target=_blank})을 읽을 때, *경로 작동* 모음에 대한 단일 `dependencies` 매개변수를 선언하는 법에 대해서 배우게 될 것입니다. +## *경로 처리* 그룹에 대한 의존성 { #dependencies-for-groups-of-path-operations } + +나중에 여러 파일을 포함할 수도 있는 더 큰 애플리케이션을 구조화하는 법([더 큰 애플리케이션 - 여러 파일들](../../tutorial/bigger-applications.md){.internal-link target=_blank})을 읽을 때, *경로 처리* 그룹에 대한 단일 `dependencies` 매개변수를 선언하는 법을 배우게 될 것입니다. diff --git a/docs/ko/docs/tutorial/dependencies/index.md b/docs/ko/docs/tutorial/dependencies/index.md index b35a41e37..167e6a478 100644 --- a/docs/ko/docs/tutorial/dependencies/index.md +++ b/docs/ko/docs/tutorial/dependencies/index.md @@ -1,14 +1,14 @@ -# 의존성 +# 의존성 { #dependencies } -**FastAPI**는 아주 강력하지만 직관적인 **<abbr title="컴포넌트, 자원, 제공자, 서비스, 인젝터블로 알려져 있습니다">의존성 주입</abbr>** 시스템을 가지고 있습니다. +**FastAPI**는 아주 강력하지만 직관적인 **<abbr title="also known as components, resources, providers, services, injectables">의존성 주입</abbr>** 시스템을 가지고 있습니다. 이는 사용하기 아주 쉽게 설계했으며, 어느 개발자나 다른 컴포넌트와 **FastAPI**를 쉽게 통합할 수 있도록 만들었습니다. -## "의존성 주입"은 무엇입니까? +## "의존성 주입"은 무엇입니까? { #what-is-dependency-injection } -**"의존성 주입"**은 프로그래밍에서 여러분의 코드(이 경우, 경로 작동 함수)가 작동하고 사용하는 데 필요로 하는 것, 즉 "의존성"을 선언할 수 있는 방법을 의미합니다. +**"의존성 주입"**은 프로그래밍에서 여러분의 코드(이 경우, *경로 처리 함수*)가 작동하고 사용하는 데 필요로 하는 것, 즉 "의존성"을 선언할 수 있는 방법을 의미합니다. -그 후에, 시스템(이 경우 FastAPI)은 여러분의 코드가 요구하는 의존성을 제공하기 위해 필요한 모든 작업을 처리합니다.(의존성을 "주입"합니다) +그 후에, 시스템(이 경우 **FastAPI**)은 여러분의 코드가 요구하는 의존성을 제공하기 위해 필요한 모든 작업을 처리합니다.(의존성을 "주입"합니다) 이는 여러분이 다음과 같은 사항을 필요로 할 때 매우 유용합니다: @@ -19,17 +19,17 @@ 이 모든 사항을 할 때 코드 반복을 최소화합니다. -## 첫번째 단계 +## 첫번째 단계 { #first-steps } 아주 간단한 예제를 봅시다. 너무 간단할 것이기에 지금 당장은 유용하지 않을 수 있습니다. 하지만 이를 통해 **의존성 주입** 시스템이 어떻게 작동하는지에 중점을 둘 것입니다. -### 의존성 혹은 "디펜더블" 만들기 +### 의존성 혹은 "디펜더블" 만들기 { #create-a-dependency-or-dependable } 의존성에 집중해 봅시다. -*경로 작동 함수*가 가질 수 있는 모든 매개변수를 갖는 단순한 함수입니다: +*경로 처리 함수*가 가질 수 있는 모든 매개변수를 갖는 단순한 함수입니다: {* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8:9] *} @@ -37,9 +37,9 @@ **단 두 줄입니다**. -그리고, 이 함수는 여러분의 모든 *경로 작동 함수*가 가지고 있는 것과 같은 형태와 구조를 가지고 있습니다. +그리고, 이 함수는 여러분의 모든 *경로 처리 함수*가 가지고 있는 것과 같은 형태와 구조를 가지고 있습니다. -여러분은 이를 "데코레이터"가 없는 (`@app.get("/some-path")`가 없는) *경로 작동 함수*라고 생각할 수 있습니다. +여러분은 이를 "데코레이터"가 없는 (`@app.get("/some-path")`가 없는) *경로 처리 함수*라고 생각할 수 있습니다. 그리고 여러분이 원하는 무엇이든 반환할 수 있습니다. @@ -47,7 +47,7 @@ * 선택적인 쿼리 매개변수 `q`, `str`을 자료형으로 가집니다. * 선택적인 쿼리 매개변수 `skip`, `int`를 자료형으로 가지며 기본 값은 `0`입니다. -* 선택적인 쿼리 매개변수 `limit`,`int`를 자료형으로 가지며 기본 값은 `100`입니다. +* 선택적인 쿼리 매개변수 `limit` that is an `int`, and by default is `100`. 그 후 위의 값을 포함한 `dict` 자료형으로 반환할 뿐입니다. @@ -57,17 +57,17 @@ FastAPI는 0.95.0 버전부터 `Annotated`에 대한 지원을 (그리고 이를 옛날 버전을 가지고 있는 경우, `Annotated`를 사용하려 하면 에러를 맞이하게 될 것입니다. -`Annotated`를 사용하기 전에 최소 0.95.1로 [FastAPI 버전 업그레이드](../../deployment/versions.md#fastapi_2){.internal-link target=_blank}를 확실하게 하세요. +`Annotated`를 사용하기 전에 최소 0.95.1로 [FastAPI 버전 업그레이드](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}를 확실하게 하세요. /// -### `Depends` 불러오기 +### `Depends` 불러오기 { #import-depends } {* ../../docs_src/dependencies/tutorial001_an_py310.py hl[3] *} -### "의존자"에 의존성 명시하기 +### "의존자"에 의존성 명시하기 { #declare-the-dependency-in-the-dependant } -*경로 작동 함수*의 매개변수로 `Body`, `Query` 등을 사용하는 방식과 같이 새로운 매개변수로 `Depends`를 사용합니다: +*경로 처리 함수*의 매개변수로 `Body`, `Query` 등을 사용하는 방식과 같이 새로운 매개변수로 `Depends`를 사용합니다: {* ../../docs_src/dependencies/tutorial001_an_py310.py hl[13,18] *} @@ -79,7 +79,7 @@ FastAPI는 0.95.0 버전부터 `Annotated`에 대한 지원을 (그리고 이를 여러분은 직접 **호출하지 않았습니다** (끝에 괄호를 치지 않았습니다), 단지 `Depends()`에 매개변수로 넘겨 줬을 뿐입니다. -그리고 그 함수는 *경로 작동 함수*가 작동하는 것과 같은 방식으로 매개변수를 받습니다. +그리고 그 함수는 *경로 처리 함수*가 작동하는 것과 같은 방식으로 매개변수를 받습니다. /// tip | 팁 @@ -91,7 +91,7 @@ FastAPI는 0.95.0 버전부터 `Annotated`에 대한 지원을 (그리고 이를 * 올바른 매개변수를 가진 의존성("디펜더블") 함수를 호출합니다. * 함수에서 결과를 받아옵니다. -* *경로 작동 함수*에 있는 매개변수에 그 결과를 할당합니다 +* *경로 처리 함수*에 있는 매개변수에 그 결과를 할당합니다 ```mermaid graph TB @@ -104,7 +104,7 @@ common_parameters --> read_items common_parameters --> read_users ``` -이렇게 하면 공용 코드를 한번만 적어도 되며, **FastAPI**는 *경로 작동*을 위해 이에 대한 호출을 처리합니다. +이렇게 하면 공용 코드를 한번만 적어도 되며, **FastAPI**는 *경로 처리*을 위해 이에 대한 호출을 처리합니다. /// check | 확인 @@ -114,7 +114,7 @@ common_parameters --> read_users /// -## `Annotated`인 의존성 공유하기 +## `Annotated`인 의존성 공유하기 { #share-annotated-dependencies } 위의 예제에서 몇몇 작은 **코드 중복**이 있다는 것을 보았을 겁니다. @@ -138,25 +138,25 @@ commons: Annotated[dict, Depends(common_parameters)] 이 의존성은 계속해서 예상한대로 작동할 것이며, **제일 좋은 부분**은 **타입 정보가 보존된다는 것입니다**. 즉 여러분의 편집기가 **자동 완성**, **인라인 에러** 등을 계속해서 제공할 수 있다는 것입니다. `mypy`같은 다른 도구도 마찬가지입니다. -이는 특히 **많은 *경로 작동***에서 **같은 의존성**을 계속해서 사용하는 **거대 코드 기반**안에서 사용하면 유용할 것입니다. +이는 특히 **많은 *경로 처리***에서 **같은 의존성**을 계속해서 사용하는 **거대 코드 기반**안에서 사용하면 유용할 것입니다. -## `async`하게, 혹은 `async`하지 않게 +## `async`하게, 혹은 `async`하지 않게 { #to-async-or-not-to-async } -의존성이 (*경로 작동 함수*에서 처럼 똑같이) **FastAPI**에 의해 호출될 수 있으며, 함수를 정의할 때 동일한 규칙이 적용됩니다. +의존성이 (*경로 처리 함수*에서 처럼 똑같이) **FastAPI**에 의해 호출될 수 있으며, 함수를 정의할 때 동일한 규칙이 적용됩니다. `async def`을 사용하거나 혹은 일반적인 `def`를 사용할 수 있습니다. -그리고 일반적인 `def` *경로 작동 함수* 안에 `async def`로 의존성을 선언할 수 있으며, `async def` *경로 작동 함수* 안에 `def`로 의존성을 선언하는 등의 방법이 있습니다. +그리고 일반적인 `def` *경로 처리 함수* 안에 `async def`로 의존성을 선언할 수 있으며, `async def` *경로 처리 함수* 안에 `def`로 의존성을 선언하는 등의 방법이 있습니다. 아무 문제 없습니다. **FastAPI**는 무엇을 할지 알고 있습니다. /// note | 참고 -잘 모르시겠다면, [Async: *"In a hurry?"*](../../async.md){.internal-link target=_blank} 문서에서 `async`와 `await`에 대해 확인할 수 있습니다. +잘 모르시겠다면, [Async: *"In a hurry?"*](../../async.md#in-a-hurry){.internal-link target=_blank} 문서에서 `async`와 `await`에 대해 확인할 수 있습니다. /// -## OpenAPI와 통합 +## OpenAPI와 통합 { #integrated-with-openapi } 모든 요청 선언, 검증과 의존성(및 하위 의존성)에 대한 요구 사항은 동일한 OpenAPI 스키마에 통합됩니다. @@ -164,15 +164,15 @@ commons: Annotated[dict, Depends(common_parameters)] <img src="/img/tutorial/dependencies/image01.png"> -## 간단한 사용법 +## 간단한 사용법 { #simple-usage } -이를 보면, *경로 작동 함수*는 *경로*와 *작동*이 매칭되면 언제든지 사용되도록 정의되었으며, **FastAPI**는 올바른 매개변수를 가진 함수를 호출하고 해당 요청에서 데이터를 추출합니다. +이를 보면, *경로 처리 함수*는 *경로*와 *작동*이 매칭되면 언제든지 사용되도록 정의되었으며, **FastAPI**는 올바른 매개변수를 가진 함수를 호출하고 해당 요청에서 데이터를 추출합니다. 사실, 모든 (혹은 대부분의) 웹 프레임워크는 이와 같은 방식으로 작동합니다. 여러분은 이러한 함수들을 절대 직접 호출하지 않습니다. 프레임워크(이 경우 **FastAPI**)에 의해 호출됩니다. -의존성 주입 시스템과 함께라면 **FastAPI**에게 여러분의 *경로 작동 함수*가 실행되기 전에 실행되어야 하는 무언가에 여러분의 *경로 작동 함수* 또한 "의존"하고 있음을 알릴 수 있으며, **FastAPI**는 이를 실행하고 결과를 "주입"할 것입니다. +의존성 주입 시스템과 함께라면 **FastAPI**에게 여러분의 *경로 처리 함수*가 실행되기 전에 실행되어야 하는 무언가에 여러분의 *경로 처리 함수* 또한 "의존"하고 있음을 알릴 수 있으며, **FastAPI**는 이를 실행하고 결과를 "주입"할 것입니다. "의존성 주입"이라는 동일한 아이디어에 대한 다른 일반적인 용어는 다음과 같습니다: @@ -182,15 +182,15 @@ commons: Annotated[dict, Depends(common_parameters)] * 인젝터블 * 컴포넌트 -## **FastAPI** 플러그인 +## **FastAPI** 플러그인 { #fastapi-plug-ins } -통합과 "플러그인"은 **의존성 주입** 시스템을 사용하여 구축할 수 있습니다. 하지만 실제로 **"플러그인"을 만들 필요는 없습니다**, 왜냐하면 의존성을 사용함으로써 여러분의 *경로 작동 함수*에 통합과 상호 작용을 무한대로 선언할 수 있기 때문입니다. +통합과 "플러그인"은 **의존성 주입** 시스템을 사용하여 구축할 수 있습니다. 하지만 실제로 **"플러그인"을 만들 필요는 없습니다**, 왜냐하면 의존성을 사용함으로써 여러분의 *경로 처리 함수*에 통합과 상호 작용을 무한대로 선언할 수 있기 때문입니다. 그리고 "말 그대로", 그저 필요로 하는 파이썬 패키지를 임포트하고 단 몇 줄의 코드로 여러분의 API 함수와 통합함으로써, 의존성을 아주 간단하고 직관적인 방법으로 만들 수 있습니다. 관계형 및 NoSQL 데이터베이스, 보안 등, 이에 대한 예시를 다음 장에서 볼 수 있습니다. -## **FastAPI** 호환성 +## **FastAPI** 호환성 { #fastapi-compatibility } 의존성 주입 시스템의 단순함은 **FastAPI**를 다음과 같은 요소들과 호환할 수 있게 합니다: @@ -203,7 +203,7 @@ commons: Annotated[dict, Depends(common_parameters)] * 응답 데이터 주입 시스템 * 기타 등등. -## 간편하고 강력하다 +## 간편하고 강력하다 { #simple-and-powerful } 계층적인 의존성 주입 시스템은 정의하고 사용하기 쉽지만, 여전히 매우 강력합니다. @@ -211,7 +211,7 @@ commons: Annotated[dict, Depends(common_parameters)] 끝에는, 계층적인 나무로 된 의존성이 만들어지며, 그리고 **의존성 주입** 시스템은 (하위 의존성도 마찬가지로) 이러한 의존성들을 처리하고 각 단계마다 결과를 제공합니다(주입합니다). -예를 들면, 여러분이 4개의 API 엔드포인트(*경로 작동*)를 가지고 있다고 해봅시다: +예를 들면, 여러분이 4개의 API 엔드포인트(*경로 처리*)를 가지고 있다고 해봅시다: * `/items/public/` * `/items/private/` @@ -243,8 +243,8 @@ admin_user --> activate_user paying_user --> pro_items ``` -## **OpenAPI**와의 통합 +## **OpenAPI**와의 통합 { #integrated-with-openapi_1 } -이 모든 의존성은 각각의 요구사항을 선언하는 동시에, *경로 작동*에 매개변수, 검증 등을 추가합니다. +이 모든 의존성은 각각의 요구사항을 선언하는 동시에, *경로 처리*에 매개변수, 검증 등을 추가합니다. **FastAPI**는 이 모든 것을 OpenAPI 스키마에 추가할 것이며, 이를 통해 대화형 문서 시스템에 나타날 것입니다. diff --git a/docs/ko/docs/tutorial/encoder.md b/docs/ko/docs/tutorial/encoder.md index 4323957f4..804f00e35 100644 --- a/docs/ko/docs/tutorial/encoder.md +++ b/docs/ko/docs/tutorial/encoder.md @@ -1,35 +1,35 @@ -# JSON 호환 가능 인코더 +# JSON 호환 가능 인코더 { #json-compatible-encoder } -데이터 유형(예: Pydantic 모델)을 JSON과 호환된 형태로 반환해야 하는 경우가 있습니다. (예: `dict`, `list` 등) +데이터 유형(예: Pydantic 모델)을 JSON과 호환되는 형태(예: `dict`, `list` 등)로 변환해야 하는 경우가 있습니다. -예를 들면, 데이터베이스에 저장해야하는 경우입니다. +예를 들면, 데이터베이스에 저장해야 하는 경우입니다. -이를 위해, **FastAPI** 에서는 `jsonable_encoder()` 함수를 제공합니다. +이를 위해, **FastAPI**에서는 `jsonable_encoder()` 함수를 제공합니다. -## `jsonable_encoder` 사용 +## `jsonable_encoder` 사용 { #using-the-jsonable-encoder } JSON 호환 가능 데이터만 수신하는 `fake_db` 데이터베이스가 존재한다고 가정하겠습니다. -예를 들면, `datetime` 객체는 JSON과 호환되는 데이터가 아니므로 이 데이터는 받아들여지지 않습니다. +예를 들면, `datetime` 객체는 JSON과 호환되지 않으므로 이 데이터베이스는 이를 받지 않습니다. -따라서 `datetime` 객체는 <a href="https://en.wikipedia.org/wiki/ISO_8601" class="external-link" target="_blank">ISO format</a> 데이터를 포함하는 `str`로 변환되어야 합니다. +따라서 `datetime` 객체는 <a href="https://en.wikipedia.org/wiki/ISO_8601" class="external-link" target="_blank">ISO format</a>의 데이터를 포함하는 `str`로 변환되어야 합니다. -같은 방식으로 이 데이터베이스는 Pydantic 모델(속성이 있는 객체)을 받지 않고, `dict` 만을 받습니다. +같은 방식으로 이 데이터베이스는 Pydantic 모델(속성이 있는 객체)을 받지 않고, `dict`만을 받습니다. -이를 위해 `jsonable_encoder` 를 사용할 수 있습니다. +이를 위해 `jsonable_encoder`를 사용할 수 있습니다. -Pydantic 모델과 같은 객체를 받고 JSON 호환 가능한 버전으로 반환합니다: +Pydantic 모델 같은 객체를 받고 JSON 호환 가능한 버전을 반환합니다: -{* ../../docs_src/encoder/tutorial001.py hl[5,22] *} +{* ../../docs_src/encoder/tutorial001_py310.py hl[4,21] *} -이 예시는 Pydantic 모델을 `dict`로, `datetime` 형식을 `str`로 변환합니다. +이 예시에서는 Pydantic 모델을 `dict`로, `datetime`을 `str`로 변환합니다. -이렇게 호출한 결과는 파이썬 표준인 <a href="https://docs.python.org/3/library/json.html#json.dumps" class="external-link" target="_blank">`json.dumps()`</a>로 인코딩 할 수 있습니다. +이렇게 호출한 결과는 파이썬 표준인 <a href="https://docs.python.org/3/library/json.html#json.dumps" class="external-link" target="_blank">`json.dumps()`</a>로 인코딩할 수 있습니다. -길이가 긴 문자열 형태의 JSON 형식(문자열)의 데이터가 들어있는 상황에서는 `str`로 반환하지 않습니다. JSON과 모두 호환되는 값과 하위 값이 있는 Python 표준 데이터 구조 (예: `dict`)를 반환합니다. +JSON 형식(문자열)의 데이터가 들어있는 큰 `str`을 반환하지 않습니다. JSON과 모두 호환되는 값과 하위 값이 있는 파이썬 표준 데이터 구조(예: `dict`)를 반환합니다. /// note | 참고 -실제로 `jsonable_encoder`는 **FastAPI** 에서 내부적으로 데이터를 변환하는 데 사용하지만, 다른 많은 곳에서도 이는 유용합니다. +`jsonable_encoder`는 실제로 **FastAPI**에서 내부적으로 데이터를 변환하는 데 사용하지만, 다른 많은 시나리오에서도 유용합니다. /// diff --git a/docs/ko/docs/tutorial/extra-data-types.md b/docs/ko/docs/tutorial/extra-data-types.md index 4a41ba0dc..4a51e9286 100644 --- a/docs/ko/docs/tutorial/extra-data-types.md +++ b/docs/ko/docs/tutorial/extra-data-types.md @@ -1,4 +1,4 @@ -# 추가 데이터 자료형 +# 추가 데이터 자료형 { #extra-data-types } 지금까지 일반적인 데이터 자료형을 사용했습니다. 예를 들면 다음과 같습니다: @@ -17,7 +17,7 @@ * 데이터 검증. * 자동 어노테이션과 문서화. -## 다른 데이터 자료형 +## 다른 데이터 자료형 { #other-data-types } 아래의 추가적인 데이터 자료형을 사용할 수 있습니다: @@ -36,7 +36,7 @@ * `datetime.timedelta`: * 파이썬의 `datetime.timedelta`. * 요청과 응답에서 전체 초(seconds)의 `float`로 표현됩니다. - * Pydantic은 "ISO 8601 시차 인코딩"으로 표현하는 것 또한 허용합니다. <a href="https://docs.pydantic.dev/latest/concepts/serialization/#json_encoders" class="external-link" target="_blank">더 많은 정보는 이 문서에서 확인하십시오.</a>. + * Pydantic은 "ISO 8601 time diff encoding"으로 표현하는 것 또한 허용합니다. <a href="https://docs.pydantic.dev/latest/concepts/serialization/#custom-serializers" class="external-link" target="_blank">더 많은 정보는 문서를 확인하세요</a>. * `frozenset`: * 요청과 응답에서 `set`와 동일하게 취급됩니다: * 요청 시, 리스트를 읽어 중복을 제거하고 `set`로 변환합니다. @@ -49,11 +49,11 @@ * `Decimal`: * 표준 파이썬의 `Decimal`. * 요청과 응답에서 `float`와 동일하게 다뤄집니다. -* 여기에서 모든 유효한 pydantic 데이터 자료형을 확인할 수 있습니다: <a href="https://docs.pydantic.dev/latest/usage/types/types/" class="external-link" target="_blank">Pydantic 데이터 자료형</a>. +* 여기에서 모든 유효한 Pydantic 데이터 자료형을 확인할 수 있습니다: <a href="https://docs.pydantic.dev/latest/usage/types/types/" class="external-link" target="_blank">Pydantic 데이터 자료형</a>. -## 예시 +## 예시 { #example } -위의 몇몇 자료형을 매개변수로 사용하는 *경로 작동* 예시입니다. +위의 몇몇 자료형을 매개변수로 사용하는 *경로 처리* 예시입니다. {* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[1,3,12:16] *} diff --git a/docs/ko/docs/tutorial/extra-models.md b/docs/ko/docs/tutorial/extra-models.md index 8e4559061..bb5daddea 100644 --- a/docs/ko/docs/tutorial/extra-models.md +++ b/docs/ko/docs/tutorial/extra-models.md @@ -1,43 +1,34 @@ -# 추가 모델 +# 추가 모델 { #extra-models } -지난 예제에 이어서, 연관된 모델을 여러개 갖는 것은 흔한 일입니다. +지난 예제에 이어서, 연관된 모델을 여러 개 갖는 것은 흔한 일입니다. 특히 사용자 모델의 경우에 그러한데, 왜냐하면: -* **입력 모델** 은 비밀번호를 가져야 합니다. -* **출력 모델** 은 비밀번호를 가지면 안됩니다. -* **데이터베이스 모델** 은 해시처리된 비밀번호를 가질 것입니다. +* **입력 모델**은 비밀번호를 가질 수 있어야 합니다. +* **출력 모델**은 비밀번호를 가지면 안 됩니다. +* **데이터베이스 모델**은 아마도 해시 처리된 비밀번호를 가질 필요가 있을 것입니다. /// danger | 위험 절대 사용자의 비밀번호를 평문으로 저장하지 마세요. 항상 이후에 검증 가능한 "안전한 해시(secure hash)"로 저장하세요. -만약 이게 무엇인지 모르겠다면, [security chapters](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}.에서 비밀번호 해시에 대해 배울 수 있습니다. +만약 이게 무엇인지 모르겠다면, [security chapters](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}에서 "password hash"가 무엇인지 배울 수 있습니다. /// -## 다중 모델 +## 다중 모델 { #multiple-models } 아래는 비밀번호 필드와 해당 필드가 사용되는 위치를 포함하여, 각 모델들이 어떤 형태를 가질 수 있는지 전반적인 예시입니다: {* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} +### `**user_in.model_dump()` 에 대하여 { #about-user-in-model-dump } -/// info | 정보 - -Pydantic v1에서는 해당 메서드가 `.dict()`로 불렸으며, Pydantic v2에서는 `.model_dump()`로 이름이 변경되었습니다. `.dict()`는 여전히 지원되지만 더 이상 권장되지 않습니다. - -여기에서 사용하는 예제는 Pydantic v1과의 호환성을 위해 `.dict()`를 사용하지만, Pydantic v2를 사용할 수 있다면 `.model_dump()`를 사용하는 것이 좋습니다. - -/// - -### `**user_in.dict()` 에 대하여 - -#### Pydantic의 `.dict()` +#### Pydantic의 `.model_dump()` { #pydantics-model-dump } `user_in`은 Pydantic 모델 클래스인 `UserIn`입니다. -Pydantic 모델은 모델 데이터를 포함한 `dict`를 반환하는 `.dict()` 메서드를 제공합니다. +Pydantic 모델은 모델 데이터를 포함한 `dict`를 반환하는 `.model_dump()` 메서드를 제공합니다. 따라서, 다음과 같이 Pydantic 객체 `user_in`을 생성할 수 있습니다: @@ -48,7 +39,7 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com 그 다음, 다음과 같이 호출합니다: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() ``` 이제 변수 `user_dict`에 데이터가 포함된 `dict`를 가지게 됩니다(이는 Pydantic 모델 객체가 아닌 `dict`입니다). @@ -70,7 +61,7 @@ Python의 `dict`가 다음과 같이 출력됩니다: } ``` -#### `dict` 언패킹(Unpacking) +#### `dict` 언패킹 { #unpacking-a-dict } `user_dict`와 같은 `dict`를 함수(또는 클래스)에 `**user_dict`로 전달하면, Python은 이를 "언팩(unpack)"합니다. 이 과정에서 `user_dict`의 키와 값을 각각 키-값 인자로 직접 전달합니다. @@ -102,31 +93,31 @@ UserInDB( ) ``` -#### 다른 모델 데이터로 새 Pydantic 모델 생성 +#### 다른 모델 데이터로 새 Pydantic 모델 생성 { #a-pydantic-model-from-the-contents-of-another } -위의 예제에서 `user_in.dict()`로부터 `user_dict`를 생성한 것처럼, 아래 코드는: +위의 예제에서 `user_in.model_dump()`로부터 `user_dict`를 생성한 것처럼, 아래 코드는: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() UserInDB(**user_dict) ``` 다음과 동일합니다: ```Python -UserInDB(**user_in.dict()) +UserInDB(**user_in.model_dump()) ``` -...왜냐하면 `user_in.dict()`는 `dict`이며, 이를 `**`로 Python이 "언팩(unpack)"하도록 하여 `UserInDB`에 전달하기 때문입니다. +...왜냐하면 `user_in.model_dump()`는 `dict`이며, 이를 `**`로 Python이 "언팩(unpack)"하도록 하여 `UserInDB`에 전달하기 때문입니다. 따라서, 다른 Pydantic 모델의 데이터를 사용하여 새로운 Pydantic 모델을 생성할 수 있습니다. -#### `dict` 언패킹(Unpacking)과 추가 키워드 +#### `dict` 언패킹과 추가 키워드 { #unpacking-a-dict-and-extra-keywords } 그리고 다음과 같이 추가 키워드 인자 `hashed_password=hashed_password`를 추가하면: ```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) ``` 다음과 같은 결과를 생성합니다: @@ -147,7 +138,7 @@ UserInDB( /// -## 중복 줄이기 +## 중복 줄이기 { #reduce-duplication } 코드 중복을 줄이는 것은 **FastAPI**의 핵심 아이디어 중 하나입니다. @@ -161,11 +152,11 @@ UserInDB( 모든 데이터 변환, 검증, 문서화 등은 정상적으로 작동할 것입니다. -이렇게 하면 각 모델 간의 차이점만 선언할 수 있습니다(평문 `password`가 있는 경우, `hashed_password`만 있는 경우, 혹은 비밀번호가 없는 경우): +이렇게 하면 각 모델 간의 차이점만 선언할 수 있습니다(평문 `password`, `hashed_password`, 그리고 비밀번호가 없는 경우): {* ../../docs_src/extra_models/tutorial002_py310.py hl[7,13:14,17:18,21:22] *} -## `Union` 또는 `anyOf` +## `Union` 또는 `anyOf` { #union-or-anyof } 두 가지 이상의 타입을 포함하는 `Union`으로 응답을 선언할 수 있습니다. 이는 응답이 그 중 하나의 타입일 수 있음을 의미합니다. @@ -175,18 +166,17 @@ OpenAPI에서는 이를 `anyOf`로 정의합니다. /// note | 참고 -<a href="https://docs.pydantic.dev/latest/concepts/types/#unions" class="external-link" target="_blank">`Union`</a>을 정의할때는 더 구체적인 타입을 먼저 포함하고, 덜 구체적인 타입을 그 뒤에 나열해야합니다. 아래 예제에서는 `Union[PlaneItem, CarItem]` 를 보면, 더 구체적인 `PlaneItem`이 `CarItem`보다 앞에 위치합니다. +<a href="https://docs.pydantic.dev/latest/concepts/types/#unions" class="external-link" target="_blank">`Union`</a>을 정의할 때는 더 구체적인 타입을 먼저 포함하고, 덜 구체적인 타입을 그 뒤에 나열해야 합니다. 아래 예제에서는 `Union[PlaneItem, CarItem]`에서 더 구체적인 `PlaneItem`이 `CarItem`보다 앞에 위치합니다. /// {* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *} - -### Python 3.10에서 `Union` +### Python 3.10에서 `Union` { #union-in-python-3-10 } 위의 예제에서는 `response_model` 인자 값으로 `Union[PlaneItem, CarItem]`을 전달합니다. -이 경우, 이를 **타입 어노테이션(type annotation)** 이 아닌 **인자 값(argument value)** 으로 전달하고 있기 때문에 Python 3.10에서도 `Union`을 사용해야 합니다. +이 경우, 이를 **타입 어노테이션(type annotation)**이 아닌 **인자 값(argument value)**으로 전달하고 있기 때문에 Python 3.10에서도 `Union`을 사용해야 합니다. 만약 타입 어노테이션에 사용한다면, 다음과 같이 수직 막대(|)를 사용할 수 있습니다: @@ -194,9 +184,9 @@ OpenAPI에서는 이를 `anyOf`로 정의합니다. some_variable: PlaneItem | CarItem ``` -하지만 이를 `response_model=PlaneItem | CarItem`과 같이 할당하면 에러가 발생합니다. 이는 Python이 이를 타입 어노테이션으로 해석하지 않고, `PlaneItem`과 `CarItem` 사이의 **잘못된 연산(invalid operation)**을 시도하기 때문입니다 +하지만 이를 `response_model=PlaneItem | CarItem`과 같이 할당하면 에러가 발생합니다. 이는 Python이 이를 타입 어노테이션으로 해석하지 않고, `PlaneItem`과 `CarItem` 사이의 **잘못된 연산(invalid operation)**을 시도하기 때문입니다. -## 모델 리스트 +## 모델 리스트 { #list-of-models } 마찬가지로, 객체 리스트 형태의 응답을 선언할 수도 있습니다. @@ -204,8 +194,7 @@ some_variable: PlaneItem | CarItem {* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *} - -## 임의의 `dict` 응답 +## 임의의 `dict` 응답 { #response-with-arbitrary-dict } Pydantic 모델을 사용하지 않고, 키와 값의 타입만 선언하여 평범한 임의의 `dict`로 응답을 선언할 수도 있습니다. @@ -215,9 +204,8 @@ Pydantic 모델을 사용하지 않고, 키와 값의 타입만 선언하여 평 {* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *} - -## 요약 +## 요약 { #recap } 여러 Pydantic 모델을 사용하고, 각 경우에 맞게 자유롭게 상속하세요. -엔터티가 서로 다른 "상태"를 가져야 하는 경우, 엔터티당 단일 데이터 모델을 사용할 필요는 없습니다. 예를 들어, 사용자 "엔터티"가 `password`, `password_hash`, 또는 비밀번호가 없는 상태를 포함할 수 있는 경우처럼 말입니다. +엔터티가 서로 다른 "상태"를 가져야 하는 경우, 엔터티당 단일 데이터 모델을 사용할 필요는 없습니다. 예를 들어, 사용자 "엔터티"가 `password`, `password_hash`, 그리고 비밀번호가 없는 상태를 포함할 수 있는 경우처럼 말입니다. diff --git a/docs/ko/docs/tutorial/first-steps.md b/docs/ko/docs/tutorial/first-steps.md index 20ce0d646..db78f1678 100644 --- a/docs/ko/docs/tutorial/first-steps.md +++ b/docs/ko/docs/tutorial/first-steps.md @@ -1,8 +1,8 @@ -# 첫걸음 +# 첫걸음 { #first-steps } 가장 단순한 FastAPI 파일은 다음과 같이 보일 것입니다: -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} 위 코드를 `main.py`에 복사합니다. @@ -11,36 +11,52 @@ <div class="termy"> ```console -$ uvicorn main:app --reload +$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:solid">main.py</u> -<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -<span style="color: green;">INFO</span>: Started reloader process [28720] -<span style="color: green;">INFO</span>: Started server process [28722] -<span style="color: green;">INFO</span>: Waiting for application startup. -<span style="color: green;">INFO</span>: Application startup complete. + <span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting development server 🚀 + + Searching for package file structure from directories + with <font color="#3465A4">__init__.py</font> files + Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py + + <span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with + the following code: + + <u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> + + <span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000/docs</u></font> + + <span style="background-color:#007166"><font color="#D3D7CF"> tip </font></span> Running in development mode, for production use: + <b>fastapi run</b> + + Logs: + + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Will watch for changes in these directories: + <b>[</b><font color="#4E9A06">'/home/user/code/awesomeapp'</font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> <b>(</b>Press CTRL+C + to quit<b>)</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started reloader process <b>[</b><font color="#34E2E2"><b>383138</b></font><b>]</b> using WatchFiles + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>383153</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. ``` </div> -/// note | 참고 - -`uvicorn main:app` 명령은 다음을 의미합니다: - -* `main`: 파일 `main.py` (파이썬 "모듈"). -* `app`: `main.py` 내부의 `app = FastAPI()` 줄에서 생성한 오브젝트. -* `--reload`: 코드 변경 시 자동으로 서버 재시작. 개발 시에만 사용. - -/// - 출력되는 줄들 중에는 아래와 같은 내용이 있습니다: ```hl_lines="4" INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` -해당 줄은 로컬에서 앱이 서비스되는 URL을 보여줍니다. +해당 줄은 로컬 머신에서 앱이 서비스되는 URL을 보여줍니다. -### 확인하기 +### 확인하기 { #check-it } 브라우저로 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>를 여세요. @@ -50,7 +66,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) {"message": "Hello World"} ``` -### 대화형 API 문서 +### 대화형 API 문서 { #interactive-api-docs } 이제 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>로 가봅니다. @@ -58,7 +74,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### 대안 API 문서 +### 대안 API 문서 { #alternative-api-docs } 그리고 이제, <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>로 가봅니다. @@ -66,41 +82,41 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -### OpenAPI +### OpenAPI { #openapi } **FastAPI**는 API를 정의하기 위한 **OpenAPI** 표준을 사용하여 여러분의 모든 API를 이용해 "스키마"를 생성합니다. -#### "스키마" +#### "스키마" { #schema } "스키마"는 무언가의 정의 또는 설명입니다. 이를 구현하는 코드가 아니라 추상적인 설명일 뿐입니다. -#### API "스키마" +#### API "스키마" { #api-schema } <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a>는 API의 스키마를 어떻게 정의하는지 지시하는 규격입니다. 이 스키마 정의는 API 경로, 가능한 매개변수 등을 포함합니다. -#### 데이터 "스키마" +#### 데이터 "스키마" { #data-schema } "스키마"라는 용어는 JSON처럼 어떤 데이터의 형태를 나타낼 수도 있습니다. 이러한 경우 JSON 속성, 가지고 있는 데이터 타입 등을 뜻합니다. -#### OpenAPI와 JSON 스키마 +#### OpenAPI와 JSON 스키마 { #openapi-and-json-schema } OpenAPI는 당신의 API에 대한 API 스키마를 정의합니다. 또한 이 스키마는 JSON 데이터 스키마의 표준인 **JSON 스키마**를 사용하여 당신의 API가 보내고 받는 데이터의 정의(또는 "스키마")를 포함합니다. -#### `openapi.json` 확인 +#### `openapi.json` 확인 { #check-the-openapi-json } -FastAPI는 자동으로 API의 설명과 함께 JSON (스키마)를 생성합니다. +가공되지 않은 OpenAPI 스키마가 어떻게 생겼는지 궁금하다면, FastAPI는 자동으로 여러분의 모든 API에 대한 설명과 함께 JSON (스키마)를 생성합니다. -가공되지 않은 OpenAPI 스키마가 어떻게 생겼는지 궁금하다면, 여기에서 직접 볼 수 있습니다: <a href="http://127.0.0.1:8000/openapi.json" class="external-link" target="_blank">http://127.0.0.1:8000/openapi.json</a>. +여기에서 직접 볼 수 있습니다: <a href="http://127.0.0.1:8000/openapi.json" class="external-link" target="_blank">http://127.0.0.1:8000/openapi.json</a>. 다음과 같이 시작하는 JSON을 확인할 수 있습니다: ```JSON { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" @@ -119,7 +135,7 @@ FastAPI는 자동으로 API의 설명과 함께 JSON (스키마)를 생성합니 ... ``` -#### OpenAPI의 용도 +#### OpenAPI의 용도 { #what-is-openapi-for } OpenAPI 스키마는 포함된 두 개의 대화형 문서 시스템을 제공합니다. @@ -127,11 +143,47 @@ OpenAPI 스키마는 포함된 두 개의 대화형 문서 시스템을 제공 API와 통신하는 클라이언트(프론트엔드, 모바일, IoT 애플리케이션 등)를 위해 코드를 자동으로 생성하는 데도 사용할 수 있습니다. -## 단계별 요약 +### 앱 배포하기(선택 사항) { #deploy-your-app-optional } -### 1 단계: `FastAPI` 임포트 +선택적으로 FastAPI 앱을 <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>에 배포할 수 있습니다. 아직 대기자 명단에 등록하지 않았다면, 등록하러 가세요. 🚀 -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +이미 **FastAPI Cloud** 계정이 있다면(대기자 명단에서 초대해 드렸습니다 😉), 한 번의 명령으로 애플리케이션을 배포할 수 있습니다. + +배포하기 전에, 로그인되어 있는지 확인하세요: + +<div class="termy"> + +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +</div> + +그 다음 앱을 배포합니다: + +<div class="termy"> + +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +</div> + +이게 전부입니다! 이제 해당 URL에서 앱에 접근할 수 있습니다. ✨ + +## 단계별 요약 { #recap-step-by-step } + +### 1 단계: `FastAPI` 임포트 { #step-1-import-fastapi } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} `FastAPI`는 당신의 API를 위한 모든 기능을 제공하는 파이썬 클래스입니다. @@ -143,45 +195,17 @@ API와 통신하는 클라이언트(프론트엔드, 모바일, IoT 애플리케 /// -### 2 단계: `FastAPI` "인스턴스" 생성 +### 2 단계: `FastAPI` "인스턴스" 생성 { #step-2-create-a-fastapi-instance } -{* ../../docs_src/first_steps/tutorial001.py hl[3] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[3] *} 여기에서 `app` 변수는 `FastAPI` 클래스의 "인스턴스"가 됩니다. 이것은 당신의 모든 API를 생성하기 위한 상호작용의 주요 지점이 될 것입니다. -이 `app`은 다음 명령에서 `uvicorn`이 참조하고 있는 것과 동일합니다: +### 3 단계: *경로 처리* 생성 { #step-3-create-a-path-operation } -<div class="termy"> - -```console -$ uvicorn main:app --reload - -<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -</div> - -아래처럼 앱을 만든다면: - -{* ../../docs_src/first_steps/tutorial002.py hl[3] *} - -이를 `main.py` 파일에 넣고, `uvicorn`을 아래처럼 호출해야 합니다: - -<div class="termy"> - -```console -$ uvicorn main:my_awesome_api --reload - -<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -</div> - -### 3 단계: *경로 작동* 생성 - -#### 경로 +#### 경로 { #path } 여기서 "경로"는 첫 번째 `/`부터 시작하는 URL의 뒷부분을 의미합니다. @@ -205,7 +229,7 @@ https://example.com/items/foo API를 설계할 때 "경로"는 "관심사"와 "리소스"를 분리하기 위한 주요한 방법입니다. -#### 작동 +#### 작동 { #operation } "작동(Operation)"은 HTTP "메소드" 중 하나를 나타냅니다. @@ -240,14 +264,14 @@ API를 설계할 때 일반적으로 특정 행동을 수행하기 위해 특정 우리 역시 이제부터 메소드를 "**작동**"이라고 부를 것입니다. -#### *경로 작동 데코레이터* 정의 +#### *경로 처리 데코레이터* 정의 { #define-a-path-operation-decorator } -{* ../../docs_src/first_steps/tutorial001.py hl[6] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *} -`@app.get("/")`은 **FastAPI**에게 바로 아래에 있는 함수가 다음으로 이동하는 요청을 처리한다는 것을 알려줍니다. +`@app.get("/")`은 **FastAPI**에게 바로 아래에 있는 함수가 다음으로 이동하는 요청을 처리한다는 것을 알려줍니다: * 경로 `/` -* <abbr title="HTTP GET 메소드"><code>get</code> 작동</abbr> 사용 +* <abbr title="an HTTP GET method"><code>get</code> operation</abbr> 사용 /// info | `@decorator` 정보 @@ -259,7 +283,7 @@ API를 설계할 때 일반적으로 특정 행동을 수행하기 위해 특정 우리의 경우, 이 데코레이터는 **FastAPI**에게 아래 함수가 **경로** `/`의 `get` **작동**에 해당한다고 알려줍니다. -이것이 "**경로 작동 데코레이터**"입니다. +이것이 "**경로 처리 데코레이터**"입니다. /// @@ -288,15 +312,15 @@ API를 설계할 때 일반적으로 특정 행동을 수행하기 위해 특정 /// -### 4 단계: **경로 작동 함수** 정의 +### 4 단계: **경로 처리 함수** 정의 { #step-4-define-the-path-operation-function } -다음은 우리의 "**경로 작동 함수**"입니다: +다음은 우리의 "**경로 처리 함수**"입니다: * **경로**: 는 `/`입니다. * **작동**: 은 `get`입니다. * **함수**: 는 "데코레이터" 아래에 있는 함수입니다 (`@app.get("/")` 아래). -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} 이것은 파이썬 함수입니다. @@ -308,17 +332,17 @@ URL "`/`"에 대한 `GET` 작동을 사용하는 요청을 받을 때마다 **Fa `async def`을 이용하는 대신 일반 함수로 정의할 수 있습니다: -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note | 참고 -차이점을 모르겠다면 [Async: *"바쁘신 경우"*](../async.md#_1){.internal-link target=_blank}을 확인하세요. +차이점을 모르겠다면 [Async: *"바쁘신 경우"*](../async.md#in-a-hurry){.internal-link target=_blank}를 확인하세요. /// -### 5 단계: 콘텐츠 반환 +### 5 단계: 콘텐츠 반환 { #step-5-return-the-content } -{* ../../docs_src/first_steps/tutorial001.py hl[8] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *} `dict`, `list`, 단일값을 가진 `str`, `int` 등을 반환할 수 있습니다. @@ -326,10 +350,31 @@ Pydantic 모델을 반환할 수도 있습니다(나중에 더 자세히 살펴 JSON으로 자동 변환되는 객체들과 모델들(ORM 등을 포함해서)이 많이 있습니다. 가장 마음에 드는 것을 사용하십시오, 이미 지원되고 있을 것입니다. -## 요약 +### 6 단계: 배포하기 { #step-6-deploy-it } + +한 번의 명령으로 **<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>**에 앱을 배포합니다: `fastapi deploy`. 🎉 + +#### FastAPI Cloud 소개 { #about-fastapi-cloud } + +**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>**는 **FastAPI** 뒤에 있는 동일한 작성자와 팀이 만들었습니다. + +최소한의 노력으로 API를 **빌드**, **배포**, **접근**하는 과정을 간소화합니다. + +FastAPI로 앱을 빌드할 때의 동일한 **개발자 경험**을 클라우드에 **배포**할 때도 제공합니다. 🎉 + +FastAPI Cloud는 *FastAPI와 친구들* 오픈 소스 프로젝트의 주요 스폰서이자 자금 제공자입니다. ✨ + +#### 다른 클라우드 제공업체에 배포하기 { #deploy-to-other-cloud-providers } + +FastAPI는 오픈 소스이며 표준을 기반으로 합니다. 선택한 어떤 클라우드 제공업체에도 FastAPI 앱을 배포할 수 있습니다. + +클라우드 제공업체의 가이드를 따라 FastAPI 앱을 배포하세요. 🤓 + +## 요약 { #recap } * `FastAPI` 임포트. * `app` 인스턴스 생성. -* (`@app.get("/")`처럼) **경로 작동 데코레이터** 작성. -* (위에 있는 `def root(): ...`처럼) **경로 작동 함수** 작성. -* (`uvicorn main:app --reload`처럼) 개발 서버 실행. +* (`@app.get("/")`처럼) **경로 처리 데코레이터** 작성. +* (위에 있는 `def root(): ...`처럼) **경로 처리 함수** 작성. +* `fastapi dev` 명령으로 개발 서버 실행. +* 선택적으로 `fastapi deploy`로 앱 배포. diff --git a/docs/ko/docs/tutorial/header-param-models.md b/docs/ko/docs/tutorial/header-param-models.md index bab7291e3..ad872e153 100644 --- a/docs/ko/docs/tutorial/header-param-models.md +++ b/docs/ko/docs/tutorial/header-param-models.md @@ -1,4 +1,4 @@ -# 헤더 매개변수 모델 +# 헤더 매개변수 모델 { #header-parameter-models } 관련 있는 **헤더 매개변수** 그룹이 있는 경우, **Pydantic 모델**을 생성하여 선언할 수 있습니다. @@ -10,7 +10,7 @@ /// -## Pydantic 모델을 사용한 헤더 매개변수 +## Pydantic 모델을 사용한 헤더 매개변수 { #header-parameters-with-a-pydantic-model } **Pydantic 모델**에 필요한 **헤더 매개변수**를 선언한 다음, 해당 매개변수를 `Header`로 선언합니다: @@ -18,7 +18,7 @@ **FastAPI**는 요청에서 받은 **헤더**에서 **각 필드**에 대한 데이터를 **추출**하고 정의한 Pydantic 모델을 줍니다. -## 문서 확인하기 +## 문서 확인하기 { #check-the-docs } 문서 UI `/docs`에서 필요한 헤더를 볼 수 있습니다: @@ -26,7 +26,7 @@ <img src="/img/tutorial/header-param-models/image01.png"> </div> -## 추가 헤더 금지하기 +## 추가 헤더 금지하기 { #forbid-extra-headers } 일부 특별한 사용 사례(흔하지는 않겠지만)에서는 수신하려는 헤더를 **제한**할 수 있습니다. @@ -51,6 +51,22 @@ Pydantic의 모델 구성을 사용하여 추가(`extra`) 필드를 금지(`forb } ``` -## 요약 +## 밑줄 변환 비활성화하기 { #disable-convert-underscores } + +일반적인 헤더 매개변수와 마찬가지로, 매개변수 이름에 밑줄 문자가 있으면 **자동으로 하이픈으로 변환**됩니다. + +예를 들어, 코드에 `save_data` 헤더 매개변수가 있으면, 기대되는 HTTP 헤더는 `save-data`이고, 문서에서도 그렇게 표시됩니다. + +어떤 이유로든 이 자동 변환을 비활성화해야 한다면, 헤더 매개변수용 Pydantic 모델에서도 비활성화할 수 있습니다. + +{* ../../docs_src/header_param_models/tutorial003_an_py310.py hl[19] *} + +/// warning | 경고 + +`convert_underscores`를 `False`로 설정하기 전에, 일부 HTTP 프록시와 서버에서는 밑줄이 포함된 헤더 사용을 허용하지 않는다는 점을 염두에 두세요. + +/// + +## 요약 { #summary } **Pydantic 모델**을 사용하여 **FastAPI**에서 **헤더**를 선언할 수 있습니다. 😎 diff --git a/docs/ko/docs/tutorial/header-params.md b/docs/ko/docs/tutorial/header-params.md index 7379eb2a0..4c47644d8 100644 --- a/docs/ko/docs/tutorial/header-params.md +++ b/docs/ko/docs/tutorial/header-params.md @@ -1,20 +1,20 @@ -# 헤더 매개변수 +# 헤더 매개변수 { #header-parameters } 헤더 매개변수를 `Query`, `Path` 그리고 `Cookie` 매개변수들과 같은 방식으로 정의할 수 있습니다. -## `Header` 임포트 +## `Header` 임포트 { #import-header } 먼저 `Header`를 임포트합니다: -{* ../../docs_src/header_params/tutorial001.py hl[3] *} +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *} -## `Header` 매개변수 선언 +## `Header` 매개변수 선언 { #declare-header-parameters } `Path`, `Query` 그리고 `Cookie`를 사용한 동일한 구조를 이용하여 헤더 매개변수를 선언합니다. 첫 번째 값은 기본값이며, 추가 검증이나 어노테이션 매개변수 모두 전달할 수 있습니다: -{* ../../docs_src/header_params/tutorial001.py hl[9] *} +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[9] *} /// note | 기술 세부사항 @@ -30,7 +30,7 @@ /// -## 자동 변환 +## 자동 변환 { #automatic-conversion } `Header`는 `Path`, `Query` 그리고 `Cookie`가 제공하는 것 외에 기능이 조금 더 있습니다. @@ -46,15 +46,15 @@ 만약 언더스코어를 하이픈으로 자동 변환을 비활성화해야 할 어떤 이유가 있다면, `Header`의 `convert_underscores` 매개변수를 `False`로 설정하십시오: -{* ../../docs_src/header_params/tutorial002.py hl[10] *} +{* ../../docs_src/header_params/tutorial002_an_py310.py hl[10] *} /// warning | 경고 -`convert_underscore`를 `False`로 설정하기 전에, 어떤 HTTP 프록시들과 서버들은 언더스코어가 포함된 헤더 사용을 허락하지 않는다는 것을 명심하십시오. +`convert_underscores`를 `False`로 설정하기 전에, 어떤 HTTP 프록시들과 서버들은 언더스코어가 포함된 헤더 사용을 허락하지 않는다는 것을 명심하십시오. /// -## 중복 헤더 +## 중복 헤더 { #duplicate-headers } 중복 헤더들을 수신할 수 있습니다. 즉, 다중값을 갖는 동일한 헤더를 뜻합니다. @@ -64,9 +64,9 @@ 예를 들어, 두 번 이상 나타날 수 있는 `X-Token`헤더를 선언하려면, 다음과 같이 작성합니다: -{* ../../docs_src/header_params/tutorial003.py hl[9] *} +{* ../../docs_src/header_params/tutorial003_an_py310.py hl[9] *} -다음과 같은 두 개의 HTTP 헤더를 전송하여 해당 *경로* 와 통신할 경우: +다음과 같은 두 개의 HTTP 헤더를 전송하여 해당 *경로 처리* 와 통신할 경우: ``` X-Token: foo @@ -84,7 +84,7 @@ X-Token: bar } ``` -## 요약 +## 요약 { #recap } `Header`는 `Query`, `Path`, `Cookie`와 동일한 패턴을 사용하여 선언합니다. diff --git a/docs/ko/docs/tutorial/index.md b/docs/ko/docs/tutorial/index.md index 9f5328992..c44aac2d4 100644 --- a/docs/ko/docs/tutorial/index.md +++ b/docs/ko/docs/tutorial/index.md @@ -1,84 +1,95 @@ -# 자습서 - 사용자 안내서 +# 자습서 - 사용자 안내서 { #tutorial-user-guide } -이 자습서는 단계별로 **FastAPI**의 대부분의 기능에 대해 설명합니다. +이 자습서는 **FastAPI**의 대부분의 기능을 단계별로 사용하는 방법을 보여줍니다. -각 섹션은 이전 섹션에 기반하는 순차적인 구조로 작성되었지만, 각 주제로 구분되어 있기 때문에 필요에 따라 특정 섹션으로 바로 이동하여 필요한 내용을 바로 확인할 수 있습니다. +각 섹션은 이전 섹션을 바탕으로 점진적으로 구성되지만, 주제를 분리한 구조로 되어 있어 특정 API 요구사항을 해결하기 위해 원하는 섹션으로 바로 이동할 수 있습니다. -또한 향후에도 참조 자료로 쓰일 수 있도록 작성되었습니다. +또한 나중에 참고 자료로도 사용할 수 있도록 만들어졌으므로, 필요할 때 다시 돌아와 정확히 필요한 내용을 확인할 수 있습니다. -그러므로 필요할 때에 다시 돌아와서 원하는 것을 정확히 찾을 수 있습니다. +## 코드 실행하기 { #run-the-code } -## 코드 실행하기 +모든 코드 블록은 복사해서 바로 사용할 수 있습니다(실제로 테스트된 Python 파일입니다). -모든 코드 블록은 복사하여 바로 사용할 수 있습니다(실제로 테스트된 파이썬 파일입니다). - -예제를 실행하려면 코드를 `main.py` 파일에 복사하고 다음을 사용하여 `uvicorn`을 시작합니다: +예제 중 어떤 것이든 실행하려면, 코드를 `main.py` 파일에 복사하고 다음으로 `fastapi dev`를 시작하세요: <div class="termy"> ```console -$ uvicorn main:app --reload +$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:solid">main.py</u> -<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -<span style="color: green;">INFO</span>: Started reloader process [28720] -<span style="color: green;">INFO</span>: Started server process [28722] -<span style="color: green;">INFO</span>: Waiting for application startup. -<span style="color: green;">INFO</span>: Application startup complete. + <span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting development server 🚀 + + Searching for package file structure from directories + with <font color="#3465A4">__init__.py</font> files + Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py + + <span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with + the following code: + + <u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> + + <span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000/docs</u></font> + + <span style="background-color:#007166"><font color="#D3D7CF"> tip </font></span> Running in development mode, for production use: + <b>fastapi run</b> + + Logs: + + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Will watch for changes in these directories: + <b>[</b><font color="#4E9A06">'/home/user/code/awesomeapp'</font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> <b>(</b>Press CTRL+C + to quit<b>)</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started reloader process <b>[</b><font color="#34E2E2"><b>383138</b></font><b>]</b> using WatchFiles + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>383153</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. ``` </div> -코드를 작성하거나 복사, 편집할 때, 로컬 환경에서 실행하는 것을 **강력히 권장**합니다. - -로컬 편집기에서 사용한다면, 모든 타입 검사와 자동완성 등 작성해야 하는 코드가 얼마나 적은지 보면서 FastAPI의 이점을 비로소 경험할 수 있습니다. +코드를 작성하거나 복사한 뒤 편집하고, 로컬에서 실행하는 것을 **강력히 권장**합니다. +에디터에서 사용해 보면, 작성해야 하는 코드가 얼마나 적은지, 모든 타입 검사와 자동완성 등 FastAPI의 이점을 제대로 확인할 수 있습니다. --- -## FastAPI 설치 +## FastAPI 설치 { #install-fastapi } -첫 번째 단계는 FastAPI를 설치하는 것입니다. +첫 단계는 FastAPI를 설치하는 것입니다. -자습시에는 모든 선택적인 의존성 및 기능을 함께 설치하는 것을 추천합니다: +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고 활성화한 다음, **FastAPI를 설치**하세요: <div class="termy"> ```console -$ pip install "fastapi[all]" +$ pip install "fastapi[standard]" ---> 100% ``` </div> -...이는 코드를 실행하는 서버로 사용할 수 있는 `uvicorn` 또한 포함하고 있습니다. - /// note | 참고 -부분적으로 설치할 수도 있습니다. +`pip install "fastapi[standard]"`로 설치하면 `fastapi-cloud-cli`를 포함한 몇 가지 기본 선택적 standard 의존성이 함께 설치되며, 이를 사용해 <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>에 배포할 수 있습니다. -애플리케이션을 운영 환경에 배포하려는 경우 다음과 같이 합니다: +이러한 선택적 의존성이 필요 없다면 `pip install fastapi`로 대신 설치할 수 있습니다. -``` -pip install fastapi -``` - -추가로 서버 역할을 하는 `uvicorn`을 설치합니다: - -``` -pip install uvicorn -``` - -사용하려는 각 선택적인 의존성에 대해서도 동일합니다. +standard 의존성은 설치하되 `fastapi-cloud-cli` 없이 설치하려면 `pip install "fastapi[standard-no-fastapi-cloud-cli]"`로 설치할 수 있습니다. /// -## 고급 사용자 안내서 +## 고급 사용자 안내서 { #advanced-user-guide } -이 **자습서 - 사용자 안내서** 다음에 읽을 수 있는 **고급 사용자 안내서**도 있습니다. +이 **자습서 - 사용자 안내서**를 읽은 뒤에 나중에 읽을 수 있는 **고급 사용자 안내서**도 있습니다. -**고급 사용자 안내서**는 현재 문서를 기반으로 하고, 동일한 개념을 사용하며, 추가적인 기능들에 대해 설명합니다. +**고급 사용자 안내서**는 이 문서를 바탕으로 동일한 개념을 사용하며, 몇 가지 추가 기능을 알려줍니다. -하지만 (지금 읽고 있는) **자습서 - 사용자 안내서**를 먼저 읽는 것을 권장합니다. +하지만 먼저 **자습서 - 사용자 안내서**(지금 읽고 있는 내용)를 읽어야 합니다. -**자습서 - 사용자 안내서**만으로도 완전한 애플리케이션을 구축할 수 있도록 작성되었으며, 필요에 따라 **고급 사용자 안내서**의 추가적인 아이디어를 적용하여 다양한 방식으로 확장할 수 있습니다. +**자습서 - 사용자 안내서**만으로 완전한 애플리케이션을 만들 수 있도록 설계되었고, 필요에 따라 **고급 사용자 안내서**의 추가 아이디어를 활용해 다양한 방식으로 확장할 수 있습니다. diff --git a/docs/ko/docs/tutorial/metadata.md b/docs/ko/docs/tutorial/metadata.md index a50dfa2e7..1d1df4925 100644 --- a/docs/ko/docs/tutorial/metadata.md +++ b/docs/ko/docs/tutorial/metadata.md @@ -1,26 +1,26 @@ -# 메타데이터 및 문서화 URL +# 메타데이터 및 문서화 URL { #metadata-and-docs-urls } -**FastAPI** 응용 프로그램에서 다양한 메타데이터 구성을 사용자 맞춤 설정할 수 있습니다. +**FastAPI** 애플리케이션에서 다양한 메타데이터 구성을 사용자 맞춤 설정할 수 있습니다. -## API에 대한 메타데이터 +## API에 대한 메타데이터 { #metadata-for-api } OpenAPI 명세 및 자동화된 API 문서 UI에 사용되는 다음 필드를 설정할 수 있습니다: | 매개변수 | 타입 | 설명 | |----------|------|-------| | `title` | `str` | API의 제목입니다. | -| `summary` | `str` | API에 대한 짧은 요약입니다. <small>OpenAPI 3.1.0, FastAPI 0.99.0부터 사용 가능</small> | +| `summary` | `str` | API에 대한 짧은 요약입니다. <small>OpenAPI 3.1.0, FastAPI 0.99.0부터 사용 가능.</small> | | `description` | `str` | API에 대한 짧은 설명입니다. 마크다운을 사용할 수 있습니다. | -| `version` | `string` | API의 버전입니다. OpenAPI의 버전이 아닌, 여러분의 애플리케이션의 버전을 나타냅니다. 예: `2.5.0` | +| `version` | `string` | API의 버전입니다. OpenAPI의 버전이 아닌, 여러분의 애플리케이션의 버전을 나타냅니다. 예: `2.5.0`. | | `terms_of_service` | `str` | API 이용 약관의 URL입니다. 제공하는 경우 URL 형식이어야 합니다. | | `contact` | `dict` | 노출된 API에 대한 연락처 정보입니다. 여러 필드를 포함할 수 있습니다. <details><summary><code>contact</code> 필드</summary><table><thead><tr><th>매개변수</th><th>타입</th><th>설명</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td>연락처 인물/조직의 식별명입니다.</td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>연락처 정보가 담긴 URL입니다. URL 형식이어야 합니다.</td></tr><tr><td><code>email</code></td><td><code>str</code></td><td>연락처 인물/조직의 이메일 주소입니다. 이메일 주소 형식이어야 합니다.</td></tr></tbody></table></details> | -| `license_info` | `dict` | 노출된 API의 라이선스 정보입니다. 여러 필드를 포함할 수 있습니다. <details><summary><code>license_info</code> 필드</summary><table><thead><tr><th>매개변수</th><th>타입</th><th>설명</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td><strong>필수</strong> (<code>license_info</code>가 설정된 경우). API에 사용된 라이선스 이름입니다.</td></tr><tr><td><code>identifier</code></td><td><code>str</code></td><td>API에 대한 <a href="https://spdx.org/licenses/" class="external-link" target="_blank">SPDX</a> 라이선스 표현입니다. <code>identifier</code> 필드는 <code>url</code> 필드와 상호 배타적입니다. <small>OpenAPI 3.1.0, FastAPI 0.99.0부터 사용 가능</small></td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>API에 사용된 라이선스의 URL입니다. URL 형식이어야 합니다.</td></tr></tbody></table></details> | +| `license_info` | `dict` | 노출된 API의 라이선스 정보입니다. 여러 필드를 포함할 수 있습니다. <details><summary><code>license_info</code> 필드</summary><table><thead><tr><th>매개변수</th><th>타입</th><th>설명</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td><strong>필수</strong> (<code>license_info</code>가 설정된 경우). API에 사용된 라이선스 이름입니다.</td></tr><tr><td><code>identifier</code></td><td><code>str</code></td><td>API에 대한 <a href="https://spdx.org/licenses/" class="external-link" target="_blank">SPDX</a> 라이선스 표현입니다. <code>identifier</code> 필드는 <code>url</code> 필드와 상호 배타적입니다. <small>OpenAPI 3.1.0, FastAPI 0.99.0부터 사용 가능.</small></td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>API에 사용된 라이선스의 URL입니다. URL 형식이어야 합니다.</td></tr></tbody></table></details> | 다음과 같이 설정할 수 있습니다: -{* ../../docs_src/metadata/tutorial001.py hl[3:16,19:32] *} +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} -/// tip +/// tip | 팁 `description` 필드에 마크다운을 사용할 수 있으며, 출력에서 렌더링됩니다. @@ -30,79 +30,81 @@ OpenAPI 명세 및 자동화된 API 문서 UI에 사용되는 다음 필드를 <img src="/img/tutorial/metadata/image01.png"> -## 라이선스 식별자 +## 라이선스 식별자 { #license-identifier } -OpenAPI 3.1.0 및 FastAPI 0.99.0부터 `license_info`에 `identifier`를 URL 대신 설정할 수 있습니다. +OpenAPI 3.1.0 및 FastAPI 0.99.0부터 `license_info`에 `url` 대신 `identifier`를 설정할 수도 있습니다. 예: -{* ../../docs_src/metadata/tutorial001_1.py hl[31] *} +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} -## 태그에 대한 메타데이터 +## 태그에 대한 메타데이터 { #metadata-for-tags } -`openapi_tags` 매개변수를 사용하여 경로 작동을 그룹화하는 데 사용되는 태그에 추가 메타데이터를 추가할 수 있습니다. +`openapi_tags` 매개변수를 사용하여 경로 처리을 그룹화하는 데 사용되는 여러 태그에 추가 메타데이터를 추가할 수도 있습니다. -리스트는 각 태그에 대해 하나의 딕셔너리를 포함해야 합니다. +리스트는 각 태그에 대해 하나의 딕셔너리를 포함합니다. 각 딕셔너리에는 다음이 포함될 수 있습니다: -* `name` (**필수**): `tags` 매개변수에서 *경로 작동*과 `APIRouter`에 사용된 태그 이름과 동일한 `str`입니다. -* `description`: 태그에 대한 간단한 설명을 담은 `str`입니다. 마크다운을 사용할 수 있으며 문서 UI에 표시됩니다. +* `name` (**필수**): *경로 처리* 및 `APIRouter`의 `tags` 매개변수에서 사용하는 태그 이름과 동일한 `str`입니다. +* `description`: 태그에 대한 간단한 설명을 담은 `str`입니다. 마크다운을 포함할 수 있으며 문서 UI에 표시됩니다. * `externalDocs`: 외부 문서를 설명하는 `dict`이며: * `description`: 외부 문서에 대한 간단한 설명을 담은 `str`입니다. * `url` (**필수**): 외부 문서의 URL을 담은 `str`입니다. -### 태그에 대한 메타데이터 생성 +### 태그에 대한 메타데이터 생성 { #create-metadata-for-tags } -`users` 및 `items`에 대한 태그 예시와 함께 메타데이터를 생성하고 이를 `openapi_tags` 매개변수로 전달해 보겠습니다: +`users` 및 `items`에 대한 태그 예시로 시도해 보겠습니다. -{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *} +태그에 대한 메타데이터를 생성하고 이를 `openapi_tags` 매개변수로 전달하세요: -설명 안에 마크다운을 사용할 수 있습니다. 예를 들어 "login"은 굵게(**login**) 표시되고, "fancy"는 기울임꼴(_fancy_)로 표시됩니다. +{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *} -/// tip +설명 안에 마크다운을 사용할 수 있다는 점에 유의하세요. 예를 들어 "login"은 굵게(**login**) 표시되고, "fancy"는 기울임꼴(_fancy_)로 표시됩니다. + +/// tip | 팁 사용 중인 모든 태그에 메타데이터를 추가할 필요는 없습니다. /// -### 태그 사용 +### 태그 사용 { #use-your-tags } -`tags` 매개변수를 *경로 작동* 및 `APIRouter`와 함께 사용하여 태그에 할당할 수 있습니다: +`tags` 매개변수를 *경로 처리* (및 `APIRouter`)와 함께 사용하여 이를 서로 다른 태그에 할당하세요: -{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} -/// info +/// info | 정보 -태그에 대한 자세한 내용은 [경로 작동 구성](path-operation-configuration.md#tags){.internal-link target=_blank}에서 읽어보세요. +태그에 대한 자세한 내용은 [경로 처리 구성](path-operation-configuration.md#tags){.internal-link target=_blank}에서 읽어보세요. /// -### 문서 확인 +### 문서 확인 { #check-the-docs } 이제 문서를 확인하면 모든 추가 메타데이터가 표시됩니다: <img src="/img/tutorial/metadata/image02.png"> -### 태그 순서 +### 태그 순서 { #order-of-tags } -각 태그 메타데이터 딕셔너리의 순서는 문서 UI에 표시되는 순서를 정의합니다. +각 태그 메타데이터 딕셔너리의 순서는 문서 UI에 표시되는 순서도 정의합니다. -예를 들어, 알파벳 순서상 `users`는 `items` 뒤에 오지만, 우리는 `users` 메타데이터를 리스트의 첫 번째 딕셔너리로 추가했기 때문에 먼저 표시됩니다. +예를 들어, 알파벳 순서상 `users`는 `items` 뒤에 오지만, 우리는 해당 메타데이터를 리스트의 첫 번째 딕셔너리로 추가했기 때문에 먼저 표시됩니다. -## OpenAPI URL +## OpenAPI URL { #openapi-url } -OpenAPI 구조는 기본적으로 `/openapi.json`에서 제공됩니다. +기본적으로 OpenAPI 스키마는 `/openapi.json`에서 제공됩니다. -`openapi_url` 매개변수를 통해 이를 설정할 수 있습니다. +하지만 `openapi_url` 매개변수로 이를 설정할 수 있습니다. -예를 들어, 이를 `/api/v1/openapi.json`에 제공하도록 설정하려면: +예를 들어, 이를 `/api/v1/openapi.json`에서 제공하도록 설정하려면: -{* ../../docs_src/metadata/tutorial002.py hl[3] *} +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} -OpenAPI 구조를 완전히 비활성화하려면 `openapi_url=None`으로 설정할 수 있으며, 이를 사용하여 문서화 사용자 인터페이스도 비활성화됩니다. +OpenAPI 스키마를 완전히 비활성화하려면 `openapi_url=None`으로 설정할 수 있으며, 이를 사용하여 문서화 사용자 인터페이스도 비활성화됩니다. -## 문서화 URL +## 문서화 URL { #docs-urls } 포함된 두 가지 문서화 사용자 인터페이스를 설정할 수 있습니다: @@ -115,4 +117,4 @@ OpenAPI 구조를 완전히 비활성화하려면 `openapi_url=None`으로 설 예를 들어, Swagger UI를 `/documentation`에서 제공하고 ReDoc을 비활성화하려면: -{* ../../docs_src/metadata/tutorial003.py hl[3] *} +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/ko/docs/tutorial/middleware.md b/docs/ko/docs/tutorial/middleware.md index e0daa3c99..c213c5074 100644 --- a/docs/ko/docs/tutorial/middleware.md +++ b/docs/ko/docs/tutorial/middleware.md @@ -1,66 +1,95 @@ -# 미들웨어 +# 미들웨어 { #middleware } 미들웨어를 **FastAPI** 응용 프로그램에 추가할 수 있습니다. -"미들웨어"는 특정 *경로 작동*에 의해 처리되기 전, 모든 **요청**에 대해서 동작하는 함수입니다. 또한 모든 **응답**이 반환되기 전에도 동일하게 동작합니다. +"미들웨어"는 특정 *경로 처리*에 의해 처리되기 전, 모든 **요청**에 대해서 동작하는 함수입니다. 또한 모든 **응답**이 반환되기 전에도 동일하게 동작합니다. -* 미들웨어는 응용 프로그램으로 오는 **요청**를 가져옵니다. -* **요청** 또는 다른 필요한 코드를 실행 시킬 수 있습니다. -* **요청**을 응용 프로그램의 *경로 작동*으로 전달하여 처리합니다. -* 애플리케이션의 *경로 작업*에서 생성한 **응답**를 받습니다. -* **응답** 또는 다른 필요한 코드를 실행시키는 동작을 할 수 있습니다. -* **응답**를 반환합니다. +* 미들웨어는 응용 프로그램으로 오는 각 **요청**을 가져옵니다. +* 그런 다음 해당 **요청**에 대해 무언가를 하거나 필요한 코드를 실행할 수 있습니다. +* 그런 다음 **요청**을 나머지 애플리케이션(어떤 *경로 처리*가)을 통해 처리되도록 전달합니다. +* 그런 다음 애플리케이션(어떤 *경로 처리*가)이 생성한 **응답**을 가져옵니다. +* 그런 다음 해당 **응답**에 대해 무언가를 하거나 필요한 코드를 실행할 수 있습니다. +* 그런 다음 **응답**을 반환합니다. /// note | 기술 세부사항 -만약 `yield`를 사용한 의존성을 가지고 있다면, 미들웨어가 실행되고 난 후에 exit이 실행됩니다. +`yield`를 사용하는 의존성이 있다면, exit 코드는 미들웨어 *후에* 실행됩니다. -만약 (나중에 문서에서 다룰) 백그라운드 작업이 있다면, 모든 미들웨어가 실행되고 *난 후에* 실행됩니다. +백그라운드 작업(뒤에서 보게 될 [Background Tasks](background-tasks.md){.internal-link target=_blank} 섹션에서 다룹니다)이 있다면, 모든 미들웨어 *후에* 실행됩니다. /// -## 미들웨어 만들기 +## 미들웨어 만들기 { #create-a-middleware } -미들웨어를 작성하기 위해서 함수 상단에 `@app.middleware("http")` 데코레이터를 사용할 수 있습니다. +미들웨어를 만들기 위해 함수 상단에 데코레이터 `@app.middleware("http")`를 사용합니다. -미들웨어 함수는 다음 항목들을 받습니다: +미들웨어 함수는 다음을 받습니다: * `request`. * `request`를 매개변수로 받는 `call_next` 함수. - * 이 함수는 `request`를 해당하는 *경로 작업*으로 전달합니다. - * 그런 다음, *경로 작업*에 의해 생성된 `response` 를 반환합니다. -* `response`를 반환하기 전에 추가로 `response`를 수정할 수 있습니다. + * 이 함수는 `request`를 해당하는 *경로 처리*로 전달합니다. + * 그런 다음 해당 *경로 처리*가 생성한 `response`를 반환합니다. +* 그런 다음 반환하기 전에 `response`를 추가로 수정할 수 있습니다. -{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[8:9,11,14] *} /// tip | 팁 -사용자 정의 헤더는 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">'X-' 접두사를 사용</a>하여 추가할 수 있습니다. +사용자 정의 독점 헤더는 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">`X-` 접두사를 사용</a>하여 추가할 수 있다는 점을 기억하세요. -그러나 만약 클라이언트의 브라우저에서 볼 수 있는 사용자 정의 헤더를 가지고 있다면, 그것들을 CORS 설정([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})에 <a href="https://www.starlette.dev/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette CORS 문서</a>에 명시된 `expose_headers` 매개변수를 이용하여 헤더들을 추가하여야합니다. +하지만 브라우저에서 클라이언트가 볼 수 있게 하려는 사용자 정의 헤더가 있다면, <a href="https://www.starlette.dev/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette의 CORS 문서</a>에 문서화된 `expose_headers` 매개변수를 사용해 CORS 설정([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})에 추가해야 합니다. /// -/// note | 기술적 세부사항 +/// note | 기술 세부사항 -`from starlette.requests import request`를 사용할 수도 있습니다. +`from starlette.requests import Request`를 사용할 수도 있습니다. -**FastAPI**는 개발자에게 편의를 위해 이를 제공합니다. 그러나 Starlette에서 직접 파생되었습니다. +**FastAPI**는 개발자인 여러분의 편의를 위해 이를 제공합니다. 하지만 이는 Starlette에서 직접 가져온 것입니다. /// -### `response`의 전과 후 +### `response`의 전과 후 { #before-and-after-the-response } -*경로 작동*을 받기 전 `request`와 함께 작동할 수 있는 코드를 추가할 수 있습니다. +어떤 *경로 처리*가 받기 전에, `request`와 함께 실행될 코드를 추가할 수 있습니다. -그리고 `response` 또한 생성된 후 반환되기 전에 코드를 추가 할 수 있습니다. +또한 `response`가 생성된 후, 반환하기 전에 코드를 추가할 수도 있습니다. -예를 들어, 요청을 수행하고 응답을 생성하는데 까지 걸린 시간 값을 가지고 있는 `X-Process-Time` 같은 사용자 정의 헤더를 추가할 수 있습니다. +예를 들어, 요청을 처리하고 응답을 생성하는 데 걸린 시간을 초 단위로 담는 사용자 정의 헤더 `X-Process-Time`을 추가할 수 있습니다: -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} -## 다른 미들웨어 +/// tip | 팁 -미들웨어에 대한 더 많은 정보는 [숙련된 사용자 안내서: 향상된 미들웨어](../advanced/middleware.md){.internal-link target=\_blank}에서 확인할 수 있습니다. +여기서는 이러한 사용 사례에서 더 정확할 수 있기 때문에 `time.time()` 대신 <a href="https://docs.python.org/3/library/time.html#time.perf_counter" class="external-link" target="_blank">`time.perf_counter()`</a>를 사용합니다. 🤓 -다음 부분에서 미들웨어와 함께 <abbr title="교차-출처 리소스 공유">CORS</abbr>를 어떻게 다루는지에 대해 확인할 것입니다. +/// + +## 여러 미들웨어 실행 순서 { #multiple-middleware-execution-order } + +`@app.middleware()` 데코레이터 또는 `app.add_middleware()` 메서드를 사용해 여러 미들웨어를 추가하면, 새로 추가된 각 미들웨어가 애플리케이션을 감싸 스택을 형성합니다. 마지막에 추가된 미들웨어가 *가장 바깥쪽*이고, 처음에 추가된 미들웨어가 *가장 안쪽*입니다. + +요청 경로에서는 *가장 바깥쪽* 미들웨어가 먼저 실행됩니다. + +응답 경로에서는 마지막에 실행됩니다. + +예를 들어: + +```Python +app.add_middleware(MiddlewareA) +app.add_middleware(MiddlewareB) +``` + +이 경우 실행 순서는 다음과 같습니다: + +* **요청**: MiddlewareB → MiddlewareA → route + +* **응답**: route → MiddlewareA → MiddlewareB + +이러한 스태킹 동작은 미들웨어가 예측 가능하고 제어 가능한 순서로 실행되도록 보장합니다. + +## 다른 미들웨어 { #other-middlewares } + +다른 미들웨어에 대한 더 많은 정보는 나중에 [숙련된 사용자 안내서: 향상된 미들웨어](../advanced/middleware.md){.internal-link target=_blank}에서 확인할 수 있습니다. + +다음 섹션에서 미들웨어로 <abbr title="Cross-Origin Resource Sharing">CORS</abbr>를 처리하는 방법을 보게 될 것입니다. diff --git a/docs/ko/docs/tutorial/path-operation-configuration.md b/docs/ko/docs/tutorial/path-operation-configuration.md index 81914182a..b8a87667a 100644 --- a/docs/ko/docs/tutorial/path-operation-configuration.md +++ b/docs/ko/docs/tutorial/path-operation-configuration.md @@ -1,97 +1,107 @@ -# 경로 작동 설정 +# 경로 처리 설정 { #path-operation-configuration } -*경로 작동 데코레이터*를 설정하기 위해서 전달할수 있는 몇 가지 매개변수가 있습니다. +*경로 처리 데코레이터*를 설정하기 위해 전달할 수 있는 몇 가지 매개변수가 있습니다. /// warning | 경고 -아래 매개변수들은 *경로 작동 함수*가 아닌 *경로 작동 데코레이터*에 직접 전달된다는 사실을 기억하십시오. +아래 매개변수들은 *경로 처리 함수*가 아닌 *경로 처리 데코레이터*에 직접 전달된다는 사실을 기억하세요. /// -## 응답 상태 코드 +## 응답 상태 코드 { #response-status-code } -*경로 작동*의 응답에 사용될 (HTTP) `status_code`를 정의할수 있습니다. +*경로 처리*의 응답에 사용될 (HTTP) `status_code`를 정의할 수 있습니다. -`404`와 같은 `int`형 코드를 직접 전달할수 있습니다. +`404`와 같은 `int`형 코드를 직접 전달할 수 있습니다. -하지만 각 코드의 의미를 모른다면, `status`에 있는 단축 상수들을 사용할수 있습니다: +하지만 각 숫자 코드가 무엇을 의미하는지 기억하지 못한다면, `status`에 있는 단축 상수들을 사용할 수 있습니다: -{* ../../docs_src/path_operation_configuration/tutorial001.py hl[3,17] *} +{* ../../docs_src/path_operation_configuration/tutorial001_py310.py hl[1,15] *} -각 상태 코드들은 응답에 사용되며, OpenAPI 스키마에 추가됩니다. +해당 상태 코드는 응답에 사용되며, OpenAPI 스키마에 추가됩니다. -/// note | 기술적 세부사항 +/// note | 기술 세부사항 다음과 같이 임포트하셔도 좋습니다. `from starlette import status`. -**FastAPI**는 개발자 여러분의 편의를 위해서 `starlette.status`와 동일한 `fastapi.status`를 제공합니다. 하지만 Starlette에서 직접 온 것입니다. +**FastAPI**는 개발자 여러분의 편의를 위해 `starlette.status`와 동일한 `fastapi.status`를 제공합니다. 하지만 이는 Starlette에서 직접 온 것입니다. /// -## 태그 +## 태그 { #tags } -(보통 단일 `str`인) `str`로 구성된 `list`와 함께 매개변수 `tags`를 전달하여, `경로 작동`에 태그를 추가할 수 있습니다: +(보통 단일 `str`인) `str`로 구성된 `list`와 함께 매개변수 `tags`를 전달하여, *경로 처리*에 태그를 추가할 수 있습니다: -{* ../../docs_src/path_operation_configuration/tutorial002.py hl[17,22,27] *} +{* ../../docs_src/path_operation_configuration/tutorial002_py310.py hl[15,20,25] *} 전달된 태그들은 OpenAPI의 스키마에 추가되며, 자동 문서 인터페이스에서 사용됩니다: <img src="/img/tutorial/path-operation-configuration/image01.png"> -## 요약과 기술 +### Enum을 사용한 태그 { #tags-with-enums } + +큰 애플리케이션이 있다면, **여러 태그**가 쌓이게 될 수 있고, 관련된 *경로 처리*에 항상 **같은 태그**를 사용하는지 확인하고 싶을 것입니다. + +이런 경우에는 태그를 `Enum`에 저장하는 것이 합리적일 수 있습니다. + +**FastAPI**는 일반 문자열과 동일한 방식으로 이를 지원합니다: + +{* ../../docs_src/path_operation_configuration/tutorial002b_py39.py hl[1,8:10,13,18] *} + +## 요약과 설명 { #summary-and-description } `summary`와 `description`을 추가할 수 있습니다: -{* ../../docs_src/path_operation_configuration/tutorial003.py hl[20:21] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} -## 독스트링으로 만든 기술 +## 독스트링으로 만든 설명 { #description-from-docstring } -설명은 보통 길어지고 여러 줄에 걸쳐있기 때문에, *경로 작동* 기술을 함수 <abbr title="함수안에 있는 첫번째 표현식으로, 문서로 사용될 여러 줄에 걸친 (변수에 할당되지 않은) 문자열"> 독스트링</abbr> 에 선언할 수 있습니다, 이를 **FastAPI**가 독스트링으로부터 읽습니다. +설명은 보통 길어지고 여러 줄에 걸쳐있기 때문에, *경로 처리* 설명을 함수 <abbr title="a multi-line string as the first expression inside a function (not assigned to any variable) used for documentation – 문서화에 사용되는 함수 내부 첫 표현식의 여러 줄 문자열(어떤 변수에도 할당되지 않음)">docstring</abbr>에 선언할 수 있으며, **FastAPI**는 그곳에서 이를 읽습니다. -<a href="https://ko.wikipedia.org/wiki/%EB%A7%88%ED%81%AC%EB%8B%A4%EC%9A%B4" class="external-link" target="_blank">마크다운</a> 문법으로 독스트링을 작성할 수 있습니다, 작성된 마크다운 형식의 독스트링은 (마크다운의 들여쓰기를 고려하여) 올바르게 화면에 출력됩니다. +독스트링에는 <a href="https://en.wikipedia.org/wiki/Markdown" class="external-link" target="_blank">Markdown</a>을 작성할 수 있으며, (독스트링의 들여쓰기를 고려하여) 올바르게 해석되고 표시됩니다. -{* ../../docs_src/path_operation_configuration/tutorial004.py hl[19:27] *} +{* ../../docs_src/path_operation_configuration/tutorial004_py310.py hl[17:25] *} 이는 대화형 문서에서 사용됩니다: <img src="/img/tutorial/path-operation-configuration/image02.png"> -## 응답 기술 +## 응답 설명 { #response-description } `response_description` 매개변수로 응답에 관한 설명을 명시할 수 있습니다: -{* ../../docs_src/path_operation_configuration/tutorial005.py hl[21] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} /// info | 정보 -`response_description`은 구체적으로 응답을 지칭하며, `description`은 일반적인 *경로 작동*을 지칭합니다. +`response_description`은 구체적으로 응답을 지칭하며, `description`은 일반적인 *경로 처리*를 지칭합니다. /// /// check | 확인 -OpenAPI는 각 *경로 작동*이 응답에 관한 설명을 요구할 것을 명시합니다. +OpenAPI는 각 *경로 처리*가 응답에 관한 설명을 요구할 것을 명시합니다. -따라서, 응답에 관한 설명이 없을경우, **FastAPI**가 자동으로 "성공 응답" 중 하나를 생성합니다. +따라서, 응답에 관한 설명을 제공하지 않으면, **FastAPI**가 "Successful response" 중 하나를 자동으로 생성합니다. /// <img src="/img/tutorial/path-operation-configuration/image03.png"> -## 단일 *경로 작동* 지원중단 +## *경로 처리* 지원중단하기 { #deprecate-a-path-operation } -단일 *경로 작동*을 없애지 않고 <abbr title="구식, 사용하지 않는것이 권장됨">지원중단</abbr>을 해야한다면, `deprecated` 매개변수를 전달하면 됩니다. +*경로 처리*를 제거하지 않고 <abbr title="obsolete, recommended not to use it – 구식이며 사용하지 않는 것이 권장됨">deprecated</abbr>로 표시해야 한다면, `deprecated` 매개변수를 전달하면 됩니다: -{* ../../docs_src/path_operation_configuration/tutorial006.py hl[16] *} +{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} -대화형 문서에 지원중단이라고 표시됩니다. +대화형 문서에서 지원중단으로 명확하게 표시됩니다: <img src="/img/tutorial/path-operation-configuration/image04.png"> -지원중단된 경우와 지원중단 되지 않은 경우에 대한 *경로 작동*이 어떻게 보이는 지 확인하십시오. +지원중단된 *경로 처리*와 지원중단되지 않은 *경로 처리*가 어떻게 보이는지 확인해 보세요: <img src="/img/tutorial/path-operation-configuration/image05.png"> -## 정리 +## 정리 { #recap } -*경로 작동 데코레이터*에 매개변수(들)를 전달함으로 *경로 작동*을 설정하고 메타데이터를 추가할수 있습니다. +*경로 처리 데코레이터*에 매개변수(들)를 전달하여 *경로 처리*를 설정하고 메타데이터를 쉽게 추가할 수 있습니다. diff --git a/docs/ko/docs/tutorial/path-params-numeric-validations.md b/docs/ko/docs/tutorial/path-params-numeric-validations.md index f21c9290e..f2c52d4aa 100644 --- a/docs/ko/docs/tutorial/path-params-numeric-validations.md +++ b/docs/ko/docs/tutorial/path-params-numeric-validations.md @@ -1,116 +1,153 @@ -# 경로 매개변수와 숫자 검증 +# 경로 매개변수와 숫자 검증 { #path-parameters-and-numeric-validations } `Query`를 사용하여 쿼리 매개변수에 더 많은 검증과 메타데이터를 선언하는 방법과 동일하게 `Path`를 사용하여 경로 매개변수에 검증과 메타데이터를 같은 타입으로 선언할 수 있습니다. -## 경로 임포트 +## `Path` 임포트 { #import-path } -먼저 `fastapi`에서 `Path`를 임포트합니다: +먼저 `fastapi`에서 `Path`를 임포트하고, `Annotated`도 임포트합니다: -{* ../../docs_src/path_params_numeric_validations/tutorial001.py hl[3] *} +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *} -## 메타데이터 선언 +/// info | 정보 -`Query`에 동일한 매개변수를 선언할 수 있습니다. +FastAPI는 0.95.0 버전에서 `Annotated` 지원을 추가했고(그리고 이를 권장하기 시작했습니다). -예를 들어, `title` 메타데이터 값을 경로 매개변수 `item_id`에 선언하려면 다음과 같이 입력할 수 있습니다: +더 오래된 버전이 있다면 `Annotated`를 사용하려고 할 때 오류가 발생합니다. -{* ../../docs_src/path_params_numeric_validations/tutorial001.py hl[10] *} - -/// note | 참고 - -경로 매개변수는 경로의 일부여야 하므로 언제나 필수적입니다. - -즉, `...`로 선언해서 필수임을 나타내는게 좋습니다. - -그럼에도 `None`으로 선언하거나 기본값을 지정할지라도 아무 영향을 끼치지 않으며 언제나 필수입니다. +`Annotated`를 사용하기 전에 최소 0.95.1까지 [FastAPI 버전 업그레이드](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}를 꼭 하세요. /// -## 필요한 경우 매개변수 정렬하기 +## 메타데이터 선언 { #declare-metadata } + +`Query`에 동일한 매개변수를 선언할 수 있습니다. + +예를 들어, 경로 매개변수 `item_id`에 `title` 메타데이터 값을 선언하려면 다음과 같이 입력할 수 있습니다: + +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} + +/// note | 참고 + +경로 매개변수는 경로의 일부여야 하므로 언제나 필수입니다. `None`으로 선언하거나 기본값을 지정하더라도 아무 영향이 없으며, 항상 필수입니다. + +/// + +## 필요한 대로 매개변수 정렬하기 { #order-the-parameters-as-you-need } + +/// tip | 팁 + +`Annotated`를 사용한다면 이것은 아마 그렇게 중요하지 않거나 필요하지 않을 수 있습니다. + +/// `str` 형인 쿼리 매개변수 `q`를 필수로 선언하고 싶다고 해봅시다. -해당 매개변수에 대해 아무런 선언을 할 필요가 없으므로 `Query`를 정말로 써야할 필요는 없습니다. +해당 매개변수에 대해 아무런 선언을 할 필요가 없으므로 `Query`를 정말로 써야 할 필요는 없습니다. -하지만 `item_id` 경로 매개변수는 여전히 `Path`를 사용해야 합니다. +하지만 `item_id` 경로 매개변수는 여전히 `Path`를 사용해야 합니다. 그리고 어떤 이유로 `Annotated`를 사용하고 싶지 않다고 해봅시다. -파이썬은 "기본값"이 없는 값 앞에 "기본값"이 있는 값을 입력하면 불평합니다. +파이썬은 "기본값"이 있는 값을 "기본값"이 없는 값 앞에 두면 불평합니다. -그러나 매개변수들을 재정렬함으로써 기본값(쿼리 매개변수 `q`)이 없는 값을 처음 부분에 위치 할 수 있습니다. +하지만 순서를 재정렬해서 기본값이 없는 값(쿼리 매개변수 `q`)을 앞에 둘 수 있습니다. -**FastAPI**에서는 중요하지 않습니다. 이름, 타입 그리고 선언구(`Query`, `Path` 등)로 매개변수를 감지하며 순서는 신경 쓰지 않습니다. +**FastAPI**에서는 중요하지 않습니다. 이름, 타입 그리고 기본값 선언(`Query`, `Path` 등)로 매개변수를 감지하며 순서는 신경 쓰지 않습니다. -따라서 함수를 다음과 같이 선언 할 수 있습니다: +따라서 함수를 다음과 같이 선언할 수 있습니다: -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} -## 필요한 경우 매개변수 정렬하기, 트릭 +하지만 `Annotated`를 사용하면 이 문제가 없다는 점을 기억하세요. `Query()`나 `Path()`에 함수 매개변수 기본값을 사용하지 않기 때문에, 순서는 중요하지 않습니다. -`Query`나 아무런 기본값으로도 `q` 경로 매개변수를 선언하고 싶지 않지만 `Path`를 사용하여 경로 매개변수를 `item_id` 다른 순서로 선언하고 싶다면, 파이썬은 이를 위한 작고 특별한 문법이 있습니다. +{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} -`*`를 함수의 첫 번째 매개변수로 전달하세요. +## 필요한 대로 매개변수 정렬하기, 트릭 { #order-the-parameters-as-you-need-tricks } -파이썬은 `*`으로 아무런 행동도 하지 않지만, 따르는 매개변수들은 <abbr title="유래: K-ey W-ord Arg-uments"><code>kwargs</code></abbr>로도 알려진 키워드 인자(키-값 쌍)여야 함을 인지합니다. 기본값을 가지고 있지 않더라도 그렇습니다. +/// tip | 팁 -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} +`Annotated`를 사용한다면 이것은 아마 그렇게 중요하지 않거나 필요하지 않을 수 있습니다. -## 숫자 검증: 크거나 같음 +/// -`Query`와 `Path`(나중에 볼 다른 것들도)를 사용하여 문자열 뿐만 아니라 숫자의 제약을 선언할 수 있습니다. +유용할 수 있는 **작은 트릭**이 하나 있지만, 자주 필요하진 않을 겁니다. -여기서 `ge=1`인 경우, `item_id`는 `1`보다 "크거나(`g`reater) 같은(`e`qual)" 정수형 숫자여야 합니다. +만약 다음을 원한다면: -{* ../../docs_src/path_params_numeric_validations/tutorial004.py hl[8] *} +* `Query`나 어떤 기본값 없이 쿼리 매개변수 `q`를 선언하기 +* `Path`를 사용해서 경로 매개변수 `item_id`를 선언하기 +* 이들을 다른 순서로 두기 +* `Annotated`를 사용하지 않기 -## 숫자 검증: 크거나 같음 및 작거나 같음 +...이를 위해 파이썬에는 작은 특별한 문법이 있습니다. + +함수의 첫 번째 매개변수로 `*`를 전달하세요. + +파이썬은 `*`으로 아무것도 하지 않지만, 뒤따르는 모든 매개변수는 키워드 인자(키-값 쌍)로 호출되어야 함을 알게 됩니다. 이는 <abbr title="From: K-ey W-ord Arg-uments"><code>kwargs</code></abbr>로도 알려져 있습니다. 기본값이 없더라도 마찬가지입니다. + +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} + +### `Annotated`를 쓰면 더 좋습니다 { #better-with-annotated } + +`Annotated`를 사용하면 함수 매개변수 기본값을 사용하지 않기 때문에 이 문제가 발생하지 않으며, 아마 `*`도 사용할 필요가 없다는 점을 기억하세요. + +{* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} + +## 숫자 검증: 크거나 같음 { #number-validations-greater-than-or-equal } + +`Query`와 `Path`(그리고 나중에 볼 다른 것들)를 사용하여 숫자 제약을 선언할 수 있습니다. + +여기서 `ge=1`인 경우, `item_id`는 `1`보다 "`g`reater than or `e`qual"(크거나 같은) 정수형 숫자여야 합니다. + +{* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} + +## 숫자 검증: 크거나 및 작거나 같음 { #number-validations-greater-than-and-less-than-or-equal } 동일하게 적용됩니다: -* `gt`: 크거나(`g`reater `t`han) -* `le`: 작거나 같은(`l`ess than or `e`qual) +* `gt`: `g`reater `t`han +* `le`: `l`ess than or `e`qual -{* ../../docs_src/path_params_numeric_validations/tutorial005.py hl[9] *} +{* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *} -## 숫자 검증: 부동소수, 크거나 및 작거나 +## 숫자 검증: 부동소수, 크거나 및 작거나 { #number-validations-floats-greater-than-and-less-than } 숫자 검증은 `float` 값에도 동작합니다. -여기에서 <abbr title="greater than or equal"><code>ge</code></abbr>뿐만 아니라 <abbr title="greater than"><code>gt</code></abbr>를 선언 할 수있는 것이 중요해집니다. 예를 들어 필요한 경우, 값이 `1`보다 작더라도 반드시 `0`보다 커야합니다. +여기에서 <abbr title="greater than"><code>gt</code></abbr>를, <abbr title="greater than or equal"><code>ge</code></abbr>뿐만 아니라 선언할 수 있다는 점이 중요해집니다. 예를 들어 값이 `1`보다 작더라도, 반드시 `0`보다 커야 한다고 요구할 수 있습니다. 즉, `0.5`는 유효한 값입니다. 그러나 `0.0` 또는 `0`은 그렇지 않습니다. <abbr title="less than"><code>lt</code></abbr> 역시 마찬가지입니다. -{* ../../docs_src/path_params_numeric_validations/tutorial006.py hl[11] *} +{* ../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py hl[13] *} -## 요약 +## 요약 { #recap } `Query`, `Path`(아직 보지 못한 다른 것들도)를 사용하면 [쿼리 매개변수와 문자열 검증](query-params-str-validations.md){.internal-link target=_blank}에서와 마찬가지로 메타데이터와 문자열 검증을 선언할 수 있습니다. 그리고 숫자 검증 또한 선언할 수 있습니다: -* `gt`: 크거나(`g`reater `t`han) -* `ge`: 크거나 같은(`g`reater than or `e`qual) -* `lt`: 작거나(`l`ess `t`han) -* `le`: 작거나 같은(`l`ess than or `e`qual) +* `gt`: `g`reater `t`han +* `ge`: `g`reater than or `e`qual +* `lt`: `l`ess `t`han +* `le`: `l`ess than or `e`qual /// info | 정보 -`Query`, `Path`, 그리고 나중에게 보게될 것들은 (여러분이 사용할 필요가 없는) 공통 `Param` 클래스의 서브 클래스입니다. +`Query`, `Path`, 그리고 나중에 보게 될 다른 클래스들은 공통 `Param` 클래스의 서브클래스입니다. -그리고 이들 모두는 여태까지 본 추가 검증과 메타데이터의 동일한 모든 매개변수를 공유합니다. +이들 모두는 여러분이 본 추가 검증과 메타데이터에 대한 동일한 매개변수를 공유합니다. /// /// note | 기술 세부사항 -`fastapi`에서 `Query`, `Path` 등을 임포트 할 때, 이것들은 실제로 함수입니다. +`fastapi`에서 `Query`, `Path` 등을 임포트할 때, 이것들은 실제로 함수입니다. 호출되면 동일한 이름의 클래스의 인스턴스를 반환합니다. 즉, 함수인 `Query`를 임포트한 겁니다. 그리고 호출하면 `Query`라는 이름을 가진 클래스의 인스턴스를 반환합니다. -편집기에서 타입에 대한 오류를 표시하지 않도록 하기 위해 (클래스를 직접 사용하는 대신) 이러한 함수들이 있습니다. +이 함수들이 있는 이유는(클래스를 직접 사용하는 대신) 편집기에서 타입에 대한 오류를 표시하지 않도록 하기 위해서입니다. 이렇게 하면 오류를 무시하기 위한 사용자 설정을 추가하지 않고도 일반 편집기와 코딩 도구를 사용할 수 있습니다. diff --git a/docs/ko/docs/tutorial/path-params.md b/docs/ko/docs/tutorial/path-params.md index b72787e0b..ea5170ecc 100644 --- a/docs/ko/docs/tutorial/path-params.md +++ b/docs/ko/docs/tutorial/path-params.md @@ -1,8 +1,8 @@ -# 경로 매개변수 +# 경로 매개변수 { #path-parameters } 파이썬의 포맷 문자열 리터럴에서 사용되는 문법을 이용하여 경로 "매개변수" 또는 "변수"를 선언할 수 있습니다: -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} 경로 매개변수 `item_id`의 값은 함수의 `item_id` 인자로 전달됩니다. @@ -12,11 +12,11 @@ {"item_id":"foo"} ``` -## 타입이 있는 매개변수 +## 타입이 있는 경로 매개변수 { #path-parameters-with-types } 파이썬 표준 타입 어노테이션을 사용하여 함수에 있는 경로 매개변수의 타입을 선언할 수 있습니다: -{* ../../docs_src/path_params/tutorial002.py hl[7] *} +{* ../../docs_src/path_params/tutorial002_py39.py hl[7] *} 위의 예시에서, `item_id`는 `int`로 선언되었습니다. @@ -26,7 +26,7 @@ /// -## 데이터 <abbr title="다음으로도 알려져 있습니다: 직렬화, 파싱, 마샬링">변환</abbr> +## 데이터 <abbr title="다음으로도 알려져 있습니다: 직렬화, 파싱, 마샬링">변환</abbr> { #data-conversion } 이 예제를 실행하고 <a href="http://127.0.0.1:8000/items/3" class="external-link" target="_blank">http://127.0.0.1:8000/items/3</a>을 열면, 다음 응답을 볼 수 있습니다: @@ -42,40 +42,41 @@ /// -## 데이터 검증 +## 데이터 검증 { #data-validation } -하지만 브라우저에서 <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>로 이동하면, HTTP 오류가 잘 뜨는 것을 확인할 수 있습니다: +하지만 브라우저에서 <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>로 이동하면, 다음과 같은 HTTP 오류를 볼 수 있습니다: ```JSON { - "detail": [ - { - "loc": [ - "path", - "item_id" - ], - "msg": "value is not a valid integer", - "type": "type_error.integer" - } - ] + "detail": [ + { + "type": "int_parsing", + "loc": [ + "path", + "item_id" + ], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo" + } + ] } ``` -경로 매개변수 `item_id`는 `int`가 아닌 `"foo"` 값이기 때문입니다. +경로 매개변수 `item_id`가 `int`가 아닌 `"foo"` 값을 가졌기 때문입니다. -`int`가 아닌 `float`을 전달하는 경우에도 동일한 오류가 나타납니다: <a href="http://127.0.0.1:8000/items/4.2" class="external-link" target="_blank">http://127.0.0.1:8000/items/4.2</a> +`int` 대신 `float`을 제공하면(예: <a href="http://127.0.0.1:8000/items/4.2" class="external-link" target="_blank">http://127.0.0.1:8000/items/4.2</a>) 동일한 오류가 나타납니다. /// check | 확인 즉, 파이썬 타입 선언을 하면 **FastAPI**는 데이터 검증을 합니다. -오류에는 정확히 어느 지점에서 검증을 통과하지 못했는지 명시됩니다. +또한 오류에는 검증을 통과하지 못한 지점이 정확히 명시됩니다. 이는 API와 상호 작용하는 코드를 개발하고 디버깅하는 데 매우 유용합니다. /// -## 문서화 +## 문서화 { #documentation } 그리고 브라우저에서 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>를 열면, 다음과 같이 자동 대화식 API 문서를 볼 수 있습니다: @@ -83,23 +84,23 @@ /// check | 확인 -그저 파이썬 타입 선언을 하기만 하면 **FastAPI**는 자동 대화형 API 문서(Swagger UI)를 제공합니다. +다시 한 번, 동일한 파이썬 타입 선언만으로 **FastAPI**는 자동 대화형 문서(Swagger UI 통합)를 제공합니다. -경로 매개변수가 정수형으로 명시된 것을 확인할 수 있습니다. +경로 매개변수가 정수형으로 선언된 것을 확인할 수 있습니다. /// -## 표준 기반의 이점, 대체 문서 +## 표준 기반의 이점, 대체 문서 { #standards-based-benefits-alternative-documentation } -그리고 생성된 스키마는 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md" class="external-link" target="_blank">OpenAPI</a> 표준에서 나온 것이기 때문에 호환되는 도구가 많이 있습니다. +그리고 생성된 스키마는 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md" class="external-link" target="_blank">OpenAPI</a> 표준에서 나온 것이기 때문에 호환되는 도구가 많이 있습니다. -이 덕분에 **FastAPI**는 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>로 접속할 수 있는 (ReDoc을 사용하는) 대체 API 문서를 제공합니다: +이 덕분에 **FastAPI** 자체에서 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>로 접속할 수 있는 (ReDoc을 사용하는) 대체 API 문서를 제공합니다: <img src="/img/tutorial/path-params/image02.png"> 이와 마찬가지로 다양한 언어에 대한 코드 생성 도구를 포함하여 여러 호환되는 도구가 있습니다. -## Pydantic +## Pydantic { #pydantic } 모든 데이터 검증은 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a>에 의해 내부적으로 수행되므로 이로 인한 이점을 모두 얻을 수 있습니다. 여러분은 관리를 잘 받고 있음을 느낄 수 있습니다. @@ -107,25 +108,31 @@ 이 중 몇 가지는 자습서의 다음 장에 설명되어 있습니다. -## 순서 문제 +## 순서 문제 { #order-matters } -*경로 작동*을 만들때 고정 경로를 갖고 있는 상황들을 맞닥뜨릴 수 있습니다. +*경로 처리*를 만들 때 고정 경로를 갖고 있는 상황들을 맞닥뜨릴 수 있습니다. `/users/me`처럼, 현재 사용자의 데이터를 가져온다고 합시다. 사용자 ID를 이용해 특정 사용자의 정보를 가져오는 경로 `/users/{user_id}`도 있습니다. -*경로 작동*은 순차적으로 실행되기 때문에 `/users/{user_id}` 이전에 `/users/me`를 먼저 선언해야 합니다: +*경로 처리*는 순차적으로 평가되기 때문에 `/users/{user_id}` 이전에 `/users/me`에 대한 경로가 먼저 선언되었는지 확인해야 합니다: -{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} +{* ../../docs_src/path_params/tutorial003_py39.py hl[6,11] *} -그렇지 않으면 `/users/{user_id}`는 `/users/me` 요청 또한 매개변수 `user_id`의 값이 `"me"`인 것으로 "생각하게" 됩니다. +그렇지 않으면 `/users/{user_id}`에 대한 경로가 `/users/me`에도 매칭되어, 매개변수 `user_id`에 `"me"` 값이 들어왔다고 "생각하게" 됩니다. -## 사전정의 값 +마찬가지로, 경로 처리를 재정의할 수는 없습니다: -만약 *경로 매개변수*를 받는 *경로 작동*이 있지만, *경로 매개변수*로 가능한 값들을 미리 정의하고 싶다면 파이썬 표준 <abbr title="열거형(Enumeration)">`Enum`</abbr>을 사용할 수 있습니다. +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} -### `Enum` 클래스 생성 +경로가 먼저 매칭되기 때문에 첫 번째 것이 항상 사용됩니다. + +## 사전정의 값 { #predefined-values } + +만약 *경로 매개변수*를 받는 *경로 처리*가 있지만, 가능한 유효한 *경로 매개변수* 값들을 미리 정의하고 싶다면 파이썬 표준 <abbr title="열거형(Enumeration)">`Enum`</abbr>을 사용할 수 있습니다. + +### `Enum` 클래스 생성 { #create-an-enum-class } `Enum`을 임포트하고 `str`과 `Enum`을 상속하는 서브 클래스를 만듭니다. @@ -133,47 +140,41 @@ 가능한 값들에 해당하는 고정된 값의 클래스 어트리뷰트들을 만듭니다: -{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} - -/// info | 정보 - -<a href="https://docs.python.org/3/library/enum.html" class="external-link" target="_blank">열거형(또는 enums)</a>은 파이썬 버전 3.4 이후로 사용 가능합니다. - -/// +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | 팁 -혹시 궁금하다면, "AlexNet", "ResNet", 그리고 "LeNet"은 그저 기계 학습 <abbr title="기술적으로 정확히는 딥 러닝 모델 구조">모델</abbr>들의 이름입니다. +혹시 궁금하다면, "AlexNet", "ResNet", 그리고 "LeNet"은 그저 머신 러닝 <abbr title="기술적으로는 딥 러닝 모델 아키텍처">모델</abbr>들의 이름입니다. /// -### *경로 매개변수* 선언 +### *경로 매개변수* 선언 { #declare-a-path-parameter } 생성한 열거형 클래스(`ModelName`)를 사용하는 타입 어노테이션으로 *경로 매개변수*를 만듭니다: -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} -### 문서 확인 +### 문서 확인 { #check-the-docs } *경로 매개변수*에 사용할 수 있는 값은 미리 정의되어 있으므로 대화형 문서에서 잘 표시됩니다: <img src="/img/tutorial/path-params/image03.png"> -### 파이썬 *열거형*으로 작업하기 +### 파이썬 *열거형*으로 작업하기 { #working-with-python-enumerations } *경로 매개변수*의 값은 *열거형 멤버*가 됩니다. -#### *열거형 멤버* 비교 +#### *열거형 멤버* 비교 { #compare-enumeration-members } -열거형 `ModelName`의 *열거형 멤버*를 비교할 수 있습니다: +생성한 열거형 `ModelName`의 *열거형 멤버*와 비교할 수 있습니다: -{* ../../docs_src/path_params/tutorial005.py hl[17] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *} -#### *열거형 값* 가져오기 +#### *열거형 값* 가져오기 { #get-the-enumeration-value } `model_name.value` 또는 일반적으로 `your_enum_member.value`를 이용하여 실제 값(위 예시의 경우 `str`)을 가져올 수 있습니다: -{* ../../docs_src/path_params/tutorial005.py hl[20] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *} /// tip | 팁 @@ -181,15 +182,15 @@ /// -#### *열거형 멤버* 반환 +#### *열거형 멤버* 반환 { #return-enumeration-members } -*경로 작동*에서 *열거형 멤버*를 반환할 수 있습니다. 이는 중첩 JSON 본문(예: `dict`)내의 값으로도 가능합니다. +*경로 처리*에서 *enum 멤버*를 반환할 수 있습니다. 이는 JSON 본문(예: `dict`) 내에 중첩된 형태로도 가능합니다. 클라이언트에 반환하기 전에 해당 값(이 경우 문자열)으로 변환됩니다: -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} -클라이언트는 아래의 JSON 응답을 얻습니다: +클라이언트는 아래와 같은 JSON 응답을 얻게 됩니다: ```JSON { @@ -198,53 +199,53 @@ } ``` -## 경로를 포함하는 경로 매개변수 +## 경로를 포함하는 경로 매개변수 { #path-parameters-containing-paths } -경로를 포함하는 *경로 작동* `/files/{file_path}`이 있다고 해봅시다. +경로 `/files/{file_path}`를 가진 *경로 처리*가 있다고 해봅시다. -그런데 이 경우 `file_path` 자체가 `home/johndoe/myfile.txt`와 같은 경로를 포함해야 합니다. +하지만 `file_path` 자체가 `home/johndoe/myfile.txt`와 같은 *경로*를 포함해야 합니다. 이때 해당 파일의 URL은 다음처럼 됩니다: `/files/home/johndoe/myfile.txt`. -### OpenAPI 지원 +### OpenAPI 지원 { #openapi-support } 테스트와 정의가 어려운 시나리오로 이어질 수 있으므로 OpenAPI는 *경로*를 포함하는 *경로 매개변수*를 내부에 선언하는 방법을 지원하지 않습니다. -그럼에도 Starlette의 내부 도구중 하나를 사용하여 **FastAPI**에서는 이가 가능합니다. +그럼에도 Starlette의 내부 도구 중 하나를 사용하여 **FastAPI**에서는 이가 가능합니다. -문서에 매개변수에 경로가 포함되어야 한다는 정보가 명시되지는 않지만 여전히 작동합니다. +또한 문서가 여전히 동작하긴 하지만, 매개변수에 경로가 포함되어야 한다는 내용을 추가로 문서화하지는 않습니다. -### 경로 변환기 +### 경로 변환기 { #path-convertor } -Starlette의 옵션을 직접 이용하여 다음과 같은 URL을 사용함으로써 *path*를 포함하는 *경로 매개변수*를 선언할 수 있습니다: +Starlette의 옵션을 직접 이용하여 다음과 같은 URL을 사용함으로써 *경로*를 포함하는 *경로 매개변수*를 선언할 수 있습니다: ``` /files/{file_path:path} ``` -이러한 경우 매개변수의 이름은 `file_path`이며, 마지막 부분 `:path`는 매개변수가 *경로*와 일치해야 함을 명시합니다. +이러한 경우 매개변수의 이름은 `file_path`이며, 마지막 부분 `:path`는 매개변수가 어떤 *경로*와도 매칭되어야 함을 의미합니다. 따라서 다음과 같이 사용할 수 있습니다: -{* ../../docs_src/path_params/tutorial004.py hl[6] *} +{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *} /// tip | 팁 -매개변수가 가져야 하는 값이 `/home/johndoe/myfile.txt`와 같이 슬래시로 시작(`/`)해야 할 수 있습니다. +매개변수가 선행 슬래시(`/`)가 있는 `/home/johndoe/myfile.txt`를 포함해야 할 수도 있습니다. -이 경우 URL은: `/files//home/johndoe/myfile.txt`이며 `files`과 `home` 사이에 이중 슬래시(`//`)가 생깁니다. +그 경우 URL은: `/files//home/johndoe/myfile.txt`이며 `files`와 `home` 사이에 이중 슬래시(`//`)가 생깁니다. /// -## 요약 +## 요약 { #recap } **FastAPI**를 이용하면 짧고 직관적인 표준 파이썬 타입 선언을 사용하여 다음을 얻을 수 있습니다: * 편집기 지원: 오류 검사, 자동완성 등 -* 데이터 "<abbr title="HTTP 요청에서 전달되는 문자열을 파이썬 데이터로 변환">파싱</abbr>" +* 데이터 "<abbr title="HTTP 요청에서 전달되는 문자열을 파이썬 데이터로 변환">parsing</abbr>" * 데이터 검증 * API 주석(Annotation)과 자동 문서 -단 한번의 선언만으로 위 사항들을 모두 선언할 수 있습니다. +그리고 한 번만 선언하면 됩니다. 이는 대체 프레임워크와 비교했을 때 (엄청나게 빠른 성능 외에도) **FastAPI**의 주요한 장점일 것입니다. diff --git a/docs/ko/docs/tutorial/query-param-models.md b/docs/ko/docs/tutorial/query-param-models.md index 2ca65a331..d354c56c3 100644 --- a/docs/ko/docs/tutorial/query-param-models.md +++ b/docs/ko/docs/tutorial/query-param-models.md @@ -1,48 +1,48 @@ -# 쿼리 매개변수 모델 +# 쿼리 매개변수 모델 { #query-parameter-models } -연관된 쿼리 **매개변수** 그룹이 있다면 **Pydantic 모델** 을 사용해 선언할 수 있습니다. +연관된 **쿼리 매개변수** 그룹이 있다면 이를 선언하기 위해 **Pydantic 모델**을 생성할 수 있습니다. 이렇게 하면 **여러 곳**에서 **모델을 재사용**할 수 있을 뿐만 아니라, 매개변수에 대한 검증 및 메타데이터도 한 번에 선언할 수 있습니다. 😎 /// note | 참고 -이 기능은 FastAPI 버전 `0.115.0`부터 제공됩니다. 🤓 +이 기능은 FastAPI 버전 `0.115.0`부터 지원됩니다. 🤓 /// -## 쿼리 매개변수와 Pydantic 모델 +## Pydantic 모델과 쿼리 매개변수 { #query-parameters-with-a-pydantic-model } -필요한 **쿼리 매개변수**를 **Pydantic 모델** 안에 선언한 다음, 모델을 `Query`로 선언합니다. +필요한 **쿼리 매개변수**를 **Pydantic 모델** 안에 선언한 다음, 매개변수를 `Query`로 선언합니다: {* ../../docs_src/query_param_models/tutorial001_an_py310.py hl[9:13,17] *} -**FastAPI**는 요청의 **쿼리 매개변수**에서 **각 필드**의 데이터를 **추출**해 정의한 Pydantic 모델로 제공합니다. +**FastAPI**는 요청의 **쿼리 매개변수**에서 **각 필드**의 데이터를 **추출**해 정의한 Pydantic 모델을 제공합니다. -## 문서 확인하기 +## 문서 확인하기 { #check-the-docs } -`/docs` 경로의 API 문서에서 매개변수를 확인할 수 있습니다. +`/docs`의 문서 UI에서 쿼리 매개변수를 확인할 수 있습니다: <div class="screenshot"> <img src="/img/tutorial/query-param-models/image01.png"> </div> -## 추가 쿼리 매개변수 금지 +## 추가 쿼리 매개변수 금지 { #forbid-extra-query-parameters } -몇몇의 특이한 경우에 (흔치 않지만), 허용할 쿼리 매개변수를 **제한**해야할 수 있습니다. +몇몇의 특이한 경우에 (흔치 않지만), 받으려는 쿼리 매개변수를 **제한**하고 싶을 수 있습니다. -Pydantic 모델 설정에서 `extra` 필드를 `forbid` 로 설정할 수 있습니다. +Pydantic의 모델 설정을 사용해 어떤 `extra` 필드도 `forbid`할 수 있습니다: {* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} -만약 클라이언트가 쿼리 매개변수로 **추가적인** 데이터를 보내려고 하면, 클라이언트는 **에러** 응답을 받게 됩니다. +클라이언트가 **쿼리 매개변수**로 **추가적인** 데이터를 보내려고 하면 **에러** 응답을 받게 됩니다. -예를 들어, 아래와 같이 만약 클라이언트가 `tool` 쿼리 매개변수에 `plumbus` 라는 값을 추가해서 보내려고 하면, +예를 들어, 아래와 같이 클라이언트가 `tool` 쿼리 매개변수에 `plumbus` 값을 보내려고 하면: ```http https://example.com/items/?limit=10&tool=plumbus ``` -클라이언트는 쿼리 매개변수 `tool` 이 허용되지 않는다는 **에러** 응답을 받게 됩니다. +쿼리 매개변수 `tool`이 허용되지 않는다는 **에러** 응답을 받게 됩니다: ```json { @@ -57,12 +57,12 @@ https://example.com/items/?limit=10&tool=plumbus } ``` -## 요약 +## 요약 { #summary } -**FastAPI** 에서 **쿼리 매개변수** 를 선언할 때 **Pydantic 모델** 을 사용할 수 있습니다. 😎 +**FastAPI**에서 **쿼리 매개변수**를 선언할 때 **Pydantic 모델**을 사용할 수 있습니다. 😎 /// tip | 팁 -스포일러 경고: Pydantic 모델을 쿠키와 헤더에도 적용할 수 있습니다. 이에 대해서는 이후 튜토리얼에서 다룰 예정입니다. 🤫 +스포일러 경고: Pydantic 모델을 쿠키와 헤더에도 사용할 수 있지만, 이에 대해서는 이후 튜토리얼에서 읽게 될 것입니다. 🤫 /// diff --git a/docs/ko/docs/tutorial/query-params-str-validations.md b/docs/ko/docs/tutorial/query-params-str-validations.md index f2ca453ac..68824932e 100644 --- a/docs/ko/docs/tutorial/query-params-str-validations.md +++ b/docs/ko/docs/tutorial/query-params-str-validations.md @@ -1,118 +1,226 @@ -# 쿼리 매개변수와 문자열 검증 +# 쿼리 매개변수와 문자열 검증 { #query-parameters-and-string-validations } **FastAPI**를 사용하면 매개변수에 대한 추가 정보 및 검증을 선언할 수 있습니다. 이 응용 프로그램을 예로 들어보겠습니다: -{* ../../docs_src/query_params_str_validations/tutorial001.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} -쿼리 매개변수 `q`는 `Optional[str]` 자료형입니다. 즉, `str` 자료형이지만 `None` 역시 될 수 있음을 뜻하고, 실제로 기본값은 `None`이기 때문에 FastAPI는 이 매개변수가 필수가 아니라는 것을 압니다. +쿼리 매개변수 `q`는 `str | None` 자료형입니다. 즉, `str` 자료형이지만 `None` 역시 될 수 있음을 뜻하고, 실제로 기본값은 `None`이기 때문에 FastAPI는 이 매개변수가 필수가 아니라는 것을 압니다. /// note | 참고 FastAPI는 `q`의 기본값이 `= None`이기 때문에 필수가 아님을 압니다. -`Optional[str]`에 있는 `Optional`은 FastAPI가 사용하는게 아니지만, 편집기에게 더 나은 지원과 오류 탐지를 제공하게 해줍니다. +`str | None`을 사용하면 편집기가 더 나은 지원과 오류 탐지를 제공하게 해줍니다. /// -## 추가 검증 +## 추가 검증 { #additional-validation } -`q`가 선택적이지만 값이 주어질 때마다 **값이 50 글자를 초과하지 않게** 강제하려 합니다. +`q`가 선택적이지만 값이 주어질 때마다 **길이가 50자를 초과하지 않게** 강제하려 합니다. -### `Query` 임포트 +### `Query`와 `Annotated` 임포트 { #import-query-and-annotated } -이를 위해 먼저 `fastapi`에서 `Query`를 임포트합니다: +이를 위해 먼저 다음을 임포트합니다: -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[3] *} +* `fastapi`에서 `Query` +* `typing`에서 `Annotated` -## 기본값으로 `Query` 사용 +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} -이제 `Query`를 매개변수의 기본값으로 사용하여 `max_length` 매개변수를 50으로 설정합니다: +/// info | 정보 -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[9] *} +FastAPI는 0.95.0 버전에서 `Annotated` 지원을 추가했고(그리고 이를 권장하기 시작했습니다). -기본값 `None`을 `Query(None)`으로 바꿔야 하므로, `Query`의 첫 번째 매개변수는 기본값을 정의하는 것과 같은 목적으로 사용됩니다. +이전 버전을 사용하면 `Annotated`를 사용하려고 할 때 오류가 발생합니다. + +`Annotated`를 사용하기 전에 최소 0.95.1 버전으로 [FastAPI 버전 업그레이드](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}를 진행하세요. + +/// + +## `q` 매개변수의 타입에 `Annotated` 사용하기 { #use-annotated-in-the-type-for-the-q-parameter } + +이전에 [Python Types Intro](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}에서 `Annotated`를 사용해 매개변수에 메타데이터를 추가할 수 있다고 말씀드린 것을 기억하시나요? + +이제 FastAPI에서 사용할 차례입니다. 🚀 + +다음과 같은 타입 어노테이션이 있었습니다: + +//// tab | Python 3.10+ + +```Python +q: str | None = None +``` + +//// + +//// tab | Python 3.9+ + +```Python +q: Union[str, None] = None +``` + +//// + +여기서 `Annotated`로 감싸서 다음과 같이 만듭니다: + +//// tab | Python 3.10+ + +```Python +q: Annotated[str | None] = None +``` + +//// + +//// tab | Python 3.9+ + +```Python +q: Annotated[Union[str, None]] = None +``` + +//// + +두 버전 모두 같은 의미로, `q`는 `str` 또는 `None`이 될 수 있는 매개변수이며 기본값은 `None`입니다. + +이제 재미있는 부분으로 넘어가 봅시다. 🎉 + +## `q` 매개변수의 `Annotated`에 `Query` 추가하기 { #add-query-to-annotated-in-the-q-parameter } + +이제 이 `Annotated`에 더 많은 정보를 넣을 수 있으므로(이 경우에는 추가 검증), `Annotated` 안에 `Query`를 추가하고 `max_length` 매개변수를 `50`으로 설정합니다: + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} + +기본값은 여전히 `None`이므로, 매개변수는 여전히 선택적입니다. + +하지만 `Annotated` 안에 `Query(max_length=50)`를 넣음으로써, 이 값에 대해 **추가 검증**을 적용하고 최대 50자까지만 허용하도록 FastAPI에 알려줍니다. 😎 + +/// tip | 팁 + +여기서는 **쿼리 매개변수**이기 때문에 `Query()`를 사용합니다. 나중에 `Path()`, `Body()`, `Header()`, `Cookie()`와 같이 `Query()`와 동일한 인자를 받는 것들도 보게 될 것입니다. + +/// + +이제 FastAPI는 다음을 수행합니다: + +* 최대 길이가 50자인지 확인하도록 데이터를 **검증**합니다 +* 데이터가 유효하지 않을 때 클라이언트에게 **명확한 오류**를 보여줍니다 +* OpenAPI 스키마 *경로 처리*에 매개변수를 **문서화**합니다(따라서 **자동 문서 UI**에 표시됩니다) + +## 대안(이전 방식): 기본값으로 `Query` 사용 { #alternative-old-query-as-the-default-value } + +이전 FastAPI 버전(<abbr title="before 2023-03">0.95.0</abbr> 이전)에서는 `Annotated`에 넣는 대신, 매개변수의 기본값으로 `Query`를 사용해야 했습니다. 주변에서 이 방식을 사용하는 코드를 볼 가능성이 높기 때문에 설명해 드리겠습니다. + +/// tip | 팁 + +새 코드를 작성할 때와 가능할 때는 위에서 설명한 대로 `Annotated`를 사용하세요. 여러 장점이 있고(아래에서 설명합니다) 단점은 없습니다. 🍰 + +/// + +다음은 함수 매개변수의 기본값으로 `Query()`를 사용하면서 `max_length`를 50으로 설정하는 방법입니다: + +{* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} + +이 경우(`Annotated`를 사용하지 않는 경우) 함수에서 기본값 `None`을 `Query()`로 바꿔야 하므로, 이제 `Query(default=None)`로 기본값을 설정해야 합니다. (최소한 FastAPI 입장에서는) 이 인자는 해당 기본값을 정의하는 것과 같은 목적을 수행합니다. 그러므로: ```Python -q: Optional[str] = Query(None) +q: str | None = Query(default=None) ``` -...위 코드는 아래와 동일하게 매개변수를 선택적으로 만듭니다: +...위 코드는 기본값이 `None`인 선택적 매개변수를 만들며, 아래와 동일합니다: + ```Python -q: Optional[str] = None +q: str | None = None ``` -하지만 명시적으로 쿼리 매개변수를 선언합니다. +하지만 `Query` 버전은 이것이 쿼리 매개변수임을 명시적으로 선언합니다. -/// info | 정보 - -FastAPI는 다음 부분에 관심이 있습니다: +그 다음, `Query`로 더 많은 매개변수를 전달할 수 있습니다. 지금의 경우 문자열에 적용되는 `max_length` 매개변수입니다: ```Python -= None +q: str | None = Query(default=None, max_length=50) ``` -또는: +이는 데이터를 검증할 것이고, 데이터가 유효하지 않다면 명백한 오류를 보여주며, OpenAPI 스키마 *경로 처리*에 매개변수를 문서화 합니다. + +### 기본값으로 `Query` 사용 또는 `Annotated`에 넣기 { #query-as-the-default-value-or-in-annotated } + +`Annotated` 안에서 `Query`를 사용할 때는 `Query`에 `default` 매개변수를 사용할 수 없다는 점을 기억하세요. + +대신 함수 매개변수의 실제 기본값을 사용하세요. 그렇지 않으면 일관성이 깨집니다. + +예를 들어, 다음은 허용되지 않습니다: ```Python -= Query(None) +q: Annotated[str, Query(default="rick")] = "morty" ``` -그리고 `None`을 사용하여 쿼라 매개변수가 필수적이지 않다는 것을 파악합니다. +...왜냐하면 기본값이 `"rick"`인지 `"morty"`인지 명확하지 않기 때문입니다. -`Optional` 부분은 편집기에게 더 나은 지원을 제공하기 위해서만 사용됩니다. - -/// - -또한 `Query`로 더 많은 매개변수를 전달할 수 있습니다. 지금의 경우 문자열에 적용되는 `max_length` 매개변수입니다: +따라서 (가능하면) 다음과 같이 사용합니다: ```Python -q: str = Query(None, max_length=50) +q: Annotated[str, Query()] = "rick" ``` -이는 데이터를 검증할 것이고, 데이터가 유효하지 않다면 명백한 오류를 보여주며, OpenAPI 스키마 *경로 작동*에 매개변수를 문서화 합니다. +...또는 오래된 코드베이스에서는 다음과 같은 코드를 찾게 될 것입니다: -## 검증 추가 +```Python +q: str = Query(default="rick") +``` -매개변수 `min_length` 또한 추가할 수 있습니다: +### `Annotated`의 장점 { #advantages-of-annotated } -{* ../../docs_src/query_params_str_validations/tutorial003.py hl[9] *} +함수 매개변수의 기본값 방식 대신 **`Annotated`를 사용하는 것을 권장**합니다. 여러 이유로 **더 좋기** 때문입니다. 🤓 -## 정규식 추가 +**함수 매개변수**의 **기본값**이 **실제 기본값**이 되므로, 전반적으로 Python에 더 직관적입니다. 😌 -매개변수와 일치해야 하는 <abbr title="정규표현식(regular expression), regex 또는 regexp는 문자열 조회 패턴을 정의하는 문자들의 순열입니다">정규표현식</abbr>을 정의할 수 있습니다: +FastAPI 없이도 **다른 곳에서** 같은 함수를 **호출**할 수 있고, **예상대로 동작**합니다. **필수** 매개변수(기본값이 없는 경우)가 있다면 **편집기**가 오류로 알려줄 것이고, 필수 매개변수를 전달하지 않고 실행하면 **Python**도 오류를 냅니다. -{* ../../docs_src/query_params_str_validations/tutorial004.py hl[10] *} +`Annotated`를 사용하지 않고 **(이전) 기본값 스타일**을 사용하면, FastAPI 없이 **다른 곳에서** 함수를 호출할 때도 제대로 동작하도록 함수에 인자를 전달해야 한다는 것을 **기억**해야 합니다. 그렇지 않으면 값이 기대와 다르게 됩니다(예: `str` 대신 `QueryInfo` 같은 것). 그리고 편집기도 경고하지 않고 Python도 그 함수를 실행할 때는 불평하지 않으며, 오직 내부 동작에서 오류가 발생할 때만 문제가 드러납니다. -이 특정 정규표현식은 전달 받은 매개변수 값을 검사합니다: +`Annotated`는 하나 이상의 메타데이터 어노테이션을 가질 수 있기 때문에, 이제 <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">Typer</a> 같은 다른 도구에서도 같은 함수를 사용할 수 있습니다. 🚀 -* `^`: 이전에 문자가 없고 뒤따르는 문자로 시작합니다. -* `fixedquery`: 정확히 `fixedquery` 값을 갖습니다. -* `$`: 여기서 끝나고 `fixedquery` 이후로 아무 문자도 갖지 않습니다. +## 검증 더 추가하기 { #add-more-validations } -**"정규표현식"** 개념에 대해 상실감을 느꼈다면 걱정하지 않아도 됩니다. 많은 사람에게 어려운 주제입니다. 아직은 정규표현식 없이도 많은 작업들을 할 수 있습니다. +`min_length` 매개변수도 추가할 수 있습니다: -하지만 언제든지 가서 배울수 있고, **FastAPI**에서 직접 사용할 수 있다는 사실을 알고 있어야 합니다. +{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} -## 기본값 +## 정규식 추가 { #add-regular-expressions } -기본값으로 사용하는 첫 번째 인자로 `None`을 전달하듯이, 다른 값을 전달할 수 있습니다. +매개변수와 일치해야 하는 <abbr title="문자열에 대한 검색 패턴을 정의하는 문자들의 순열인 정규 표현식(regular expression), regex 또는 regexp입니다.">정규 표현식</abbr> `pattern`을 정의할 수 있습니다: -`min_length`가 `3`이고, 기본값이 `"fixedquery"`인 쿼리 매개변수 `q`를 선언해봅시다: +{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} -{* ../../docs_src/query_params_str_validations/tutorial005.py hl[7] *} +이 특정 정규표현식 패턴은 전달 받은 매개변수 값이 다음을 만족하는지 검사합니다: + +* `^`: 뒤따르는 문자로 시작하며, 앞에는 문자가 없습니다. +* `fixedquery`: 정확히 `fixedquery` 값을 가집니다. +* `$`: 여기서 끝나며, `fixedquery` 이후로 더 이상 문자가 없습니다. + +**"정규 표현식"** 개념에 대해 상실감을 느꼈다면 걱정하지 않아도 됩니다. 많은 사람에게 어려운 주제입니다. 아직은 정규 표현식 없이도 많은 작업들을 할 수 있습니다. + +이제 필요할 때 언제든지 **FastAPI**에서 직접 사용할 수 있다는 사실을 알게 되었습니다. + +## 기본값 { #default-values } + +물론 `None`이 아닌 다른 기본값을 사용할 수도 있습니다. + +`q` 쿼리 매개변수에 `min_length`를 `3`으로 설정하고, 기본값을 `"fixedquery"`로 선언하고 싶다고 해봅시다: + +{* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *} /// note | 참고 -기본값을 갖는 것만으로 매개변수는 선택적이 됩니다. +`None`을 포함해 어떤 타입이든 기본값을 가지면 매개변수는 선택적(필수 아님)이 됩니다. /// -## 필수로 만들기 +## 필수 매개변수 { #required-parameters } 더 많은 검증이나 메타데이터를 선언할 필요가 없는 경우, 다음과 같이 기본값을 선언하지 않고 쿼리 매개변수 `q`를 필수로 만들 수 있습니다: @@ -123,42 +231,42 @@ q: str 아래 대신: ```Python -q: Optional[str] = None +q: str | None = None ``` -그러나 이제 다음과 같이 `Query`로 선언합니다: +하지만 이제는 예를 들어 다음과 같이 `Query`로 선언합니다: ```Python -q: Optional[str] = Query(None, min_length=3) +q: Annotated[str | None, Query(min_length=3)] = None ``` -그래서 `Query`를 필수값으로 만들어야 할 때면, 첫 번째 인자로 `...`를 사용할 수 있습니다: +따라서 `Query`를 사용하면서 값을 필수로 선언해야 할 때는, 기본값을 선언하지 않으면 됩니다: -{* ../../docs_src/query_params_str_validations/tutorial006.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -/// info | 정보 +### 필수지만 `None` 가능 { #required-can-be-none } -이전에 `...`를 본적이 없다면: 특별한 단일값으로, <a href="https://docs.python.org/3/library/constants.html#Ellipsis" class="external-link" target="_blank">파이썬의 일부이며 "Ellipsis"라 부릅니다</a>. +매개변수가 `None`을 허용하지만 여전히 필수라고 선언할 수 있습니다. 이렇게 하면 값이 `None`이더라도 클라이언트는 값을 반드시 전송해야 합니다. -/// +이를 위해 `None`이 유효한 타입이라고 선언하되, 기본값은 선언하지 않으면 됩니다: -이렇게 하면 **FastAPI**가 이 매개변수는 필수임을 알 수 있습니다. +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} -## 쿼리 매개변수 리스트 / 다중값 +## 쿼리 매개변수 리스트 / 다중값 { #query-parameter-list-multiple-values } -쿼리 매개변수를 `Query`와 함께 명시적으로 선언할 때, 값들의 리스트나 다른 방법으로 여러 값을 받도록 선언 할 수도 있습니다. +`Query`로 쿼리 매개변수를 명시적으로 정의할 때 값들의 리스트를 받도록 선언할 수도 있고, 다른 말로 하면 여러 값을 받도록 선언할 수도 있습니다. -예를 들어, URL에서 여러번 나오는 `q` 쿼리 매개변수를 선언하려면 다음과 같이 작성할 수 있습니다: +예를 들어, URL에서 여러 번 나타날 수 있는 `q` 쿼리 매개변수를 선언하려면 다음과 같이 작성할 수 있습니다: -{* ../../docs_src/query_params_str_validations/tutorial011.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *} -아래와 같은 URL을 사용합니다: +그 다음, 아래와 같은 URL로: ``` http://localhost:8000/items/?q=foo&q=bar ``` -여러 `q` *쿼리 매개변수* 값들을 (`foo` 및 `bar`) 파이썬 `list`로 *경로 작동 함수* 내 *함수 매개변수* `q`로 전달 받습니다. +여러 `q` *쿼리 매개변수* 값들(`foo` 및 `bar`)을 파이썬 `list`로 *경로 처리 함수*의 *함수 매개변수* `q`에서 받게 됩니다. 따라서 해당 URL에 대한 응답은 다음과 같습니다: @@ -173,7 +281,7 @@ http://localhost:8000/items/?q=foo&q=bar /// tip | 팁 -위의 예와 같이 `list` 자료형으로 쿼리 매개변수를 선언하려면 `Query`를 명시적으로 사용해야 합니다. 그렇지 않으면 요청 본문으로 해석됩니다. +위의 예와 같이 `list` 타입으로 쿼리 매개변수를 선언하려면 `Query`를 명시적으로 사용해야 합니다. 그렇지 않으면 요청 본문으로 해석됩니다. /// @@ -181,19 +289,19 @@ http://localhost:8000/items/?q=foo&q=bar <img src="/img/tutorial/query-params-str-validations/image02.png"> -### 쿼리 매개변수 리스트 / 기본값을 사용하는 다중값 +### 쿼리 매개변수 리스트 / 기본값이 있는 다중값 { #query-parameter-list-multiple-values-with-defaults } -그리고 제공된 값이 없으면 기본 `list` 값을 정의할 수도 있습니다: +제공된 값이 없으면 기본 `list` 값을 정의할 수도 있습니다: -{* ../../docs_src/query_params_str_validations/tutorial012.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} -아래로 이동한다면: +다음으로 이동하면: ``` http://localhost:8000/items/ ``` -`q`의 기본값은: `["foo", "bar"]`이며 응답은 다음이 됩니다: +`q`의 기본값은 `["foo", "bar"]`가 되고, 응답은 다음이 됩니다: ```JSON { @@ -204,21 +312,21 @@ http://localhost:8000/items/ } ``` -#### `list` 사용하기 +#### `list`만 사용하기 { #using-just-list } -`List[str]` 대신 `list`를 직접 사용할 수도 있습니다: +`list[str]` 대신 `list`를 직접 사용할 수도 있습니다: -{* ../../docs_src/query_params_str_validations/tutorial013.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} /// note | 참고 -이 경우 FastAPI는 리스트의 내용을 검사하지 않음을 명심하기 바랍니다. +이 경우 FastAPI는 리스트의 내용을 검사하지 않음을 명심하세요. -예를 들어, `List[int]`는 리스트 내용이 정수인지 검사(및 문서화)합니다. 하지만 `list` 단독일 경우는 아닙니다. +예를 들어, `list[int]`는 리스트 내용이 정수인지 검사(및 문서화)합니다. 하지만 `list` 단독일 경우는 아닙니다. /// -## 더 많은 메타데이터 선언 +## 더 많은 메타데이터 선언 { #declare-more-metadata } 매개변수에 대한 정보를 추가할 수 있습니다. @@ -226,7 +334,7 @@ http://localhost:8000/items/ /// note | 참고 -도구에 따라 OpenAPI 지원 수준이 다를 수 있음을 명심하기 바랍니다. +도구에 따라 OpenAPI 지원 수준이 다를 수 있음을 명심하세요. 일부는 아직 선언된 추가 정보를 모두 표시하지 않을 수 있지만, 대부분의 경우 누락된 기능은 이미 개발 계획이 있습니다. @@ -234,13 +342,13 @@ http://localhost:8000/items/ `title`을 추가할 수 있습니다: -{* ../../docs_src/query_params_str_validations/tutorial007.py hl[10] *} +{* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *} 그리고 `description`도 추가할 수 있습니다: -{* ../../docs_src/query_params_str_validations/tutorial008.py hl[13] *} +{* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} -## 별칭 매개변수 +## 별칭 매개변수 { #alias-parameters } 매개변수가 `item-query`이길 원한다고 가정해 봅시다. @@ -250,31 +358,99 @@ http://localhost:8000/items/ http://127.0.0.1:8000/items/?item-query=foobaritems ``` -그러나 `item-query`은 유효한 파이썬 변수 이름이 아닙니다. +그러나 `item-query`는 유효한 파이썬 변수 이름이 아닙니다. 가장 가까운 것은 `item_query`일 겁니다. -하지만 정확히`item-query`이길 원합니다... +하지만 정확히 `item-query`이길 원합니다... 이럴 경우 `alias`를 선언할 수 있으며, 해당 별칭은 매개변수 값을 찾는 데 사용됩니다: -{* ../../docs_src/query_params_str_validations/tutorial009.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} -## 매개변수 사용하지 않게 하기 +## 매개변수 사용 중단하기 { #deprecating-parameters } -이제는 더이상 이 매개변수를 마음에 들어하지 않는다고 가정해 봅시다. +이제는 더 이상 이 매개변수를 마음에 들어하지 않는다고 가정해 봅시다. -이 매개변수를 사용하는 클라이언트가 있기 때문에 한동안은 남겨둬야 하지만, <abbr title="구식이며, 사용하지 않는 것을 추천">사용되지 않는다(deprecated)</abbr>고 확실하게 문서에서 보여주고 싶습니다. +이 매개변수를 사용하는 클라이언트가 있기 때문에 한동안은 남겨둬야 하지만, 문서에서 <abbr title="obsolete, recommended not to use it – 구식이며, 사용하지 않는 것을 추천">deprecated</abbr>로 명확하게 보여주고 싶습니다. 그렇다면 `deprecated=True` 매개변수를 `Query`로 전달합니다: -{* ../../docs_src/query_params_str_validations/tutorial010.py hl[18] *} +{* ../../docs_src/query_params_str_validations/tutorial010_an_py310.py hl[19] *} 문서가 아래와 같이 보일겁니다: <img src="/img/tutorial/query-params-str-validations/image01.png"> -## 요약 +## OpenAPI에서 매개변수 제외 { #exclude-parameters-from-openapi } + +생성된 OpenAPI 스키마(따라서 자동 문서화 시스템)에서 쿼리 매개변수를 제외하려면 `Query`의 `include_in_schema` 매개변수를 `False`로 설정하세요: + +{* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} + +## 커스텀 검증 { #custom-validation } + +위에 나온 매개변수들로는 할 수 없는 **커스텀 검증**이 필요한 경우가 있을 수 있습니다. + +그런 경우에는 일반적인 검증(예: 값이 `str`인지 검증한 뒤) 이후에 적용되는 **커스텀 검증 함수**를 사용할 수 있습니다. + +`Annotated` 안에서 <a href="https://docs.pydantic.dev/latest/concepts/validators/#field-after-validator" class="external-link" target="_blank">Pydantic의 `AfterValidator`</a>를 사용하면 이를 구현할 수 있습니다. + +/// tip | 팁 + +Pydantic에는 <a href="https://docs.pydantic.dev/latest/concepts/validators/#field-before-validator" class="external-link" target="_blank">`BeforeValidator`</a>와 같은 다른 것들도 있습니다. 🤓 + +/// + +예를 들어, 이 커스텀 validator는 <abbr title="ISBN means International Standard Book Number – 국제 표준 도서 번호">ISBN</abbr> 도서 번호의 경우 아이템 ID가 `isbn-`으로 시작하고, <abbr title="IMDB (Internet Movie Database) is a website with information about movies – 영화에 대한 정보를 제공하는 웹사이트">IMDB</abbr> 영화 URL ID의 경우 `imdb-`로 시작하는지 확인합니다: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} + +/// info | 정보 + +이는 Pydantic 2 이상 버전에서 사용할 수 있습니다. 😎 + +/// + +/// tip | 팁 + +데이터베이스나 다른 API 같은 **외부 구성요소**와 통신이 필요한 어떤 종류의 검증이든 해야 한다면, 대신 **FastAPI Dependencies**를 사용해야 합니다. 이에 대해서는 나중에 배우게 됩니다. + +이 커스텀 validator는 요청에서 제공된 **같은 데이터만**으로 확인할 수 있는 것들을 위한 것입니다. + +/// + +### 코드 이해하기 { #understand-that-code } + +중요한 부분은 **`Annotated` 안에서 함수와 함께 `AfterValidator`를 사용한다는 것**뿐입니다. 이 부분은 건너뛰셔도 됩니다. 🤸 + +--- + +하지만 이 특정 코드 예제가 궁금하고 계속 보고 싶다면, 추가 세부사항은 다음과 같습니다. + +#### `value.startswith()`를 사용한 문자열 { #string-with-value-startswith } + +알고 계셨나요? `value.startswith()`를 사용하는 문자열은 튜플을 받을 수 있으며, 튜플에 있는 각 값을 확인합니다: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *} + +#### 임의의 항목 { #a-random-item } + +`data.items()`를 사용하면 각 딕셔너리 항목의 키와 값을 담은 튜플로 구성된 <abbr title="리스트, 세트 등처럼 for 루프로 순회할 수 있는 것">iterable object</abbr>를 얻습니다. + +이 iterable object를 `list(data.items())`로 적절한 `list`로 변환합니다. + +그 다음 `random.choice()`로 리스트에서 **무작위 값**을 얻어 `(id, name)` 형태의 튜플을 얻습니다. 예를 들어 `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")` 같은 값이 될 것입니다. + +그 다음 이 튜플의 **두 값을** 변수 `id`와 `name`에 **할당**합니다. + +따라서 사용자가 아이템 ID를 제공하지 않더라도, 무작위 추천을 받게 됩니다. + +...이 모든 것을 **단 하나의 간단한 줄**로 합니다. 🤯 Python 정말 좋지 않나요? 🐍 + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} + +## 요약 { #recap } 매개변수에 검증과 메타데이터를 추가 선언할 수 있습니다. @@ -285,12 +461,14 @@ http://127.0.0.1:8000/items/?item-query=foobaritems * `description` * `deprecated` -특정 문자열 검증: +문자열에 특화된 검증: * `min_length` * `max_length` -* `regex` +* `pattern` + +`AfterValidator`를 사용하는 커스텀 검증. 예제에서 `str` 값의 검증을 어떻게 추가하는지 살펴보았습니다. -숫자와 같은 다른 자료형에 대한 검증을 어떻게 선언하는지 확인하려면 다음 장을 확인하기 바랍니다. +숫자와 같은 다른 타입에 대한 검증을 어떻게 선언하는지 확인하려면 다음 장을 확인하기 바랍니다. diff --git a/docs/ko/docs/tutorial/query-params.md b/docs/ko/docs/tutorial/query-params.md index d5b9837c4..5124f73bf 100644 --- a/docs/ko/docs/tutorial/query-params.md +++ b/docs/ko/docs/tutorial/query-params.md @@ -1,8 +1,8 @@ -# 쿼리 매개변수 +# 쿼리 매개변수 { #query-parameters } 경로 매개변수의 일부가 아닌 다른 함수 매개변수를 선언하면 "쿼리" 매개변수로 자동 해석합니다. -{* ../../docs_src/query_params/tutorial001.py hl[9] *} +{* ../../docs_src/query_params/tutorial001_py39.py hl[9] *} 쿼리는 URL에서 `?` 후에 나오고 `&`으로 구분되는 키-값 쌍의 집합입니다. @@ -24,11 +24,11 @@ URL의 일부이므로 "자연스럽게" 문자열입니다. 경로 매개변수에 적용된 동일한 프로세스가 쿼리 매개변수에도 적용됩니다: * (당연히) 편집기 지원 -* 데이터 <abbr title="HTTP 요청에서 전달되는 문자열을 파이썬 데이터로 변환">"파싱"</abbr> +* 데이터 <abbr title="converting the string that comes from an HTTP request into Python data">"파싱"</abbr> * 데이터 검증 * 자동 문서화 -## 기본값 +## 기본값 { #defaults } 쿼리 매개변수는 경로에서 고정된 부분이 아니기 때문에 선택적일 수 있고 기본값을 가질 수 있습니다. @@ -57,33 +57,25 @@ http://127.0.0.1:8000/items/?skip=20 * `skip=20`: URL에서 지정했기 때문입니다 * `limit=10`: 기본값이기 때문입니다 -## 선택적 매개변수 +## 선택적 매개변수 { #optional-parameters } 같은 방법으로 기본값을 `None`으로 설정하여 선택적 매개변수를 선언할 수 있습니다: -{* ../../docs_src/query_params/tutorial002.py hl[9] *} +{* ../../docs_src/query_params/tutorial002_py310.py hl[7] *} 이 경우 함수 매개변수 `q`는 선택적이며 기본값으로 `None` 값이 됩니다. -/// check | 확인 +/// check -**FastAPI**는 `item_id`가 경로 매개변수이고 `q`는 경로 매개변수가 아닌 쿼리 매개변수라는 것을 알 정도로 충분히 똑똑합니다. +또한 **FastAPI**는 `item_id`가 경로 매개변수이고 `q`는 경로 매개변수가 아니라서 쿼리 매개변수라는 것을 알 정도로 충분히 똑똑하다는 점도 확인하세요. /// -/// note | 참고 - -FastAPI는 `q`가 `= None`이므로 선택적이라는 것을 인지합니다. - -`Union[str, None]`에 있는 `Union`은 FastAPI(FastAPI는 `str` 부분만 사용합니다)가 사용하는게 아니지만, `Union[str, None]`은 편집기에게 코드에서 오류를 찾아낼 수 있게 도와줍니다. - -/// - -## 쿼리 매개변수 형변환 +## 쿼리 매개변수 형변환 { #query-parameter-type-conversion } `bool` 형으로 선언할 수도 있고, 아래처럼 변환됩니다: -{* ../../docs_src/query_params/tutorial003.py hl[9] *} +{* ../../docs_src/query_params/tutorial003_py310.py hl[7] *} 이 경우, 아래로 이동하면: @@ -115,10 +107,10 @@ http://127.0.0.1:8000/items/foo?short=on http://127.0.0.1:8000/items/foo?short=yes ``` -또는 다른 어떤 변형(대문자, 첫글자만 대문자 등)이더라도 함수는 매개변수 `bool`형을 가진 `short`의 값이 `True`임을 압니다. 그렇지 않은 경우 `False`입니다. +또는 다른 어떤 변형(대문자, 첫글자만 대문자 등)이더라도 함수는 `bool` 값이 `True`인 매개변수 `short`를 보게 됩니다. 그렇지 않은 경우 `False`입니다. -## 여러 경로/쿼리 매개변수 +## 여러 경로/쿼리 매개변수 { #multiple-path-and-query-parameters } 여러 경로 매개변수와 쿼리 매개변수를 동시에 선언할 수 있으며 **FastAPI**는 어느 것이 무엇인지 알고 있습니다. @@ -126,9 +118,9 @@ http://127.0.0.1:8000/items/foo?short=yes 매개변수들은 이름으로 감지됩니다: -{* ../../docs_src/query_params/tutorial004.py hl[8,10] *} +{* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## 필수 쿼리 매개변수 +## 필수 쿼리 매개변수 { #required-query-parameters } 경로가 아닌 매개변수에 대한 기본값을 선언할 때(지금은 쿼리 매개변수만 보았습니다), 해당 매개변수는 필수적(Required)이지 않았습니다. @@ -136,7 +128,7 @@ http://127.0.0.1:8000/items/foo?short=yes 그러나 쿼리 매개변수를 필수로 만들려면 단순히 기본값을 선언하지 않으면 됩니다: -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} 여기 쿼리 매개변수 `needy`는 `str`형인 필수 쿼리 매개변수입니다. @@ -150,16 +142,17 @@ http://127.0.0.1:8000/items/foo-item ```JSON { - "detail": [ - { - "loc": [ - "query", - "needy" - ], - "msg": "field required", - "type": "value_error.missing" - } - ] + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "needy" + ], + "msg": "Field required", + "input": null + } + ] } ``` @@ -180,7 +173,7 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy 그리고 물론, 일부 매개변수는 필수로, 다른 일부는 기본값을, 또 다른 일부는 선택적으로 선언할 수 있습니다: -{* ../../docs_src/query_params/tutorial006.py hl[10] *} +{* ../../docs_src/query_params/tutorial006_py310.py hl[8] *} 위 예시에서는 3가지 쿼리 매개변수가 있습니다: @@ -188,8 +181,8 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy * `skip`, 기본값이 `0`인 `int`. * `limit`, 선택적인 `int`. -/// tip | 팁 +/// tip -[경로 매개변수](path-params.md#_8){.internal-link target=_blank}와 마찬가지로 `Enum`을 사용할 수 있습니다. +[경로 매개변수](path-params.md#predefined-values){.internal-link target=_blank}와 마찬가지로 `Enum`을 사용할 수 있습니다. /// diff --git a/docs/ko/docs/tutorial/request-files.md b/docs/ko/docs/tutorial/request-files.md index 9162b353c..cc0000921 100644 --- a/docs/ko/docs/tutorial/request-files.md +++ b/docs/ko/docs/tutorial/request-files.md @@ -1,4 +1,4 @@ -# 파일 요청 +# 파일 요청 { #request-files } `File`을 사용하여 클라이언트가 업로드할 파일들을 정의할 수 있습니다. @@ -6,23 +6,27 @@ 업로드된 파일을 전달받기 위해 먼저 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>를 설치해야합니다. -예시) `pip install python-multipart`. +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고, 활성화한 다음, 예를 들어 다음과 같이 설치하세요: + +```console +$ pip install python-multipart +``` 업로드된 파일들은 "폼 데이터"의 형태로 전송되기 때문에 이 작업이 필요합니다. /// -## `File` 임포트 +## `File` 임포트 { #import-file } `fastapi` 에서 `File` 과 `UploadFile` 을 임포트 합니다: -{* ../../docs_src/request_files/tutorial001.py hl[1] *} +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *} -## `File` 매개변수 정의 +## `File` 매개변수 정의 { #define-file-parameters } `Body` 및 `Form` 과 동일한 방식으로 파일의 매개변수를 생성합니다: -{* ../../docs_src/request_files/tutorial001.py hl[7] *} +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[9] *} /// info | 정보 @@ -40,20 +44,21 @@ File의 본문을 선언할 때, 매개변수가 쿼리 매개변수 또는 본 파일들은 "폼 데이터"의 형태로 업로드 됩니다. -*경로 작동 함수*의 매개변수를 `bytes` 로 선언하는 경우 **FastAPI**는 파일을 읽고 `bytes` 형태의 내용을 전달합니다. +*경로 처리 함수*의 매개변수를 `bytes` 로 선언하는 경우 **FastAPI**는 파일을 읽고 `bytes` 형태의 내용을 전달합니다. 이것은 전체 내용이 메모리에 저장된다는 것을 의미한다는 걸 염두하기 바랍니다. 이는 작은 크기의 파일들에 적합합니다. 어떤 경우에는 `UploadFile` 을 사용하는 것이 더 유리합니다. -## `File` 매개변수와 `UploadFile` +## `UploadFile`을 사용하는 `File` 매개변수 { #file-parameters-with-uploadfile } `File` 매개변수를 `UploadFile` 타입으로 정의합니다: -{* ../../docs_src/request_files/tutorial001.py hl[12] *} +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[14] *} `UploadFile` 을 사용하는 것은 `bytes` 과 비교해 다음과 같은 장점이 있습니다: +* 매개변수의 기본값에서 `File()`을 사용할 필요가 없습니다. * "스풀 파일"을 사용합니다. * 최대 크기 제한까지만 메모리에 저장되며, 이를 초과하는 경우 디스크에 저장됩니다. * 따라서 이미지, 동영상, 큰 이진코드와 같은 대용량 파일들을 많은 메모리를 소모하지 않고 처리하기에 적합합니다. @@ -61,13 +66,13 @@ File의 본문을 선언할 때, 매개변수가 쿼리 매개변수 또는 본 * <a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> `async` 인터페이스를 갖고 있습니다. * file-like object를 필요로하는 다른 라이브러리에 직접적으로 전달할 수 있는 파이썬 <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a> 객체를 반환합니다. -### `UploadFile` +### `UploadFile` { #uploadfile } `UploadFile` 은 다음과 같은 어트리뷰트가 있습니다: * `filename` : 문자열(`str`)로 된 업로드된 파일의 파일명입니다 (예: `myimage.jpg`). * `content_type` : 문자열(`str`)로 된 파일 형식(MIME type / media type)입니다 (예: `image/jpeg`). -* `file` : <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a> (<a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">파일류</a> 객체)입니다. 이것은 "파일류" 객체를 필요로하는 다른 라이브러리에 직접적으로 전달할 수 있는 실질적인 파이썬 파일입니다. +* `file` : <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a> (a <a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> object)입니다. 이것은 "file-like" 객체를 필요로하는 다른 함수나 라이브러리에 직접적으로 전달할 수 있는 실질적인 파이썬 파일 객체입니다. `UploadFile` 에는 다음의 `async` 메소드들이 있습니다. 이들은 내부적인 `SpooledTemporaryFile` 을 사용하여 해당하는 파일 메소드를 호출합니다. @@ -80,55 +85,67 @@ File의 본문을 선언할 때, 매개변수가 쿼리 매개변수 또는 본 상기 모든 메소드들이 `async` 메소드이기 때문에 “await”을 사용하여야 합니다. -예를들어, `async` *경로 작동 함수*의 내부에서 다음과 같은 방식으로 내용을 가져올 수 있습니다: +예를들어, `async` *경로 처리 함수*의 내부에서 다음과 같은 방식으로 내용을 가져올 수 있습니다: ```Python contents = await myfile.read() ``` -만약 일반적인 `def` *경로 작동 함수*의 내부라면, 다음과 같이 `UploadFile.file` 에 직접 접근할 수 있습니다: +만약 일반적인 `def` *경로 처리 함수*의 내부라면, 다음과 같이 `UploadFile.file` 에 직접 접근할 수 있습니다: ```Python contents = myfile.file.read() ``` -/// note | "`async` 기술적 세부사항" +/// note | `async` 기술 세부사항 `async` 메소드들을 사용할 때 **FastAPI**는 스레드풀에서 파일 메소드들을 실행하고 그들을 기다립니다. /// -/// note | Starlette 기술적 세부사항 +/// note | Starlette 기술 세부사항 **FastAPI**의 `UploadFile` 은 **Starlette**의 `UploadFile` 을 직접적으로 상속받지만, **Pydantic** 및 FastAPI의 다른 부분들과의 호환성을 위해 필요한 부분들이 추가되었습니다. /// -## "폼 데이터"란 +## "폼 데이터"란 { #what-is-form-data } HTML의 폼들(`<form></form>`)이 서버에 데이터를 전송하는 방식은 대개 데이터에 JSON과는 다른 "특별한" 인코딩을 사용합니다. **FastAPI**는 JSON 대신 올바른 위치에서 데이터를 읽을 수 있도록 합니다. -/// note | 기술적 세부사항 +/// note | 기술 세부사항 폼의 데이터는 파일이 포함되지 않은 경우 일반적으로 "미디어 유형" `application/x-www-form-urlencoded` 을 사용해 인코딩 됩니다. 하지만 파일이 포함된 경우, `multipart/form-data`로 인코딩됩니다. `File`을 사용하였다면, **FastAPI**는 본문의 적합한 부분에서 파일을 가져와야 한다는 것을 인지합니다. -인코딩과 폼 필드에 대해 더 알고싶다면, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><code>POST</code>에 관한<abbr title="Mozilla Developer Network">MDN</abbr>웹 문서</a> 를 참고하기 바랍니다,. +인코딩과 폼 필드에 대해 더 알고싶다면, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> web docs for <code>POST</code></a>를 참고하기 바랍니다. /// /// warning | 경고 -다수의 `File` 과 `Form` 매개변수를 한 *경로 작동*에 선언하는 것이 가능하지만, 요청의 본문이 `application/json` 가 아닌 `multipart/form-data` 로 인코딩 되기 때문에 JSON으로 받아야하는 `Body` 필드를 함께 선언할 수는 없습니다. +다수의 `File` 과 `Form` 매개변수를 한 *경로 처리*에 선언하는 것이 가능하지만, 요청의 본문이 `application/json` 가 아닌 `multipart/form-data` 로 인코딩 되기 때문에 JSON으로 받아야하는 `Body` 필드를 함께 선언할 수는 없습니다. 이는 **FastAPI**의 한계가 아니라, HTTP 프로토콜에 의한 것입니다. /// -## 다중 파일 업로드 +## 선택적 파일 업로드 { #optional-file-upload } + +표준 타입 애너테이션을 사용하고 기본값을 `None`으로 설정하여 파일을 선택적으로 만들 수 있습니다: + +{* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} + +## 추가 메타데이터를 포함한 `UploadFile` { #uploadfile-with-additional-metadata } + +추가 메타데이터를 설정하기 위해 예를 들어 `UploadFile`과 함께 `File()`을 사용할 수도 있습니다: + +{* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} + +## 다중 파일 업로드 { #multiple-file-uploads } 여러 파일을 동시에 업로드 할 수 있습니다. @@ -136,21 +153,11 @@ HTML의 폼들(`<form></form>`)이 서버에 데이터를 전송하는 방식은 이 기능을 사용하기 위해 , `bytes` 의 `List` 또는 `UploadFile` 를 선언하기 바랍니다: -{* ../../docs_src/request_files/tutorial002.py hl[10,15] *} +{* ../../docs_src/request_files/tutorial002_an_py39.py hl[10,15] *} 선언한대로, `bytes` 의 `list` 또는 `UploadFile` 들을 전송받을 것입니다. -/// note | 참고 - -2019년 4월 14일부터 Swagger UI가 하나의 폼 필드로 다수의 파일을 업로드하는 것을 지원하지 않습니다. 더 많은 정보를 원하면, <a href="https://github.com/swagger-api/swagger-ui/issues/4276" class="external-link" target="_blank">#4276</a>과 <a href="https://github.com/swagger-api/swagger-ui/issues/3641" class="external-link" target="_blank">#3641</a>을 참고하세요. - -그럼에도, **FastAPI**는 표준 Open API를 사용해 이미 호환이 가능합니다. - -따라서 Swagger UI 또는 기타 그 외의 OpenAPI를 지원하는 툴이 다중 파일 업로드를 지원하는 경우, 이들은 **FastAPI**와 호환됩니다. - -/// - -/// note | 기술적 세부사항 +/// note | 기술 세부사항 `from starlette.responses import HTMLResponse` 역시 사용할 수 있습니다. @@ -158,6 +165,12 @@ HTML의 폼들(`<form></form>`)이 서버에 데이터를 전송하는 방식은 /// -## 요약 +### 추가 메타데이터를 포함한 다중 파일 업로드 { #multiple-file-uploads-with-additional-metadata } -폼 데이터로써 입력 매개변수로 업로드할 파일을 선언할 경우 `File` 을 사용하기 바랍니다. +이전과 같은 방식으로 `UploadFile`에 대해서도 `File()`을 사용해 추가 매개변수를 설정할 수 있습니다: + +{* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} + +## 요약 { #recap } + +`File`, `bytes`, `UploadFile`을 사용하여 폼 데이터로 전송되는 요청에서 업로드할 파일을 선언하세요. diff --git a/docs/ko/docs/tutorial/request-form-models.md b/docs/ko/docs/tutorial/request-form-models.md index 3316a93d5..b37186dfb 100644 --- a/docs/ko/docs/tutorial/request-form-models.md +++ b/docs/ko/docs/tutorial/request-form-models.md @@ -1,12 +1,12 @@ -# 폼 모델 +# 폼 모델 { #form-models } FastAPI에서 **Pydantic 모델**을 이용하여 **폼 필드**를 선언할 수 있습니다. /// info | 정보 -폼(Form)을 사용하려면, 먼저 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>를 설치하세요. +폼을 사용하려면, 먼저 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>를 설치하세요. -[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고 활성화한 다음, 아래와 같이 설치할 수 있습니다: +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고 활성화한 다음, 예를 들어 아래와 같이 설치하세요: ```console $ pip install python-multipart @@ -20,7 +20,7 @@ $ pip install python-multipart /// -## Pydantic 모델을 사용한 폼 +## 폼을 위한 Pydantic 모델 { #pydantic-models-for-forms } **폼 필드**로 받고 싶은 필드를 **Pydantic 모델**로 선언한 다음, 매개변수를 `Form`으로 선언하면 됩니다: @@ -28,7 +28,7 @@ $ pip install python-multipart **FastAPI**는 요청에서 받은 **폼 데이터**에서 **각 필드**에 대한 데이터를 **추출**하고 정의한 Pydantic 모델을 줍니다. -## 문서 확인하기 +## 문서 확인하기 { #check-the-docs } 문서 UI `/docs`에서 확인할 수 있습니다: @@ -36,9 +36,9 @@ $ pip install python-multipart <img src="/img/tutorial/request-form-models/image01.png"> </div> -## 추가 폼 필드 금지하기 +## 추가 폼 필드 금지하기 { #forbid-extra-form-fields } -일부 특별한 사용 사례(흔하지는 않겠지만)에서는 Pydantic 모델에서 정의한 폼 필드를 **제한**하길 원할 수도 있습니다. 그리고 **추가** 필드를 **금지**할 수도 있습니다. +일부 특별한 사용 사례(아마도 흔하지는 않겠지만)에서는 Pydantic 모델에서 선언된 폼 필드로만 **제한**하길 원할 수도 있습니다. 그리고 **추가** 필드를 **금지**할 수도 있습니다. /// note | 참고 @@ -46,7 +46,7 @@ $ pip install python-multipart /// -Pydantic의 모델 구성을 사용하여 추가(`extra`) 필드를 금지(`forbid`)할 수 있습니다: +Pydantic의 모델 구성을 사용하여 `extra` 필드를 `forbid`할 수 있습니다: {* ../../docs_src/request_form_models/tutorial002_an_py39.py hl[12] *} @@ -73,6 +73,6 @@ Pydantic의 모델 구성을 사용하여 추가(`extra`) 필드를 금지(`forb } ``` -## 요약 +## 요약 { #summary } Pydantic 모델을 사용하여 FastAPI에서 폼 필드를 선언할 수 있습니다. 😎 diff --git a/docs/ko/docs/tutorial/request-forms-and-files.md b/docs/ko/docs/tutorial/request-forms-and-files.md index dc1bda21a..a5309b5c0 100644 --- a/docs/ko/docs/tutorial/request-forms-and-files.md +++ b/docs/ko/docs/tutorial/request-forms-and-files.md @@ -1,37 +1,41 @@ -# 폼 및 파일 요청 +# 폼 및 파일 요청 { #request-forms-and-files } -`File` 과 `Form` 을 사용하여 파일과 폼을 함께 정의할 수 있습니다. +`File` 과 `Form` 을 사용하여 파일과 폼 필드를 동시에 정의할 수 있습니다. /// info | 정보 -파일과 폼 데이터를 함께, 또는 각각 업로드하기 위해 먼저 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>를 설치해야합니다. +업로드된 파일 및/또는 폼 데이터를 받으려면 먼저 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>를 설치해야 합니다. -예 ) `pip install python-multipart`. +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고, 활성화한 다음 설치해야 합니다. 예: + +```console +$ pip install python-multipart +``` /// -## `File` 및 `Form` 업로드 +## `File` 및 `Form` 임포트 { #import-file-and-form } -{* ../../docs_src/request_forms_and_files/tutorial001.py hl[1] *} +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[3] *} -## `File` 및 `Form` 매개변수 정의 +## `File` 및 `Form` 매개변수 정의 { #define-file-and-form-parameters } `Body` 및 `Query`와 동일한 방식으로 파일과 폼의 매개변수를 생성합니다: -{* ../../docs_src/request_forms_and_files/tutorial001.py hl[8] *} +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[10:12] *} -파일과 폼 필드는 폼 데이터 형식으로 업로드되어 파일과 폼 필드로 전달됩니다. +파일과 폼 필드는 폼 데이터로 업로드되며, 파일과 폼 필드를 받게 됩니다. -어떤 파일들은 `bytes`로, 또 어떤 파일들은 `UploadFile`로 선언할 수 있습니다. +또한 일부 파일은 `bytes`로, 일부 파일은 `UploadFile`로 선언할 수 있습니다. /// warning | 경고 -다수의 `File`과 `Form` 매개변수를 한 *경로 작동*에 선언하는 것이 가능하지만, 요청의 본문이 `application/json`가 아닌 `multipart/form-data`로 인코딩 되기 때문에 JSON으로 받아야하는 `Body` 필드를 함께 선언할 수는 없습니다. +다수의 `File`과 `Form` 매개변수를 한 *경로 처리*에 선언하는 것이 가능하지만, 요청의 본문이 `application/json`가 아닌 `multipart/form-data`로 인코딩되기 때문에 JSON으로 받기를 기대하는 `Body` 필드를 함께 선언할 수는 없습니다. -이는 **FastAPI**의 한계가 아니라, HTTP 프로토콜에 의한 것입니다. +이는 **FastAPI**의 한계가 아니라, HTTP 프로토콜의 일부입니다. /// -## 요약 +## 요약 { #recap } 하나의 요청으로 데이터와 파일들을 받아야 할 경우 `File`과 `Form`을 함께 사용하기 바랍니다. diff --git a/docs/ko/docs/tutorial/request-forms.md b/docs/ko/docs/tutorial/request-forms.md index 5ca17b0d6..584cbba35 100644 --- a/docs/ko/docs/tutorial/request-forms.md +++ b/docs/ko/docs/tutorial/request-forms.md @@ -1,4 +1,4 @@ -# 폼 데이터 +# 폼 데이터 { #form-data } JSON 대신 폼 필드를 받아야 하는 경우 `Form`을 사용할 수 있습니다. @@ -14,13 +14,13 @@ $ pip install python-multipart /// -## `Form` 임포트하기 +## `Form` 임포트하기 { #import-form } `fastapi`에서 `Form`을 임포트합니다: {* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} -## `Form` 매개변수 정의하기 +## `Form` 매개변수 정의하기 { #define-form-parameters } `Body` 또는 `Query`와 동일한 방식으로 폼 매개변수를 만듭니다: @@ -28,7 +28,7 @@ $ pip install python-multipart 예를 들어, OAuth2 사양을 사용할 수 있는 방법 중 하나("패스워드 플로우"라고 함)로 `username`과 `password`를 폼 필드로 보내야 합니다. -<abbr title="specification">사양</abbr>에서는 필드 이름이 `username` 및 `password`로 정확하게 명명되어야 하고, JSON이 아닌 폼 필드로 전송해야 합니다. +<abbr title="specification">spec</abbr>에서는 필드 이름이 `username` 및 `password`로 정확하게 명명되어야 하고, JSON이 아닌 폼 필드로 전송해야 합니다. `Form`을 사용하면 유효성 검사, 예제, 별칭(예: `username` 대신 `user-name`) 등을 포함하여 `Body`(및 `Query`, `Path`, `Cookie`)와 동일한 구성을 선언할 수 있습니다. @@ -44,7 +44,7 @@ $ pip install python-multipart /// -## "폼 필드"에 대해 +## "폼 필드"에 대해 { #about-form-fields } HTML 폼(`<form></form>`)이 데이터를 서버로 보내는 방식은 일반적으로 해당 데이터에 대해 "특수" 인코딩을 사용하며, 이는 JSON과 다릅니다. @@ -56,19 +56,18 @@ HTML 폼(`<form></form>`)이 데이터를 서버로 보내는 방식은 일반 그러나 폼에 파일이 포함된 경우, `multipart/form-data`로 인코딩합니다. 다음 장에서 파일 처리에 대해 읽을 겁니다. - -이러한 인코딩 및 폼 필드에 대해 더 읽고 싶다면, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><code>POST</code>에 대한 <abbr title="Mozilla Developer Network">MDN</a> 웹 문서를 참조하세요. +이러한 인코딩 및 폼 필드에 대해 더 읽고 싶다면, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><code>POST</code>에 대한 <abbr title="Mozilla Developer Network">MDN</abbr> 웹 문서를 참조하세요</a>. /// /// warning | 경고 -*경로 작업*에서 여러 `Form` 매개변수를 선언할 수 있지만, JSON으로 수신할 것으로 예상되는 `Body` 필드와 함께 선언할 수 없습니다. 요청 본문은 `application/json` 대신에 `application/x-www-form-urlencoded`를 사용하여 인코딩되기 때문입니다. +*경로 처리*에서 여러 `Form` 매개변수를 선언할 수 있지만, JSON으로 수신할 것으로 예상되는 `Body` 필드와 함께 선언할 수 없습니다. 요청 본문은 `application/json` 대신에 `application/x-www-form-urlencoded`를 사용하여 인코딩되기 때문입니다. 이는 **FastAPI**의 제한 사항이 아니며 HTTP 프로토콜의 일부입니다. /// -## 요약 +## 요약 { #recap } 폼 데이터 입력 매개변수를 선언하려면 `Form`을 사용하세요. diff --git a/docs/ko/docs/tutorial/response-model.md b/docs/ko/docs/tutorial/response-model.md index a71d649f9..6246ed9ad 100644 --- a/docs/ko/docs/tutorial/response-model.md +++ b/docs/ko/docs/tutorial/response-model.md @@ -1,81 +1,173 @@ -# 응답 모델 +# 응답 모델 - 반환 타입 { #response-model-return-type } -어떤 *경로 작동*이든 매개변수 `response_model`를 사용하여 응답을 위한 모델을 선언할 수 있습니다: +*경로 처리 함수*의 **반환 타입**을 어노테이션하여 응답에 사용될 타입을 선언할 수 있습니다. + +함수 **매개변수**에서 입력 데이터를 위해 사용하는 것과 동일하게 **타입 어노테이션**을 사용할 수 있으며, Pydantic 모델, 리스트, 딕셔너리, 정수/불리언 같은 스칼라 값 등을 사용할 수 있습니다. + +{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} + +FastAPI는 이 반환 타입을 사용하여: + +* 반환된 데이터를 **검증**합니다. + * 데이터가 유효하지 않다면(예: 필드가 누락된 경우), 이는 *여러분의* 앱 코드가 깨져서 의도한 값을 반환하지 못한다는 의미이며, 잘못된 데이터를 반환하는 대신 서버 오류를 반환합니다. 이렇게 하면 여러분과 클라이언트는 기대한 데이터와 데이터 형태를 받는다는 것을 확실히 할 수 있습니다. +* OpenAPI *경로 처리*의 응답에 **JSON Schema**를 추가합니다. + * 이는 **자동 문서**에서 사용됩니다. + * 또한 자동 클라이언트 코드 생성 도구에서도 사용됩니다. + +하지만 가장 중요한 것은: + +* 반환 타입에 정의된 내용으로 출력 데이터를 **제한하고 필터링**합니다. + * 이는 특히 **보안**에 매우 중요합니다. 아래에서 더 자세히 살펴보겠습니다. + +## `response_model` 매개변수 { #response-model-parameter } + +타입 선언이 말하는 것과 정확히 일치하지 않는 데이터를 반환해야 하거나 반환하고 싶은 경우가 있습니다. + +예를 들어, **딕셔너리**나 데이터베이스 객체를 **반환**하고 싶지만, **Pydantic 모델로 선언**하고 싶을 수 있습니다. 이렇게 하면 Pydantic 모델이 반환한 객체(예: 딕셔너리나 데이터베이스 객체)에 대해 데이터 문서화, 검증 등 모든 작업을 수행합니다. + +반환 타입 어노테이션을 추가했다면, 도구와 에디터가 함수가 선언한 타입(예: Pydantic 모델)과 다른 타입(예: dict)을 반환하고 있다는 (올바른) 오류로 불평할 것입니다. + +그런 경우에는 반환 타입 대신 *경로 처리 데코레이터*의 매개변수 `response_model`을 사용할 수 있습니다. + +`response_model` 매개변수는 모든 *경로 처리*에서 사용할 수 있습니다: * `@app.get()` * `@app.post()` * `@app.put()` * `@app.delete()` -* 기타. +* 등. -{* ../../docs_src/response_model/tutorial001.py hl[17] *} +{* ../../docs_src/response_model/tutorial001_py310.py hl[17,22,24:27] *} /// note | 참고 -`response_model`은 "데코레이터" 메소드(`get`, `post`, 등)의 매개변수입니다. 모든 매개변수들과 본문(body)처럼 *경로 작동 함수*가 아닙니다. +`response_model`은 "데코레이터" 메서드(`get`, `post` 등)의 매개변수입니다. 모든 매개변수와 body처럼, *경로 처리 함수*의 매개변수가 아닙니다. /// -Pydantic 모델 어트리뷰트를 선언한 것과 동일한 타입을 수신하므로 Pydantic 모델이 될 수 있지만, `List[Item]`과 같이 Pydantic 모델들의 `list`일 수도 있습니다. +`response_model`은 Pydantic 모델 필드에 선언하는 것과 동일한 타입을 받습니다. 따라서 Pydantic 모델이 될 수도 있고, `List[Item]`처럼 Pydantic 모델의 `list`가 될 수도 있습니다. -FastAPI는 이 `response_model`를 사용하여: +FastAPI는 이 `response_model`을 사용해 데이터 문서화, 검증 등을 수행하고, 또한 출력 데이터를 타입 선언에 맞게 **변환하고 필터링**합니다. -* 출력 데이터를 타입 선언으로 변환. -* 데이터 검증. -* OpenAPI *경로 작동*의 응답에 JSON 스키마 추가. -* 자동 생성 문서 시스템에 사용. +/// tip | 팁 -하지만 가장 중요한 것은: +에디터, mypy 등에서 엄격한 타입 체크를 사용하고 있다면, 함수 반환 타입을 `Any`로 선언할 수 있습니다. -* 해당 모델의 출력 데이터 제한. 이것이 얼마나 중요한지 아래에서 볼 것입니다. - -/// note | 기술 세부사항 - -응답 모델은 함수의 타입 어노테이션 대신 이 매개변수로 선언하는데, 경로 함수가 실제 응답 모델을 반환하지 않고 `dict`, 데이터베이스 객체나 기타 다른 모델을 `response_model`을 사용하여 필드 제한과 직렬화를 수행하고 반환할 수 있기 때문입니다 +이렇게 하면 에디터에 의도적으로 어떤 값이든 반환한다고 알려줍니다. 하지만 FastAPI는 여전히 `response_model`을 사용하여 데이터 문서화, 검증, 필터링 등을 수행합니다. /// -## 동일한 입력 데이터 반환 +### `response_model` 우선순위 { #response-model-priority } -여기서 우리는 평문 비밀번호를 포함하는 `UserIn` 모델을 선언합니다: +반환 타입과 `response_model`을 둘 다 선언하면, `response_model`이 우선순위를 가지며 FastAPI에서 사용됩니다. -{* ../../docs_src/response_model/tutorial002.py hl[9,11] *} +이렇게 하면 응답 모델과 다른 타입을 반환하는 경우에도 에디터와 mypy 같은 도구에서 사용할 올바른 타입 어노테이션을 함수에 추가할 수 있습니다. 그리고 동시에 FastAPI가 `response_model`을 사용하여 데이터 검증, 문서화 등을 수행하게 할 수도 있습니다. + +또한 `response_model=None`을 사용해 해당 *경로 처리*에 대한 응답 모델 생성을 비활성화할 수도 있습니다. 이는 유효한 Pydantic 필드가 아닌 것들에 대해 타입 어노테이션을 추가하는 경우에 필요할 수 있으며, 아래 섹션 중 하나에서 예시를 볼 수 있습니다. + +## 동일한 입력 데이터 반환 { #return-the-same-input-data } + +여기서는 평문 비밀번호를 포함하는 `UserIn` 모델을 선언합니다: + +{* ../../docs_src/response_model/tutorial002_py310.py hl[7,9] *} + +/// info | 정보 + +`EmailStr`을 사용하려면 먼저 <a href="https://github.com/JoshData/python-email-validator" class="external-link" target="_blank">`email-validator`</a>를 설치하세요. + +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고, 활성화한 다음 설치해야 합니다. 예를 들어: + +```console +$ pip install email-validator +``` + +또는 다음과 같이: + +```console +$ pip install "pydantic[email]" +``` + +/// 그리고 이 모델을 사용하여 입력을 선언하고 같은 모델로 출력을 선언합니다: -{* ../../docs_src/response_model/tutorial002.py hl[17:18] *} +{* ../../docs_src/response_model/tutorial002_py310.py hl[16] *} 이제 브라우저가 비밀번호로 사용자를 만들 때마다 API는 응답으로 동일한 비밀번호를 반환합니다. -이 경우, 사용자가 스스로 비밀번호를 발신했기 때문에 문제가 되지 않을 수 있습니다. +이 경우, 동일한 사용자가 비밀번호를 보내는 것이므로 문제가 되지 않을 수도 있습니다. -그러나 동일한 모델을 다른 *경로 작동*에서 사용할 경우, 모든 클라이언트에게 사용자의 비밀번호를 발신할 수 있습니다. +하지만 동일한 모델을 다른 *경로 처리*에서 사용하면, 모든 클라이언트에게 사용자의 비밀번호를 보내게 될 수도 있습니다. /// danger | 위험 -절대로 사용자의 평문 비밀번호를 저장하거나 응답으로 발신하지 마십시오. +모든 주의사항을 알고 있으며 무엇을 하는지 정확히 알고 있지 않다면, 이런 방식으로 사용자의 평문 비밀번호를 저장하거나 응답으로 보내지 마세요. /// -## 출력 모델 추가 +## 출력 모델 추가 { #add-an-output-model } -대신 평문 비밀번호로 입력 모델을 만들고 해당 비밀번호 없이 출력 모델을 만들 수 있습니다: +대신 평문 비밀번호를 포함하는 입력 모델과, 비밀번호가 없는 출력 모델을 만들 수 있습니다: -{* ../../docs_src/response_model/tutorial003.py hl[9,11,16] *} +{* ../../docs_src/response_model/tutorial003_py310.py hl[9,11,16] *} -여기서 *경로 작동 함수*가 비밀번호를 포함하는 동일한 입력 사용자를 반환할지라도: +여기서 *경로 처리 함수*가 비밀번호를 포함하는 동일한 입력 사용자를 반환하더라도: -{* ../../docs_src/response_model/tutorial003.py hl[24] *} +{* ../../docs_src/response_model/tutorial003_py310.py hl[24] *} -...`response_model`을 `UserOut` 모델로 선언했기 때문에 비밀번호를 포함하지 않습니다: +...비밀번호를 포함하지 않는 `UserOut` 모델로 `response_model`을 선언했습니다: -{* ../../docs_src/response_model/tutorial003.py hl[22] *} +{* ../../docs_src/response_model/tutorial003_py310.py hl[22] *} -따라서 **FastAPI**는 출력 모델에서 선언하지 않은 모든 데이터를 (Pydantic을 사용하여) 필터링합니다. +따라서 **FastAPI**는 출력 모델에 선언되지 않은 모든 데이터를 (Pydantic을 사용하여) 필터링합니다. -## 문서에서 보기 +### `response_model` 또는 반환 타입 { #response-model-or-return-type } -자동 생성 문서를 보면 입력 모델과 출력 모델이 각자의 JSON 스키마를 가지고 있음을 확인할 수 있습니다: +이 경우 두 모델이 서로 다르기 때문에, 함수 반환 타입을 `UserOut`으로 어노테이션하면 에디터와 도구는 서로 다른 클래스인데 잘못된 타입을 반환하고 있다고 불평할 것입니다. + +그래서 이 예제에서는 `response_model` 매개변수로 선언해야 합니다. + +...하지만 아래를 계속 읽으면 이를 극복하는 방법을 볼 수 있습니다. + +## 반환 타입과 데이터 필터링 { #return-type-and-data-filtering } + +이전 예제에서 계속해 봅시다. 함수에 **하나의 타입으로 어노테이션**을 하고 싶지만, 함수에서 실제로는 **더 많은 데이터**를 포함하는 것을 반환할 수 있길 원했습니다. + +FastAPI가 응답 모델을 사용해 데이터를 계속 **필터링**하길 원합니다. 그래서 함수가 더 많은 데이터를 반환하더라도, 응답에는 응답 모델에 선언된 필드만 포함되게 합니다. + +이전 예제에서는 클래스가 달랐기 때문에 `response_model` 매개변수를 써야 했습니다. 하지만 이는 에디터와 도구가 함수 반환 타입을 체크해 주는 지원을 받지 못한다는 뜻이기도 합니다. + +하지만 대부분 이런 작업이 필요한 경우에는, 이 예제처럼 모델로 일부 데이터를 **필터링/제거**하길 원하는 경우가 많습니다. + +그리고 그런 경우에는 클래스와 상속을 사용하여 함수 **타입 어노테이션**을 활용해 에디터/도구에서 더 나은 지원을 받으면서도 FastAPI의 **데이터 필터링**을 유지할 수 있습니다. + +{* ../../docs_src/response_model/tutorial003_01_py310.py hl[7:10,13:14,18] *} + +이를 통해 이 코드는 타입 관점에서 올바르므로 에디터와 mypy 등의 도구 지원을 받을 수 있고, 동시에 FastAPI의 데이터 필터링도 받을 수 있습니다. + +이게 어떻게 동작할까요? 확인해 봅시다. 🤓 + +### 타입 어노테이션과 도구 지원 { #type-annotations-and-tooling } + +먼저 에디터, mypy 및 기타 도구가 이를 어떻게 보는지 살펴봅시다. + +`BaseUser`는 기본 필드를 가집니다. 그리고 `UserIn`은 `BaseUser`를 상속하고 `password` 필드를 추가하므로, 두 모델의 모든 필드를 포함하게 됩니다. + +함수 반환 타입을 `BaseUser`로 어노테이션하지만, 실제로는 `UserIn` 인스턴스를 반환합니다. + +에디터, mypy 및 기타 도구는 이에 대해 불평하지 않습니다. 타이핑 관점에서 `UserIn`은 `BaseUser`의 서브클래스이므로, `BaseUser`인 어떤 것이 기대되는 곳에서는 *유효한* 타입이기 때문입니다. + +### FastAPI 데이터 필터링 { #fastapi-data-filtering } + +이제 FastAPI는 반환 타입을 보고, 여러분이 반환하는 값이 해당 타입에 선언된 필드 **만** 포함하도록 보장합니다. + +FastAPI는 Pydantic을 내부적으로 여러 방식으로 사용하여, 클래스 상속의 동일한 규칙이 반환 데이터 필터링에는 적용되지 않도록 합니다. 그렇지 않으면 기대한 것보다 훨씬 더 많은 데이터를 반환하게 될 수도 있습니다. + +이렇게 하면 **도구 지원**이 있는 타입 어노테이션과 **데이터 필터링**이라는 두 가지 장점을 모두 얻을 수 있습니다. + +## 문서에서 보기 { #see-it-in-the-docs } + +자동 생성 문서를 보면 입력 모델과 출력 모델이 각자의 JSON Schema를 가지고 있음을 확인할 수 있습니다: <img src="/img/tutorial/response-model/image01.png"> @@ -83,29 +175,73 @@ FastAPI는 이 `response_model`를 사용하여: <img src="/img/tutorial/response-model/image02.png"> -## 응답 모델 인코딩 매개변수 +## 기타 반환 타입 어노테이션 { #other-return-type-annotations } + +유효한 Pydantic 필드가 아닌 것을 반환하면서도, 도구(에디터, mypy 등)가 제공하는 지원을 받기 위해 함수에 어노테이션을 달아두는 경우가 있을 수 있습니다. + +### 응답을 직접 반환하기 { #return-a-response-directly } + +가장 흔한 경우는 [고급 문서에서 나중에 설명하는 대로 Response를 직접 반환하는 것](../advanced/response-directly.md){.internal-link target=_blank}입니다. + +{* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} + +이 간단한 경우는 반환 타입 어노테이션이 `Response` 클래스(또는 그 서브클래스)이기 때문에 FastAPI에서 자동으로 처리됩니다. + +그리고 `RedirectResponse`와 `JSONResponse`는 모두 `Response`의 서브클래스이므로, 타입 어노테이션이 올바르기 때문에 도구들도 만족합니다. + +### Response 서브클래스 어노테이션 { #annotate-a-response-subclass } + +타입 어노테이션에 `Response`의 서브클래스를 사용할 수도 있습니다: + +{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} + +이는 `RedirectResponse`가 `Response`의 서브클래스이기 때문에 동작하며, FastAPI가 이 간단한 경우를 자동으로 처리합니다. + +### 유효하지 않은 반환 타입 어노테이션 { #invalid-return-type-annotations } + +하지만 유효한 Pydantic 타입이 아닌 다른 임의의 객체(예: 데이터베이스 객체)를 반환하고, 함수에서 그렇게 어노테이션하면, FastAPI는 그 타입 어노테이션으로부터 Pydantic 응답 모델을 만들려고 시도하다가 실패합니다. + +또한, 유효한 Pydantic 타입이 아닌 타입이 하나 이상 포함된 여러 타입 간의 <abbr title='여러 타입 간 union은 "이 타입들 중 아무거나"를 의미합니다.'>union</abbr>이 있는 경우에도 동일합니다. 예를 들어, 아래는 실패합니다 💥: + +{* ../../docs_src/response_model/tutorial003_04_py310.py hl[8] *} + +...이는 타입 어노테이션이 Pydantic 타입이 아니고, 단일 `Response` 클래스/서브클래스도 아니며, `Response`와 `dict` 간 union(둘 중 아무거나)이기 때문에 실패합니다. + +### 응답 모델 비활성화 { #disable-response-model } + +위 예제에서 이어서, FastAPI가 수행하는 기본 데이터 검증, 문서화, 필터링 등을 원하지 않을 수 있습니다. + +하지만 에디터나 타입 체커(예: mypy) 같은 도구 지원을 받기 위해 함수에 반환 타입 어노테이션은 유지하고 싶을 수도 있습니다. + +이 경우 `response_model=None`으로 설정하여 응답 모델 생성을 비활성화할 수 있습니다: + +{* ../../docs_src/response_model/tutorial003_05_py310.py hl[7] *} + +그러면 FastAPI는 응답 모델 생성을 건너뛰며, FastAPI 애플리케이션에 영향을 주지 않고 필요한 반환 타입 어노테이션을 어떤 것이든 사용할 수 있습니다. 🤓 + +## 응답 모델 인코딩 매개변수 { #response-model-encoding-parameters } 응답 모델은 아래와 같이 기본값을 가질 수 있습니다: -{* ../../docs_src/response_model/tutorial004.py hl[11,13:14] *} +{* ../../docs_src/response_model/tutorial004_py310.py hl[9,11:12] *} -* `description: Optional[str] = None`은 기본값으로 `None`을 갖습니다. +* `description: Union[str, None] = None` (또는 Python 3.10에서 `str | None = None`)은 기본값으로 `None`을 갖습니다. * `tax: float = 10.5`는 기본값으로 `10.5`를 갖습니다. -* `tags: List[str] = []` 빈 리스트의 기본값으로: `[]`. +* `tags: List[str] = []`는 기본값으로 빈 리스트 `[]`를 갖습니다. -그러나 실제로 저장되지 않았을 경우 결과에서 값을 생략하고 싶을 수 있습니다. +하지만 실제로 저장되지 않았을 경우 결과에서 이를 생략하고 싶을 수 있습니다. 예를 들어, NoSQL 데이터베이스에 많은 선택적 속성이 있는 모델이 있지만, 기본값으로 가득 찬 매우 긴 JSON 응답을 보내고 싶지 않습니다. -### `response_model_exclude_unset` 매개변수 사용 +### `response_model_exclude_unset` 매개변수 사용 { #use-the-response-model-exclude-unset-parameter } -*경로 작동 데코레이터* 매개변수를 `response_model_exclude_unset=True`로 설정 할 수 있습니다: +*경로 처리 데코레이터* 매개변수 `response_model_exclude_unset=True`로 설정할 수 있습니다: -{* ../../docs_src/response_model/tutorial004.py hl[24] *} +{* ../../docs_src/response_model/tutorial004_py310.py hl[22] *} -이러한 기본값은 응답에 포함되지 않고 실제로 설정된 값만 포함됩니다. +그러면 이러한 기본값은 응답에 포함되지 않고, 실제로 설정된 값만 포함됩니다. -따라서 해당 *경로 작동*에 ID가 `foo`인 항목(items)을 요청으로 보내면 (기본값을 제외한) 응답은 다음과 같습니다: +따라서 ID가 `foo`인 항목에 대해 해당 *경로 처리*로 요청을 보내면, (기본값을 제외한) 응답은 다음과 같습니다: ```JSON { @@ -116,24 +252,18 @@ FastAPI는 이 `response_model`를 사용하여: /// info | 정보 -FastAPI는 이를 위해 Pydantic 모델의 `.dict()`의 <a href="https://docs.pydantic.dev/latest/concepts/serialization/#modeldict" class="external-link" target="_blank"> `exclude_unset` 매개변수</a>를 사용합니다. - -/// - -/// info | 정보 - -아래 또한 사용할 수 있습니다: +다음도 사용할 수 있습니다: * `response_model_exclude_defaults=True` * `response_model_exclude_none=True` -<a href="https://docs.pydantic.dev/latest/concepts/serialization/#modeldict" class="external-link" target="_blank">Pydantic 문서</a>에서 `exclude_defaults` 및 `exclude_none`에 대해 설명한 대로 사용할 수 있습니다. +`exclude_defaults` 및 `exclude_none`에 대해 <a href="https://docs.pydantic.dev/1.10/usage/exporting_models/#modeldict" class="external-link" target="_blank">Pydantic 문서</a>에 설명된 대로 사용할 수 있습니다. /// -#### 기본값이 있는 필드를 갖는 값의 데이터 +#### 기본값이 있는 필드에 값이 있는 데이터 { #data-with-values-for-fields-with-defaults } -하지만 모델의 필드가 기본값이 있어도 ID가 `bar`인 항목(items)처럼 데이터가 값을 갖는다면: +하지만 ID가 `bar`인 항목처럼, 기본값이 있는 모델의 필드에 값이 있다면: ```Python hl_lines="3 5" { @@ -144,12 +274,11 @@ FastAPI는 이를 위해 Pydantic 모델의 `.dict()`의 <a href="https://docs.p } ``` -응답에 해당 값들이 포함됩니다. +응답에 포함됩니다. -#### 기본값과 동일한 값을 갖는 데이터 +#### 기본값과 동일한 값을 갖는 데이터 { #data-with-the-same-values-as-the-defaults } -If the data has the same values as the default ones, like the item with ID `baz`: -ID가 `baz`인 항목(items)처럼 기본값과 동일한 값을 갖는다면: +데이터가 ID가 `baz`인 항목처럼 기본값과 동일한 값을 갖는다면: ```Python hl_lines="3 5-6" { @@ -161,37 +290,37 @@ ID가 `baz`인 항목(items)처럼 기본값과 동일한 값을 갖는다면: } ``` -`description`, `tax` 그리고 `tags`가 기본값과 같더라도 (기본값에서 가져오는 대신) 값들이 명시적으로 설정되었다는 것을 인지할 정도로 FastAPI는 충분히 똑똑합니다(사실, Pydantic이 충분히 똑똑합니다). +FastAPI는 충분히 똑똑해서(사실, Pydantic이 충분히 똑똑합니다) `description`, `tax`, `tags`가 기본값과 동일하더라도, 기본값에서 가져온 것이 아니라 명시적으로 설정되었다는 것을 알아냅니다. -따라서 JSON 스키마에 포함됩니다. +그래서 JSON 응답에 포함됩니다. /// tip | 팁 -`None` 뿐만 아니라 다른 어떤 것도 기본값이 될 수 있습니다. +기본값은 `None`뿐만 아니라 어떤 것이든 될 수 있습니다. 리스트(`[]`), `float`인 `10.5` 등이 될 수 있습니다. /// -### `response_model_include` 및 `response_model_exclude` +### `response_model_include` 및 `response_model_exclude` { #response-model-include-and-response-model-exclude } -*경로 작동 데코레이터* 매개변수 `response_model_include` 및 `response_model_exclude`를 사용할 수 있습니다. +*경로 처리 데코레이터* 매개변수 `response_model_include` 및 `response_model_exclude`를 사용할 수도 있습니다. -이들은 포함(나머지 생략)하거나 제외(나머지 포함) 할 어트리뷰트의 이름과 `str`의 `set`을 받습니다. +이들은 포함(나머지 생략)하거나 제외(나머지 포함)할 어트리뷰트 이름을 담은 `str`의 `set`을 받습니다. -Pydantic 모델이 하나만 있고 출력에서 ​​일부 데이터를 제거하려는 경우 빠른 지름길로 사용할 수 있습니다. +Pydantic 모델이 하나만 있고 출력에서 일부 데이터를 제거하려는 경우, 빠른 지름길로 사용할 수 있습니다. /// tip | 팁 -하지만 이러한 매개변수 대신 여러 클래스를 사용하여 위 아이디어를 사용하는 것을 추천합니다. +하지만 이러한 매개변수 대신, 위에서 설명한 것처럼 여러 클래스를 사용하는 것을 여전히 권장합니다. -이는 일부 어트리뷰트를 생략하기 위해 `response_model_include` 또는 `response_model_exclude`를 사용하더라도 앱의 OpenAPI(및 문서)가 생성한 JSON 스키마가 여전히 전체 모델에 대한 스키마이기 때문입니다. +이는 일부 어트리뷰트를 생략하기 위해 `response_model_include` 또는 `response_model_exclude`를 사용하더라도, 앱의 OpenAPI(및 문서)에 생성되는 JSON Schema가 여전히 전체 모델에 대한 스키마이기 때문입니다. -비슷하게 작동하는 `response_model_by_alias` 역시 마찬가지로 적용됩니다. +비슷하게 동작하는 `response_model_by_alias`에도 동일하게 적용됩니다. /// -{* ../../docs_src/response_model/tutorial005.py hl[31,37] *} +{* ../../docs_src/response_model/tutorial005_py310.py hl[29,35] *} /// tip | 팁 @@ -201,14 +330,14 @@ Pydantic 모델이 하나만 있고 출력에서 ​​일부 데이터를 제 /// -#### `set` 대신 `list` 사용하기 +#### `set` 대신 `list` 사용하기 { #using-lists-instead-of-sets } -`list` 또는 `tuple` 대신 `set`을 사용하는 법을 잊었더라도, FastAPI는 `set`으로 변환하고 정상적으로 작동합니다: +`set`을 쓰는 것을 잊고 `list`나 `tuple`을 대신 사용하더라도, FastAPI는 이를 `set`으로 변환하므로 올바르게 동작합니다: -{* ../../docs_src/response_model/tutorial006.py hl[31,37] *} +{* ../../docs_src/response_model/tutorial006_py310.py hl[29,35] *} -## 요약 +## 요약 { #recap } -응답 모델을 정의하고 개인정보가 필터되는 것을 보장하기 위해 *경로 작동 데코레이터*의 매개변수 `response_model`을 사용하세요. +응답 모델을 정의하고 특히 개인정보가 필터링되도록 보장하려면 *경로 처리 데코레이터*의 매개변수 `response_model`을 사용하세요. 명시적으로 설정된 값만 반환하려면 `response_model_exclude_unset`을 사용하세요. diff --git a/docs/ko/docs/tutorial/response-status-code.md b/docs/ko/docs/tutorial/response-status-code.md index bcaf7843b..257f6a8d6 100644 --- a/docs/ko/docs/tutorial/response-status-code.md +++ b/docs/ko/docs/tutorial/response-status-code.md @@ -1,18 +1,18 @@ -# 응답 상태 코드 +# 응답 상태 코드 { #response-status-code } -응답 모델과 같은 방법으로, 어떤 *경로 작동*이든 `status_code` 매개변수를 사용하여 응답에 대한 HTTP 상태 코드를 선언할 수 있습니다. +응답 모델을 지정하는 것과 같은 방법으로, 어떤 *경로 처리*에서든 `status_code` 매개변수를 사용하여 응답에 사용할 HTTP 상태 코드를 선언할 수도 있습니다: * `@app.get()` * `@app.post()` * `@app.put()` * `@app.delete()` -* 기타 +* 등 -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} /// note | 참고 -`status_code` 는 "데코레이터" 메소드(`get`, `post` 등)의 매개변수입니다. 모든 매개변수들과 본문처럼 *경로 작동 함수*가 아닙니다. +`status_code` 는 "데코레이터" 메소드(`get`, `post` 등)의 매개변수입니다. 모든 매개변수들과 본문처럼 *경로 처리 함수*가 아닙니다. /// @@ -20,75 +20,75 @@ /// info | 정보 -`status_code` 는 파이썬의 `http.HTTPStatus` 와 같은 `IntEnum` 을 입력받을 수도 있습니다. +`status_code` 는 파이썬의 <a href="https://docs.python.org/3/library/http.html#http.HTTPStatus" class="external-link" target="_blank">`http.HTTPStatus`</a> 와 같은 `IntEnum` 을 입력받을 수도 있습니다. /// `status_code` 매개변수는: * 응답에서 해당 상태 코드를 반환합니다. -* 상태 코드를 OpenAPI 스키마(및 사용자 인터페이스)에 문서화 합니다. +* 상태 코드를 OpenAPI 스키마(따라서, 사용자 인터페이스에도)에 문서화합니다: -<img src="https://fastapi.tiangolo.com/img/tutorial/response-status-code/image01.png"> +<img src="/img/tutorial/response-status-code/image01.png"> /// note | 참고 -어떤 응답 코드들은 해당 응답에 본문이 없다는 것을 의미하기도 합니다 (다음 항목 참고). +일부 응답 코드(다음 섹션 참고)는 응답에 본문이 없다는 것을 나타냅니다. -이에 따라 FastAPI는 응답 본문이 없음을 명시하는 OpenAPI를 생성합니다. +FastAPI는 이를 알고 있으며, 응답 본문이 없다고 명시하는 OpenAPI 문서를 생성합니다. /// -## HTTP 상태 코드에 대하여 +## HTTP 상태 코드에 대하여 { #about-http-status-codes } /// note | 참고 -만약 HTTP 상태 코드에 대하여 이미 알고있다면, 다음 항목으로 넘어가십시오. +만약 HTTP 상태 코드가 무엇인지 이미 알고 있다면, 다음 섹션으로 넘어가세요. /// -HTTP는 세자리의 숫자 상태 코드를 응답의 일부로 전송합니다. +HTTP에서는 응답의 일부로 3자리 숫자 상태 코드를 보냅니다. -이 상태 코드들은 각자를 식별할 수 있도록 지정된 이름이 있으나, 중요한 것은 숫자 코드입니다. +이 상태 코드들은 이를 식별할 수 있도록 이름이 연결되어 있지만, 중요한 부분은 숫자입니다. 요약하자면: -* `1xx` 상태 코드는 "정보"용입니다. 이들은 직접적으로는 잘 사용되지는 않습니다. 이 상태 코드를 갖는 응답들은 본문을 가질 수 없습니다. -* **`2xx`** 상태 코드는 "성공적인" 응답을 위해 사용됩니다. 가장 많이 사용되는 유형입니다. - * `200` 은 디폴트 상태 코드로, 모든 것이 "성공적임"을 의미합니다. - * 다른 예로는 `201` "생성됨"이 있습니다. 일반적으로 데이터베이스에 새로운 레코드를 생성한 후 사용합니다. - * 단, `204` "내용 없음"은 특별한 경우입니다. 이것은 클라이언트에게 반환할 내용이 없는 경우 사용합니다. 따라서 응답은 본문을 가질 수 없습니다. -* **`3xx`** 상태 코드는 "리다이렉션"용입니다. 본문을 가질 수 없는 `304` "수정되지 않음"을 제외하고, 이 상태 코드를 갖는 응답에는 본문이 있을 수도, 없을 수도 있습니다. -* **`4xx`** 상태 코드는 "클라이언트 오류" 응답을 위해 사용됩니다. 이것은 아마 가장 많이 사용하게 될 두번째 유형입니다. - * 일례로 `404` 는 "찾을 수 없음" 응답을 위해 사용합니다. - * 일반적인 클라이언트 오류의 경우 `400` 을 사용할 수 있습니다. -* `5xx` 상태 코드는 서버 오류에 사용됩니다. 이것들을 직접 사용할 일은 거의 없습니다. 응용 프로그램 코드나 서버의 일부에서 문제가 발생하면 자동으로 이들 상태 코드 중 하나를 반환합니다. +* `100 - 199` 는 "정보"용입니다. 직접 사용할 일은 거의 없습니다. 이 상태 코드를 갖는 응답은 본문을 가질 수 없습니다. +* **`200 - 299`** 는 "성공적인" 응답을 위한 것입니다. 가장 많이 사용하게 될 유형입니다. + * `200` 은 기본 상태 코드로, 모든 것이 "OK"임을 의미합니다. + * 다른 예로는 `201` "생성됨"이 있습니다. 일반적으로 데이터베이스에 새 레코드를 생성한 후 사용합니다. + * 특별한 경우로 `204` "내용 없음"이 있습니다. 이 응답은 클라이언트에게 반환할 내용이 없을 때 사용되며, 따라서 응답은 본문을 가지면 안 됩니다. +* **`300 - 399`** 는 "리다이렉션"용입니다. 이 상태 코드를 갖는 응답은 본문이 있을 수도 없을 수도 있으며, 본문이 없어야 하는 `304` "수정되지 않음"을 제외합니다. +* **`400 - 499`** 는 "클라이언트 오류" 응답을 위한 것입니다. 아마 두 번째로 가장 많이 사용하게 될 유형입니다. + * 예를 들어 `404` 는 "찾을 수 없음" 응답을 위해 사용합니다. + * 클라이언트의 일반적인 오류에는 `400` 을 그냥 사용할 수 있습니다. +* `500 - 599` 는 서버 오류에 사용됩니다. 직접 사용할 일은 거의 없습니다. 애플리케이션 코드의 일부나 서버에서 문제가 발생하면 자동으로 이들 상태 코드 중 하나를 반환합니다. /// tip | 팁 -각각의 상태 코드와 이들이 의미하는 내용에 대해 더 알고싶다면 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> HTTP 상태 코드에 관한 문서</a> 를 확인하십시오. +각 상태 코드와 어떤 코드가 어떤 용도인지 더 알고 싶다면 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr>의 HTTP 상태 코드에 관한 문서</a>를 확인하세요. /// -## 이름을 기억하는 쉬운 방법 +## 이름을 기억하는 쉬운 방법 { #shortcut-to-remember-the-names } -상기 예시 참고: +이전 예시를 다시 확인해보겠습니다: -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} -`201` 은 "생성됨"를 의미하는 상태 코드입니다. +`201` 은 "생성됨"을 위한 상태 코드입니다. -하지만 모든 상태 코드들이 무엇을 의미하는지 외울 필요는 없습니다. +하지만 각각의 코드가 무엇을 의미하는지 외울 필요는 없습니다. `fastapi.status` 의 편의 변수를 사용할 수 있습니다. -{* ../../docs_src/response_status_code/tutorial002.py hl[1,6] *} +{* ../../docs_src/response_status_code/tutorial002_py39.py hl[1,6] *} -이것은 단순히 작업을 편리하게 하기 위한 것으로, HTTP 상태 코드와 동일한 번호를 갖고있지만, 이를 사용하면 편집기의 자동완성 기능을 사용할 수 있습니다: +이것들은 단지 편의를 위한 것으로, 동일한 숫자를 갖고 있지만, 이를 통해 편집기의 자동완성 기능으로 찾을 수 있습니다: -<img src="https://fastapi.tiangolo.com/img/tutorial/response-status-code/image02.png"> +<img src="/img/tutorial/response-status-code/image02.png"> -/// note | 기술적 세부사항 +/// note | 기술 세부사항 `from starlette import status` 역시 사용할 수 있습니다. @@ -96,6 +96,6 @@ HTTP는 세자리의 숫자 상태 코드를 응답의 일부로 전송합니다 /// -## 기본값 변경 +## 기본값 변경 { #changing-the-default } -추후 여기서 선언하는 기본 상태 코드가 아닌 다른 상태 코드를 반환하는 방법을 [숙련된 사용자 지침서](https://fastapi.tiangolo.com/ko/advanced/response-change-status-code/){.internal-link target=_blank}에서 확인할 수 있습니다. +나중에 [고급 사용자 지침서](../advanced/response-change-status-code.md){.internal-link target=_blank}에서, 여기서 선언하는 기본값과 다른 상태 코드를 반환하는 방법을 확인할 수 있습니다. diff --git a/docs/ko/docs/tutorial/schema-extra-example.md b/docs/ko/docs/tutorial/schema-extra-example.md index 77e94db72..b2b54836a 100644 --- a/docs/ko/docs/tutorial/schema-extra-example.md +++ b/docs/ko/docs/tutorial/schema-extra-example.md @@ -1,43 +1,21 @@ -# 요청 예제 데이터 선언 +# 요청 예제 데이터 선언 { #declare-request-example-data } 여러분의 앱이 받을 수 있는 데이터 예제를 선언할 수 있습니다. 여기 이를 위한 몇가지 방식이 있습니다. -## Pydantic 모델 속 추가 JSON 스키마 데이터 +## Pydantic 모델 속 추가 JSON 스키마 데이터 { #extra-json-schema-data-in-pydantic-models } 생성된 JSON 스키마에 추가될 Pydantic 모델을 위한 `examples`을 선언할 수 있습니다. -//// tab | Pydantic v2 - {* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *} - -//// - 추가 정보는 있는 그대로 해당 모델의 **JSON 스키마** 결과에 추가되고, API 문서에서 사용합니다. -//// tab | Pydantic v2 - -Pydantic 버전 2에서 <a href="https://docs.pydantic.dev/latest/usage/model_config/" class="external-link" target="_blank">Pydantic 공식 문서: Model Config</a>에 나와 있는 것처럼 `dict`를 받는 `model_config` 어트리뷰트를 사용할 것입니다. +<a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">Pydantic 문서: Configuration</a>에 설명된 것처럼 `dict`를 받는 `model_config` 어트리뷰트를 사용할 수 있습니다. `"json_schema_extra"`를 생성된 JSON 스키마에서 보여주고 싶은 별도의 데이터와 `examples`를 포함하는 `dict`으로 설정할 수 있습니다. -//// - -//// tab | Pydantic v1 - -Pydantic v1에서 <a href="https://docs.pydantic.dev/1.10/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic 공식 문서: Schema customization</a>에서 설명하는 것처럼, 내부 클래스인 `Config`와 `schema_extra`를 사용할 것입니다. - -`schema_extra`를 생성된 JSON 스키마에서 보여주고 싶은 별도의 데이터와 `examples`를 포함하는 `dict`으로 설정할 수 있습니다. - -//// - /// tip | 팁 JSON 스키마를 확장하고 여러분의 별도의 자체 데이터를 추가하기 위해 같은 기술을 사용할 수 있습니다. @@ -52,19 +30,19 @@ JSON 스키마를 확장하고 여러분의 별도의 자체 데이터를 추가 그 전에는, 하나의 예제만 가능한 `example` 키워드만 지원했습니다. 이는 아직 OpenAPI 3.1.0에서 지원하지만, 지원이 종료될 것이며 JSON 스키마 표준에 포함되지 않습니다. 그렇기에 `example`을 `examples`으로 이전하는 것을 추천합니다. 🤓 -이 문서 끝에 더 많은 읽을거리가 있습니다. +이 페이지 끝에서 더 많은 내용을 읽을 수 있습니다. /// -## `Field` 추가 인자 +## `Field` 추가 인자 { #field-additional-arguments } Pydantic 모델과 같이 `Field()`를 사용할 때 추가적인 `examples`를 선언할 수 있습니다: {* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *} -## JSON Schema에서의 `examples` - OpenAPI +## JSON Schema에서의 `examples` - OpenAPI { #examples-in-json-schema-openapi } -이들 중에서 사용합니다: +다음 중 하나를 사용할 때: * `Path()` * `Query()` @@ -74,45 +52,45 @@ Pydantic 모델과 같이 `Field()`를 사용할 때 추가적인 `examples`를 * `Form()` * `File()` -**OpenAPI**의 **JSON 스키마**에 추가될 부가적인 정보를 포함한 `examples` 모음을 선언할 수 있습니다. +**OpenAPI** 안의 **JSON 스키마**에 추가될 부가적인 정보를 포함한 `examples` 모음을 선언할 수도 있습니다. -### `examples`를 포함한 `Body` +### `examples`를 포함한 `Body` { #body-with-examples } 여기, `Body()`에 예상되는 예제 데이터 하나를 포함한 `examples`를 넘겼습니다: {* ../../docs_src/schema_extra_example/tutorial003_an_py310.py hl[22:29] *} -### 문서 UI 예시 +### 문서 UI 예시 { #example-in-the-docs-ui } 위의 어느 방법과 함께라면 `/docs`에서 다음과 같이 보일 것입니다: <img src="/img/tutorial/body-fields/image01.png"> -### 다중 `examples`를 포함한 `Body` +### 다중 `examples`를 포함한 `Body` { #body-with-multiple-examples } 물론 여러 `examples`를 넘길 수 있습니다: {* ../../docs_src/schema_extra_example/tutorial004_an_py310.py hl[23:38] *} -이와 같이 하면 이 예제는 그 본문 데이터를 위한 내부 **JSON 스키마**의 일부가 될 것입니다. +이와 같이 하면 예제들은 그 본문 데이터의 내부 **JSON 스키마**의 일부가 될 것입니다. 그럼에도 불구하고, 지금 <abbr title="2023-08-26">이 문서를 작성하는 시간</abbr>에, 문서 UI를 보여주는 역할을 맡은 Swagger UI는 **JSON 스키마** 속 데이터를 위한 여러 예제의 표현을 지원하지 않습니다. 하지만 해결 방안을 밑에서 읽어보세요. -### OpenAPI-특화 `examples` +### OpenAPI-특화 `examples` { #openapi-specific-examples } -**JSON 스키마**가 `examples`를 지원하기 전 부터, OpenAPI는 `examples`이라 불리는 다른 필드를 지원해 왔습니다. +**JSON 스키마**가 `examples`를 지원하기 전부터 OpenAPI는 `examples`이라 불리는 다른 필드를 지원해 왔습니다. -이 **OpenAPI-특화** `examples`는 OpenAPI 명세서의 다른 구역으로 들어갑니다. 각 JSON 스키마 내부가 아니라 **각 *경로 작동* 세부 정보**에 포함됩니다. +이 **OpenAPI-특화** `examples`는 OpenAPI 명세서의 다른 구역으로 들어갑니다. 각 JSON 스키마 내부가 아니라 **각 *경로 처리* 세부 정보**에 포함됩니다. 그리고 Swagger UI는 이 특정한 `examples` 필드를 한동안 지원했습니다. 그래서, 이를 다른 **문서 UI에 있는 예제**를 **표시**하기 위해 사용할 수 있습니다. -이 OpenAPI-특화 필드인 `examples`의 형태는 (`list`대신에) **다중 예제**가 포함된 `dict`이며, 각각의 별도 정보 또한 **OpenAPI**에 추가될 것입니다. +이 OpenAPI-특화 필드인 `examples`의 형태는 (`list` 대신에) **다중 예제**가 포함된 `dict`이며, 각각의 별도 정보 또한 **OpenAPI**에 추가될 것입니다. -이는 OpenAPI에 포함된 JSON 스키마 안으로 포함되지 않으며, *경로 작동*에 직접적으로 포함됩니다. +이는 OpenAPI에 포함된 각 JSON 스키마 안으로 포함되지 않으며, *경로 처리*에 직접적으로 포함됩니다. -### `openapi_examples` 매개변수 사용하기 +### `openapi_examples` 매개변수 사용하기 { #using-the-openapi-examples-parameter } -다음 예시 속에 OpenAPI-특화 `examples`를 FastAPI 안에서 매개변수 `openapi_examples` 매개변수와 함께 선언할 수 있습니다: +다음에 대해 FastAPI에서 매개변수 `openapi_examples`로 OpenAPI-특화 `examples`를 선언할 수 있습니다: * `Path()` * `Query()` @@ -122,26 +100,26 @@ Pydantic 모델과 같이 `Field()`를 사용할 때 추가적인 `examples`를 * `Form()` * `File()` -`dict`의 키가 또 다른 `dict`인 각 예제와 값을 구별합니다. +`dict`의 키는 각 예제를 식별하고, 각 값은 또 다른 `dict`입니다. -각각의 특정 `examples` 속 `dict` 예제는 다음을 포함할 수 있습니다: +`examples` 안의 각 특정 예제 `dict`에는 다음이 포함될 수 있습니다: * `summary`: 예제에 대한 짧은 설명문. * `description`: 마크다운 텍스트를 포함할 수 있는 긴 설명문. * `value`: 실제로 보여지는 예시, 예를 들면 `dict`. -* `externalValue`: `value`의 대안이며 예제를 가르키는 URL. 비록 `value`처럼 많은 도구를 지원하지 못할 수 있습니다. +* `externalValue`: `value`의 대안이며 예제를 가리키는 URL. 비록 `value`처럼 많은 도구를 지원하지 못할 수 있습니다. 이를 다음과 같이 사용할 수 있습니다: {* ../../docs_src/schema_extra_example/tutorial005_an_py310.py hl[23:49] *} -### 문서 UI에서의 OpenAPI 예시 +### 문서 UI에서의 OpenAPI 예시 { #openapi-examples-in-the-docs-ui } -`Body()`에 추가된 `openapi_examples`를 포함한 `/docs`는 다음과 같이 보일 것입니다: +`Body()`에 `openapi_examples`가 추가되면 `/docs`는 다음과 같이 보일 것입니다: <img src="/img/tutorial/body-fields/image02.png"> -## 기술적 세부 사항 +## 기술적 세부 사항 { #technical-details } /// tip | 팁 @@ -167,12 +145,12 @@ JSON 스키마는 `examples`를 가지고 있지 않았고, 따라서 OpenAPI는 OpenAPI는 또한 `example`과 `examples` 필드를 명세서의 다른 부분에 추가했습니다: -* <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-object" class="external-link" target="_blank">`(명세서에 있는) Parameter Object`</a>는 FastAPI의 다음 기능에서 쓰였습니다: +* <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-object" class="external-link" target="_blank">`Parameter Object` (명세서에 있는)</a>는 FastAPI의 다음 기능에서 쓰였습니다: * `Path()` * `Query()` * `Header()` * `Cookie()` -* <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#media-type-object" class="external-link" target="_blank">(명세서에 있는)`Media Type Object`속 `content`에 있는 `Request Body Object`</a>는 FastAPI의 다음 기능에서 쓰였습니다: +* <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#media-type-object" class="external-link" target="_blank">`Request Body Object`, `Media Type Object` (명세서에 있는)의 `content` 필드에 있는</a>는 FastAPI의 다음 기능에서 쓰였습니다: * `Body()` * `File()` * `Form()` @@ -183,15 +161,15 @@ OpenAPI는 또한 `example`과 `examples` 필드를 명세서의 다른 부분 /// -### JSON 스키마의 `examples` 필드 +### JSON 스키마의 `examples` 필드 { #json-schemas-examples-field } -하지만, 후에 JSON 스키마는 <a href="https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.5" class="external-link" target="_blank">`examples`</a>필드를 명세서의 새 버전에 추가했습니다. +하지만, 후에 JSON 스키마는 <a href="https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.5" class="external-link" target="_blank">`examples`</a> 필드를 명세서의 새 버전에 추가했습니다. 그리고 새로운 OpenAPI 3.1.0은 이 새로운 `examples` 필드가 포함된 최신 버전 (JSON 스키마 2020-12)을 기반으로 했습니다. -이제 새로운 `examples` 필드는 이전의 단일 (그리고 커스텀) `example` 필드보다 우선되며, `example`은 사용하지 않는 것이 좋습니다. +그리고 이제 이 새로운 `examples` 필드는 이제 지원 중단된, 예전의 단일 (그리고 커스텀) `example` 필드보다 우선됩니다. -JSON 스키마의 새로운 `examples` 필드는 예제 속 **단순한 `list`**이며, (위에서 상술한 것처럼) OpenAPI의 다른 곳에 존재하는 dict으로 된 추가적인 메타데이터가 아닙니다. +JSON 스키마의 새로운 `examples` 필드는 예제의 **단순한 `list`**일 뿐이며, (위에서 상술한 것처럼) OpenAPI의 다른 곳에 존재하는 추가 메타데이터가 있는 dict가 아닙니다. /// info | 정보 @@ -201,24 +179,24 @@ JSON 스키마의 새로운 `examples` 필드는 예제 속 **단순한 `list`** /// -### Pydantic과 FastAPI `examples` +### Pydantic과 FastAPI `examples` { #pydantic-and-fastapi-examples } -`examples`를 Pydantic 모델 속에 추가할 때, `schema_extra` 혹은 `Field(examples=["something"])`를 사용하면 Pydantic 모델의 **JSON 스키마**에 해당 예시가 추가됩니다. +Pydantic 모델 안에 `examples`를 추가할 때, `schema_extra` 또는 `Field(examples=["something"])`를 사용하면 그 예제는 해당 Pydantic 모델의 **JSON 스키마**에 추가됩니다. 그리고 Pydantic 모델의 **JSON 스키마**는 API의 **OpenAPI**에 포함되고, 그 후 문서 UI 속에서 사용됩니다. -FastAPI 0.99.0 이전 버전에서 (0.99.0 이상 버전은 새로운 OpenAPI 3.1.0을 사용합니다), `example` 혹은 `examples`를 다른 유틸리티(`Query()`, `Body()` 등)와 함께 사용했을 때, 저러한 예시는 데이터를 설명하는 JSON 스키마에 추가되지 않으며 (심지어 OpenAPI의 자체 JSON 스키마에도 포함되지 않습니다), OpenAPI의 *경로 작동* 선언에 직접적으로 추가됩니다 (JSON 스키마를 사용하는 OpenAPI 부분 외에도). +FastAPI 0.99.0 이전 버전에서 (0.99.0 이상 버전은 새로운 OpenAPI 3.1.0을 사용합니다), 다른 유틸리티(`Query()`, `Body()` 등)와 함께 `example` 또는 `examples`를 사용했을 때, 그러한 예제는 그 데이터를 설명하는 JSON 스키마에 추가되지 않고 (OpenAPI 자체의 JSON 스키마에도 포함되지 않습니다), OpenAPI의 *경로 처리* 선언에 직접적으로 추가됩니다 (JSON 스키마를 사용하는 OpenAPI 부분 밖에서). -하지만 지금은 FastAPI 0.99.0 및 이후 버전에서는 JSON 스키마 2020-12를 사용하는 OpenAPI 3.1.0과 Swagger UI 5.0.0 및 이후 버전을 사용하며, 모든 것이 더 일관성을 띄고 예시는 JSON 스키마에 포함됩니다. +하지만 이제 FastAPI 0.99.0 및 이후 버전에서는 JSON 스키마 2020-12를 사용하는 OpenAPI 3.1.0과 Swagger UI 5.0.0 및 이후 버전을 사용하기 때문에, 모든 것이 더 일관성을 띄고 예제도 JSON 스키마에 포함됩니다. -### Swagger UI와 OpenAPI-특화 `examples` +### Swagger UI와 OpenAPI-특화 `examples` { #swagger-ui-and-openapi-specific-examples } -현재 (2023-08-26), Swagger UI가 다중 JSON 스키마 예시를 지원하지 않으며, 사용자는 다중 예시를 문서에 표시하는 방법이 없었습니다. +Swagger UI는 다중 JSON 스키마 예제를 지원하지 않았기 때문에(2023-08-26 기준), 사용자는 문서에 여러 예제를 표시할 방법이 없었습니다. -이를 해결하기 위해, FastAPI `0.103.0`은 새로운 매개변수인 `openapi_examples`를 포함하는 예전 **OpenAPI-특화** `examples` 필드를 선언하기 위한 **지원을 추가**했습니다. 🤓 +이를 해결하기 위해, FastAPI `0.103.0`은 새로운 매개변수인 `openapi_examples`로 동일한 예전 **OpenAPI-특화** `examples` 필드를 선언하는 **지원**을 추가했습니다. 🤓 -### 요약 +### 요약 { #summary } -저는 역사를 그다지 좋아하는 편이 아니라고 말하고는 했지만... "기술 역사" 강의를 가르치는 지금의 저를 보세요. +저는 역사를 그다지 좋아하는 편이 아니라고 말하고는 했지만... "기술 역사" 강의를 하는 지금의 저를 보세요. 😅 -요약하자면 **FastAPI 0.99.0 혹은 그 이상의 버전**으로 업그레이드하는 것은 많은 것들이 더 **쉽고, 일관적이며 직관적이게** 되며, 여러분은 이 모든 역사적 세부 사항을 알 필요가 없습니다. 😎 +요약하자면 **FastAPI 0.99.0 혹은 그 이상의 버전**으로 업그레이드하면, 많은 것들이 훨씬 더 **단순하고, 일관적이며 직관적**이 되며, 여러분은 이 모든 역사적 세부 사항을 알 필요가 없습니다. 😎 diff --git a/docs/ko/docs/tutorial/security/get-current-user.md b/docs/ko/docs/tutorial/security/get-current-user.md index 98ef3885e..f21a22b7a 100644 --- a/docs/ko/docs/tutorial/security/get-current-user.md +++ b/docs/ko/docs/tutorial/security/get-current-user.md @@ -1,105 +1,105 @@ -# 현재 사용자 가져오기 +# 현재 사용자 가져오기 { #get-current-user } -이전 장에서 (의존성 주입 시스템을 기반으로 한)보안 시스템은 *경로 작동 함수*에서 `str`로 `token`을 제공했습니다: +이전 장에서 (의존성 주입 시스템을 기반으로 한) 보안 시스템은 *경로 처리 함수*에 `str`로 `token`을 제공했습니다: -{* ../../docs_src/security/tutorial001.py hl[10] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} -그러나 아직도 유용하지 않습니다. +하지만 이는 여전히 그다지 유용하지 않습니다. -현재 사용자를 제공하도록 합시다. +현재 사용자를 제공하도록 해봅시다. -## 유저 모델 생성하기 +## 사용자 모델 생성하기 { #create-a-user-model } -먼저 Pydantic 유저 모델을 만들어 보겠습니다. +먼저 Pydantic 사용자 모델을 만들어 봅시다. -Pydantic을 사용하여 본문을 선언하는 것과 같은 방식으로 다른 곳에서 사용할 수 있습니다. +Pydantic을 사용해 본문을 선언하는 것과 같은 방식으로, 다른 곳에서도 어디서든 사용할 수 있습니다: -{* ../../docs_src/security/tutorial002.py hl[5,12:16] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *} -## `get_current_user` 의존성 생성하기 +## `get_current_user` 의존성 생성하기 { #create-a-get-current-user-dependency } 의존성 `get_current_user`를 만들어 봅시다. -의존성이 하위 의존성을 가질 수 있다는 것을 기억하십니까? +의존성이 하위 의존성을 가질 수 있다는 것을 기억하시나요? -`get_current_user`는 이전에 생성한 것과 동일한 `oauth2_scheme`과 종속성을 갖게 됩니다. +`get_current_user`는 이전에 생성한 것과 동일한 `oauth2_scheme`에 대한 의존성을 갖게 됩니다. -이전에 *경로 작동*에서 직접 수행했던 것과 동일하게 새 종속성 `get_current_user`는 하위 종속성 `oauth2_scheme`에서 `str`로 `token`을 수신합니다. +이전에 *경로 처리*에서 직접 수행했던 것과 동일하게, 새 의존성 `get_current_user`는 하위 의존성 `oauth2_scheme`로부터 `str`로 `token`을 받게 됩니다: -{* ../../docs_src/security/tutorial002.py hl[25] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *} -## 유저 가져오기 +## 사용자 가져오기 { #get-the-user } -`get_current_user`는 토큰을 `str`로 취하고 Pydantic `User` 모델을 반환하는 우리가 만든 (가짜) 유틸리티 함수를 사용합니다. +`get_current_user`는 우리가 만든 (가짜) 유틸리티 함수를 사용합니다. 이 함수는 `str`로 토큰을 받아 Pydantic `User` 모델을 반환합니다: -{* ../../docs_src/security/tutorial002.py hl[19:22,26:27] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *} -## 현재 유저 주입하기 +## 현재 사용자 주입하기 { #inject-the-current-user } -이제 *경로 작동*에서 `get_current_user`와 동일한 `Depends`를 사용할 수 있습니다. +이제 *경로 처리*에서 `get_current_user`와 함께 같은 `Depends`를 사용할 수 있습니다: -{* ../../docs_src/security/tutorial002.py hl[31] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} -Pydantic 모델인 `User`로 `current_user`의 타입을 선언하는 것을 알아야 합니다. +`current_user`의 타입을 Pydantic 모델 `User`로 선언한다는 점에 주목하세요. -이것은 모든 완료 및 타입 검사를 통해 함수 내부에서 우리를 도울 것입니다. +이는 함수 내부에서 자동 완성과 타입 체크에 도움을 줍니다. /// tip | 팁 -요청 본문도 Pydantic 모델로 선언된다는 것을 기억할 것입니다. +요청 본문도 Pydantic 모델로 선언된다는 것을 기억하실지도 모릅니다. -여기서 **FastAPI**는 `Depends`를 사용하고 있기 때문에 혼동되지 않습니다. +여기서 **FastAPI**는 `Depends`를 사용하고 있기 때문에 혼동하지 않습니다. /// /// check | 확인 -이 의존성 시스템이 설계된 방식은 모두 `User` 모델을 반환하는 다양한 의존성(다른 "의존적인")을 가질 수 있도록 합니다. +이 의존성 시스템이 설계된 방식은 모두 `User` 모델을 반환하는 서로 다른 의존성(서로 다른 "dependables")을 가질 수 있도록 합니다. -해당 타입의 데이터를 반환할 수 있는 의존성이 하나만 있는 것으로 제한되지 않습니다. +해당 타입의 데이터를 반환할 수 있는 의존성이 하나만 있어야 하는 것으로 제한되지 않습니다. /// -## 다른 모델 +## 다른 모델 { #other-models } -이제 *경로 작동 함수*에서 현재 사용자를 직접 가져올 수 있으며 `Depends`를 사용하여 **의존성 주입** 수준에서 보안 메커니즘을 처리할 수 있습니다. +이제 *경로 처리 함수*에서 현재 사용자를 직접 가져올 수 있으며, `Depends`를 사용해 **의존성 주입** 수준에서 보안 메커니즘을 처리할 수 있습니다. -그리고 보안 요구 사항에 대한 모든 모델 또는 데이터를 사용할 수 있습니다(이 경우 Pydantic 모델 `User`). +그리고 보안 요구 사항을 위해 어떤 모델이나 데이터든 사용할 수 있습니다(이 경우 Pydantic 모델 `User`). -그러나 일부 특정 데이터 모델, 클래스 또는 타입을 사용하도록 제한되지 않습니다. +하지만 특정 데이터 모델, 클래스 또는 타입만 사용해야 하는 것은 아닙니다. -모델에 `id`와 `email`이 있고 `username`이 없길 원하십니까? 맞습니다. 이들은 동일한 도구를 사용할 수 있습니다. +모델에 `id`와 `email`이 있고 `username`은 없게 하고 싶으신가요? 물론입니다. 같은 도구를 사용할 수 있습니다. -`str`만 갖고 싶습니까? 아니면 그냥 `dict`를 갖고 싶습니까? 아니면 데이터베이스 클래스 모델 인스턴스를 직접 갖고 싶습니까? 그들은 모두 같은 방식으로 작동합니다. +`str`만 갖고 싶으신가요? 아니면 `dict`만요? 또는 데이터베이스 클래스 모델 인스턴스를 직접 쓰고 싶으신가요? 모두 같은 방식으로 동작합니다. -실제로 애플리케이션에 로그인하는 사용자가 없지만 액세스 토큰만 있는 로봇, 봇 또는 기타 시스템이 있습니까? 다시 말하지만 모두 동일하게 작동합니다. +애플리케이션에 로그인하는 사용자는 없고, 액세스 토큰만 가진 로봇, 봇 또는 다른 시스템만 있나요? 이것도 마찬가지로 모두 동일하게 동작합니다. -애플리케이션에 필요한 모든 종류의 모델, 모든 종류의 클래스, 모든 종류의 데이터베이스를 사용하십시오. **FastAPI**는 의존성 주입 시스템을 다루었습니다. +애플리케이션에 필요한 어떤 종류의 모델, 어떤 종류의 클래스, 어떤 종류의 데이터베이스든 사용하세요. **FastAPI**는 의존성 주입 시스템으로 이를 지원합니다. -## 코드 사이즈 +## 코드 크기 { #code-size } -이 예는 장황해 보일 수 있습니다. 동일한 파일에서 보안, 데이터 모델, 유틸리티 기능 및 *경로 작동*을 혼합하고 있음을 염두에 두십시오. +이 예시는 장황해 보일 수 있습니다. 동일한 파일에서 보안, 데이터 모델, 유틸리티 함수 및 *경로 처리*를 섞어서 사용하고 있다는 점을 기억하세요. -그러나 이게 키포인트입니다. +하지만 여기 핵심이 있습니다. -보안과 종속성 주입 항목을 한 번만 작성하면 됩니다. +보안과 의존성 주입 관련 코드는 한 번만 작성합니다. -그리고 원하는 만큼 복잡하게 만들 수 있습니다. 그래도 유연성과 함께 한 곳에 한 번에 작성할 수 있습니다. +그리고 원하는 만큼 복잡하게 만들 수 있습니다. 그럼에도 여전히 한 번만, 한 곳에만 작성하면 됩니다. 유연성을 모두 유지하면서요. -그러나 동일한 보안 시스템을 사용하여 수천 개의 엔드포인트(*경로 작동*)를 가질 수 있습니다. +하지만 같은 보안 시스템을 사용해 수천 개의 엔드포인트(*경로 처리*)를 가질 수 있습니다. -그리고 그들 모두(또는 원하는 부분)는 이러한 의존성 또는 생성한 다른 의존성을 재사용하는 이점을 얻을 수 있습니다. +그리고 그들 모두(또는 원하는 일부)는 이러한 의존성 또는 여러분이 생성한 다른 의존성을 재사용하는 이점을 얻을 수 있습니다. -그리고 이 수천 개의 *경로 작동*은 모두 3줄 정도로 줄일 수 있습니다. +그리고 이 수천 개의 *경로 처리*는 3줄 정도로도 만들 수 있습니다: -{* ../../docs_src/security/tutorial002.py hl[30:32] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} -## 요약 +## 요약 { #recap } -이제 *경로 작동 함수*에서 현재 사용자를 직접 가져올 수 있습니다. +이제 *경로 처리 함수*에서 현재 사용자를 직접 가져올 수 있습니다. -우리는 이미 이들 사이에 있습니다. +우리는 이미 절반은 왔습니다. -사용자/클라이언트가 실제로 `username`과 `password`를 보내려면 *경로 작동*을 추가하기만 하면 됩니다. +사용자/클라이언트가 실제로 `username`과 `password`를 보내도록 하는 *경로 처리*만 추가하면 됩니다. -다음 장을 확인해 봅시다. +다음에 이어집니다. diff --git a/docs/ko/docs/tutorial/security/oauth2-jwt.md b/docs/ko/docs/tutorial/security/oauth2-jwt.md index 8d27856e8..907795ca4 100644 --- a/docs/ko/docs/tutorial/security/oauth2-jwt.md +++ b/docs/ko/docs/tutorial/security/oauth2-jwt.md @@ -1,36 +1,36 @@ -# 패스워드 해싱을 이용한 OAuth2, JWT 토큰을 사용하는 Bearer 인증 +# 패스워드(해싱 포함)를 사용하는 OAuth2, JWT 토큰을 사용하는 Bearer { #oauth2-with-password-and-hashing-bearer-with-jwt-tokens } -모든 보안 흐름을 구성했으므로, 이제 <abbr title="JSON Web Tokens">JWT</abbr> 토큰과 패스워드 해싱을 사용해 애플리케이션을 안전하게 만들 것입니다. +모든 보안 흐름을 구성했으므로, 이제 <abbr title="JSON Web Tokens">JWT</abbr> 토큰과 안전한 패스워드 해싱을 사용해 애플리케이션을 실제로 안전하게 만들겠습니다. -이 코드는 실제로 애플리케이션에서 패스워드를 해싱하여 DB에 저장하는 등의 작업에 활용할 수 있습니다. +이 코드는 실제로 애플리케이션에서 사용할 수 있으며, 패스워드 해시를 데이터베이스에 저장하는 등의 작업에 활용할 수 있습니다. -이전 장에 이어서 시작해 봅시다. +이전 장에서 멈춘 지점부터 시작해 내용을 확장해 나가겠습니다. -## JWT +## JWT 알아보기 { #about-jwt } -JWT 는 "JSON Web Tokens" 을 의미합니다. +JWT는 "JSON Web Tokens"를 의미합니다. -JSON 객체를 공백이 없는 긴 문자열로 인코딩하는 표준이며, 다음과 같은 형태입니다: +JSON 객체를 공백이 없는 길고 밀집된 문자열로 부호화하는 표준입니다. 다음과 같은 형태입니다: ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ``` -JWT는 암호화되지 않아 누구든지 토큰에서 정보를 복원할 수 있습니다. +암호화된 것이 아니므로, 누구나 내용에서 정보를 복원할 수 있습니다. -하지만 JWT는 서명되어 있습니다. 그래서 자신이 발급한 토큰을 받았을 때, 실제로 자신이 발급한게 맞는지 검증할 수 있습니다. +하지만 서명되어 있습니다. 따라서 자신이 발급한 토큰을 받았을 때, 실제로 자신이 발급한 것이 맞는지 검증할 수 있습니다. -만료 기간이 일주일인 토큰을 발행했다고 가정해 봅시다. 다음 날 사용자가 토큰을 가져왔을 때, 그 사용자가 시스템에 여전히 로그인되어 있다는 것을 알 수 있습니다. +예를 들어 만료 기간이 1주일인 토큰을 생성할 수 있습니다. 그리고 사용자가 다음 날 토큰을 가지고 돌아오면, 그 사용자가 시스템에 여전히 로그인되어 있다는 것을 알 수 있습니다. -일주일 뒤에는 토큰이 만료될 것이고, 사용자는 인가되지 않아 새 토큰을 받기 위해 다시 로그인해야 할 것입니다. 만약 사용자(또는 제3자)가 토큰을 수정하거나 만료일을 변경하면, 서명이 일치하지 않기 때문에 알아챌 수 있을 것입니다. +1주일 뒤에는 토큰이 만료되고 사용자는 인가되지 않으므로 새 토큰을 받기 위해 다시 로그인해야 합니다. 그리고 사용자(또는 제3자)가 만료 시간을 바꾸기 위해 토큰을 수정하려고 하면, 서명이 일치하지 않기 때문에 이를 알아챌 수 있습니다. -만약 JWT 토큰을 다뤄보고, 작동 방식도 알아보고 싶다면 <a href="https://jwt.io/" class="external-link" target="_blank">https://jwt.io</a> 을 확인하십시오. +JWT 토큰을 직접 다뤄보고 동작 방식을 확인해보고 싶다면 <a href="https://jwt.io/" class="external-link" target="_blank">https://jwt.io</a>를 확인하십시오. -## `PyJWT` 설치 +## `PyJWT` 설치 { #install-pyjwt } -파이썬으로 JWT 토큰을 생성하고 검증하려면 `PyJWT` 를 설치해야 합니다. +Python에서 JWT 토큰을 생성하고 검증하려면 `PyJWT`를 설치해야 합니다. -[가상환경](../../virtual-environments.md){.internal-link target=_blank} 을 만들고 활성화한 다음 `pyjwt` 를 설치하십시오: +[가상환경](../../virtual-environments.md){.internal-link target=_blank}을 만들고 활성화한 다음 `pyjwt`를 설치하십시오: <div class="termy"> @@ -42,77 +42,77 @@ $ pip install pyjwt </div> -/// info | 참고 +/// info -RSA나 ECDSA 같은 전자 서명 알고리즘을 사용하려면, `pyjwt[crypto]`라는 암호화 라이브러리 의존성을 설치해야 합니다. +RSA나 ECDSA 같은 전자 서명 알고리즘을 사용할 계획이라면, cryptography 라이브러리 의존성인 `pyjwt[crypto]`를 설치해야 합니다. -더 자세한 내용은 <a href="https://pyjwt.readthedocs.io/en/latest/installation.html" class="external-link" target="_blank">PyJWT 설치</a> 에서 확인할 수 있습니다. +자세한 내용은 <a href="https://pyjwt.readthedocs.io/en/latest/installation.html" class="external-link" target="_blank">PyJWT Installation docs</a>에서 확인할 수 있습니다. /// -## 패스워드 해싱 +## 패스워드 해싱 { #password-hashing } -"해싱(Hashing)"은 어떤 내용(여기서는 패스워드)을 해석할 수 없는 일련의 바이트 집합(단순 문자열)으로 변환하는 것을 의미합니다. +"해싱(Hashing)"은 어떤 내용(여기서는 패스워드)을 알아볼 수 없는 바이트 시퀀스(그냥 문자열)로 변환하는 것을 의미합니다. -동일한 내용(똑같은 패스워드)을 해싱하면 동일한 문자열을 얻습니다. +정확히 같은 내용(정확히 같은 패스워드)을 넣으면 정확히 같은 알아볼 수 없는 문자열이 나옵니다. -하지만 그 문자열을 다시 패스워드로 되돌릴 수는 없습니다. +하지만 그 알아볼 수 없는 문자열에서 다시 패스워드로 되돌릴 수는 없습니다. -### 패스워드를 해싱하는 이유 +### 패스워드 해싱을 사용하는 이유 { #why-use-password-hashing } -데이터베이스를 탈취당하더라도, 침입자는 사용자의 평문 패스워드 대신 해시 값만 얻을 수 있습니다. +데이터베이스를 탈취당하더라도, 침입자는 사용자의 평문 패스워드 대신 해시만 얻게 됩니다. -따라서 침입자는 훔친 사용자 패스워드를 다른 시스템에서 활용할 수 없습니다. (대다수 사용자가 여러 시스템에서 동일한 패스워드를 사용하기 때문에 평문 패스워드가 유출되면 위험합니다.) +따라서 침입자는 그 패스워드를 다른 시스템에서 사용해 보려고 시도할 수 없습니다(많은 사용자가 어디서나 같은 패스워드를 사용하므로, 이는 위험합니다). -## `passlib` 설치 +## `pwdlib` 설치 { #install-pwdlib } -PassLib는 패스워드 해시를 다루는 훌륭한 파이썬 패키지입니다. +pwdlib는 패스워드 해시를 다루기 위한 훌륭한 Python 패키지입니다. -많은 안전한 해시 알고리즘과 도구들을 지원합니다. +많은 안전한 해싱 알고리즘과 이를 다루기 위한 유틸리티를 지원합니다. -추천하는 알고리즘은 "Bcrypt"입니다. +추천 알고리즘은 "Argon2"입니다. -[가상환경](../../virtual-environments.md){.internal-link target=_blank} 을 만들고 활성화한 다음 PassLib와 Bcrypt를 설치하십시오: +[가상환경](../../virtual-environments.md){.internal-link target=_blank}을 만들고 활성화한 다음 Argon2와 함께 pwdlib를 설치하십시오: <div class="termy"> ```console -$ pip install "passlib[bcrypt]" +$ pip install "pwdlib[argon2]" ---> 100% ``` </div> -/// tip | 팁 +/// tip -`passlib`를 사용하여, **Django**, **Flask** 의 보안 플러그인이나 다른 도구로 생성한 패스워드를 읽을 수 있도록 설정할 수도 있습니다. +`pwdlib`를 사용하면 **Django**, **Flask** 보안 플러그인 또는 다른 여러 도구로 생성한 패스워드를 읽을 수 있도록 설정할 수도 있습니다. -예를 들자면, FastAPI 애플리케이션과 Django 애플리케이션이 같은 데이터베이스에서 데이터를 공유할 수 있습니다. 또는 같은 데이터베이스를 사용하여 Django 애플리케이션을 점진적으로 마이그레이션 할 수도 있습니다. +따라서 예를 들어, FastAPI 애플리케이션과 Django 애플리케이션이 같은 데이터베이스에서 동일한 데이터를 공유할 수 있습니다. 또는 같은 데이터베이스를 사용하면서 Django 애플리케이션을 점진적으로 마이그레이션할 수도 있습니다. -그리고 사용자는 FastAPI 애플리케이션과 Django 애플리케이션에 동시에 로그인할 수 있습니다. +그리고 사용자는 Django 앱 또는 **FastAPI** 앱에서 동시에 로그인할 수 있습니다. /// -## 패스워드의 해시와 검증 +## 패스워드 해시 및 검증 { #hash-and-verify-the-passwords } -필요한 도구를 `passlib`에서 임포트합니다. +`pwdlib`에서 필요한 도구를 임포트합니다. -PassLib "컨텍스트(context)"를 생성합니다. 이것은 패스워드를 해싱하고 검증하는데 사용합니다. +권장 설정으로 PasswordHash 인스턴스를 생성합니다. 이는 패스워드를 해싱하고 검증하는 데 사용됩니다. -/// tip | 팁 +/// tip -PassLib 컨텍스트는 다양한 해싱 알고리즘을 사용할 수 있는 기능을 제공하며, 더 이상 사용이 권장되지 않는 오래된 해싱 알고리즘을 검증하는 기능도 포함되어 있습니다. +pwdlib는 bcrypt 해싱 알고리즘도 지원하지만 레거시 알고리즘은 포함하지 않습니다. 오래된 해시로 작업해야 한다면 passlib 라이브러리를 사용하는 것을 권장합니다. -예를 들어, 다른 시스템(Django 같은)에서 생성한 패스워드를 읽고 검증할 수 있으며, 새로운 패스워드를 Bcrypt 같은 다른 알고리즘으로 해싱할 수도 있습니다. +예를 들어, 다른 시스템(Django 같은)에서 생성한 패스워드를 읽고 검증하되, 새 패스워드는 Argon2나 Bcrypt 같은 다른 알고리즘으로 해싱하도록 할 수 있습니다. -그리고 동시에 그런 모든 알고리즘과 호환성을 유지합니다. +그리고 동시에 그 모든 것과 호환되게 만들 수 있습니다. /// 사용자로부터 받은 패스워드를 해싱하는 유틸리티 함수를 생성합니다. -그리고 받은 패스워드가 저장된 해시와 일치하는지 검증하는 또 다른 유틸리티 함수도 생성합니다. +그리고 받은 패스워드가 저장된 해시와 일치하는지 검증하는 또 다른 유틸리티도 생성합니다. 그리고 사용자를 인증하고 반환하는 또 다른 함수도 생성합니다. @@ -120,17 +120,17 @@ PassLib 컨텍스트는 다양한 해싱 알고리즘을 사용할 수 있는 /// note -새로운 (가짜) 데이터베이스 `fake_users_db`를 확인하면, 해시 처리된 패스워드가 어떻게 생겼는지 볼 수 있습니다: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. +새로운 (가짜) 데이터베이스 `fake_users_db`를 확인하면, 이제 해시 처리된 패스워드가 어떻게 생겼는지 볼 수 있습니다: `"$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc"`. /// -## JWT 토큰 처리 +## JWT 토큰 처리 { #handle-jwt-tokens } -설치된 모듈을 임포트 합니다. +설치된 모듈을 임포트합니다. -JWT 토큰 서명에 사용될 임의의 비밀키를 생성합니다. +JWT 토큰을 서명하는 데 사용할 임의의 비밀 키를 생성합니다. -안전한 임의의 비밀키를 생성하려면 다음 명령어를 사용하십시오: +안전한 임의의 비밀 키를 생성하려면 다음 명령을 사용하십시오: <div class="termy"> @@ -142,67 +142,67 @@ $ openssl rand -hex 32 </div> -그리고 생성한 비밀키를 복사해 변수 `SECRET_KEY`에 대입합니다. (이 예제의 변수 값을 그대로 사용하지 마십시오.) +그리고 출력 결과를 변수 `SECRET_KEY`에 복사합니다(예제의 값을 사용하지 마십시오). -JWT 토큰을 서명하는 데 사용될 알고리즘을 위한 변수 `ALGORITHM` 을 생성하고 `"HS256"` 으로 설정합니다. +JWT 토큰을 서명하는 데 사용될 알고리즘을 위한 변수 `ALGORITHM`을 생성하고 `"HS256"`으로 설정합니다. -토큰 만료 기간을 위한 변수를 생성합니다. +토큰 만료를 위한 변수를 생성합니다. -응답을 위한 토큰 엔드포인트에 사용될 Pydantic 모델을 정의합니다. +응답을 위해 토큰 엔드포인트에서 사용될 Pydantic 모델을 정의합니다. 새 액세스 토큰을 생성하기 위한 유틸리티 함수를 생성합니다. {* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *} -## 의존성 수정 +## 의존성 업데이트 { #update-the-dependencies } -`get_current_user` 함수를 이전과 동일한 토큰을 받도록 수정하되, 이번에는 JWT 토큰을 사용하도록 합니다. +`get_current_user`가 이전과 동일한 토큰을 받도록 업데이트하되, 이번에는 JWT 토큰을 사용하도록 합니다. -받은 토큰을 디코딩하여 검증한 후 현재 사용자를 반환합니다. +받은 토큰을 디코딩하고 검증한 뒤 현재 사용자를 반환합니다. -토큰이 유효하지 않다면 HTTP 오류를 반환합니다. +토큰이 유효하지 않다면 즉시 HTTP 오류를 반환합니다. {* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *} -## `/token` 경로 작업 수정 +## `/token` *경로 처리* 업데이트 { #update-the-token-path-operation } -토큰의 만료 시각을 설정하기 위해 `timedelta` 를 생성합니다. +토큰의 만료 시간으로 `timedelta`를 생성합니다. 실제 JWT 액세스 토큰을 생성하여 반환합니다. {* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *} -### JWT "주체(subject)" `sub`에 대한 기술 세부 사항 +### JWT "주체(subject)" `sub`에 대한 기술 세부사항 { #technical-details-about-the-jwt-subject-sub } -JWT 명세에 따르면 토큰의 주체를 포함하는 `sub`라는 키가 있습니다. +JWT 명세에 따르면 토큰의 주체를 담는 `sub` 키가 있습니다. -사용 여부는 선택사항이지만, 사용자의 식별 정보를 저장할 수 있으므로 여기서는 이를 사용합니다. +선택적으로 사용할 수 있지만, 여기에 사용자 식별 정보를 넣게 되므로 여기서는 이를 사용합니다. -JWT는 사용자를 식별하고 사용자가 API를 직접 사용할 수 있도록 허용하는 것 외에도 다른 용도로 사용될 수도 있습니다. +JWT는 사용자를 식별하고 사용자가 API에서 직접 작업을 수행할 수 있도록 허용하는 것 외에도 다른 용도로 사용될 수 있습니다. -예를 들어 "자동차"나 "블로그 게시물"을 식별하는 데 사용할 수 있습니다. +예를 들어 "자동차"나 "블로그 게시물"을 식별할 수 있습니다. -그리고 "자동차를 운전하다"나 "블로그 게시물을 수정하다"처럼 해당 엔터티에 대한 권한을 추가할 수 있습니다. +그런 다음 해당 엔터티에 대한 권한(자동차의 경우 "drive", 블로그의 경우 "edit" 등)을 추가할 수 있습니다. -그 후 이 JWT 토큰을 사용자(또는 봇)에게 제공하면, 그들은 계정을 따로 만들 필요 없이 API가 생성한 JWT 토큰만으로 작업(자동차 운전 또는 블로그 게시물 편집)을 수행할 수 있습니다. +그리고 그 JWT 토큰을 사용자(또는 봇)에게 제공하면, 계정이 없어도 API가 생성한 JWT 토큰만으로 그 동작들(자동차 운전, 블로그 편집)을 수행할 수 있습니다. -이러한 개념을 활용하면 JWT는 훨씬 더 복잡한 시나리오에도 사용할 수 있습니다. +이러한 아이디어를 활용하면 JWT는 훨씬 더 정교한 시나리오에도 사용될 수 있습니다. -이 경우 여러 엔터티가 동일한 ID를 가질 수 있습니다. 예를 들어 foo라는 ID를 가진 사용자, 자동차, 블로그 게시물이 있을 수 있습니다. +그런 경우 여러 엔터티가 동일한 ID(예: `foo`)를 가질 수도 있습니다(사용자 `foo`, 자동차 `foo`, 블로그 게시물 `foo`). -그래서 ID 충돌을 방지하기 위해, 사용자의 JWT 토큰을 생성할 때 접두사로 `sub` 키를 추가할 수 있습니다. 예를 들어 `username:` 을 붙이는 방식입니다. 이 예제에서는 `sub` 값이 `username:johndoe`이 될 수 있습니다. +따라서 ID 충돌을 방지하기 위해, 사용자에 대한 JWT 토큰을 생성할 때 `sub` 키의 값에 접두사를 붙일 수 있습니다. 예를 들어 `username:` 같은 것입니다. 그러면 이 예제에서 `sub` 값은 `username:johndoe`가 될 수 있습니다. -가장 중요한 점은 `sub` 키는 전체 애플리케이션에서 고유한 식별자가 되어야 하며 문자열이어야 한다는 점입니다. +기억해야 할 중요한 점은 `sub` 키가 전체 애플리케이션에서 고유한 식별자여야 하고, 문자열이어야 한다는 것입니다. -## 확인해봅시다 +## 확인하기 { #check-it } 서버를 실행하고 문서로 이동하십시오: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. -다음과 같은 사용자 인터페이스를 볼 수 있습니다: +다음과 같은 사용자 인터페이스가 보일 것입니다: <img src="/img/tutorial/security/image07.png"> -이전과 같은 방법으로 애플리케이션에 인증하십시오. +이전과 같은 방법으로 애플리케이션을 인가하십시오. 다음 인증 정보를 사용하십시오: @@ -211,13 +211,13 @@ Password: `secret` /// check -코드 어디에도 평문 패스워드 "`secret`" 이 없다는 점에 유의하십시오. 해시된 버전만 있습니다. +코드 어디에도 평문 패스워드 "`secret`"은 없고, 해시된 버전만 있다는 점에 유의하십시오. /// <img src="/img/tutorial/security/image08.png"> -`/users/me/` 를 호출하면 다음과 같은 응답을 얻을 수 있습니다: +엔드포인트 `/users/me/`를 호출하면 다음과 같은 응답을 받게 됩니다: ```JSON { @@ -230,44 +230,44 @@ Password: `secret` <img src="/img/tutorial/security/image09.png"> -개발자 도구를 열어보면 전송된 데이터에 토큰만 포함된 것을 확인할 수 있습니다. 패스워드는 사용자를 인증하고 액세스 토큰을 받기 위한 첫 번째 요청에만 전송되며, 이후에는 전송되지 않습니다: +개발자 도구를 열어보면 전송된 데이터에는 토큰만 포함되어 있고, 패스워드는 사용자를 인증하고 해당 액세스 토큰을 얻기 위한 첫 번째 요청에서만 전송되며 이후에는 전송되지 않는 것을 확인할 수 있습니다: <img src="/img/tutorial/security/image10.png"> /// note -`Bearer `로 시작하는 `Authorization` 헤더에 주목하십시오. +`Bearer `로 시작하는 값을 가진 `Authorization` 헤더에 주목하십시오. /// -## `scopes` 의 고급 사용법 +## `scopes`의 고급 사용법 { #advanced-usage-with-scopes } -OAuth2는 "스코프(scopes)" 라는 개념을 갖고 있습니다. +OAuth2에는 "scopes"라는 개념이 있습니다. -이를 사용하여 JWT 토큰에 특정 권한 집합을 추가할 수 있습니다. +이를 사용해 JWT 토큰에 특정 권한 집합을 추가할 수 있습니다. -그 후 이 토큰을 사용자에게 직접 제공하거나 제3자에게 제공하여, 특정 제한사항 하에있는 API와 통신하도록 할 수 있습니다. +그런 다음 이 토큰을 사용자에게 직접 제공하거나 제3자에게 제공하여, 특정 제한사항 하에서 API와 상호작용하도록 할 수 있습니다. -**FastAPI** 에서의 사용 방법과 통합 방식은 **심화 사용자 안내서** 에서 자세히 배울 수 있습니다. +어떻게 사용하는지, 그리고 **FastAPI**에 어떻게 통합되는지는 이후 **심화 사용자 안내서**에서 배울 수 있습니다. -## 요약 +## 요약 { #recap } -지금까지 살펴본 내용을 바탕으로, OAuth2와 JWT 같은 표준을 사용하여 안전한 **FastAPI** 애플리케이션을 만들 수 있습니다. +지금까지 살펴본 내용을 바탕으로, OAuth2와 JWT 같은 표준을 사용해 안전한 **FastAPI** 애플리케이션을 설정할 수 있습니다. -거의 모든 프레임워크에서 보안 처리는 상당히 복잡한 주제입니다. +거의 모든 프레임워크에서 보안 처리는 꽤 빠르게 복잡한 주제가 됩니다. -이를 단순화하는 많은 패키지는 데이터 모델, 데이터베이스, 사용 가능한 기능들에 대해 여러 제약이 있습니다. 그리고 지나치게 단순화하는 일부 패키지들은 심각한 보안 결함을 가질 수도 있습니다. +이를 크게 단순화하는 많은 패키지들은 데이터 모델, 데이터베이스, 사용 가능한 기능들에 대해 많은 타협을 해야 합니다. 그리고 지나치게 단순화하는 일부 패키지들은 실제로 내부에 보안 결함이 있기도 합니다. --- -**FastAPI** 는 어떤 데이터베이스, 데이터 모델, 도구도 강요하지 않습니다. +**FastAPI**는 어떤 데이터베이스, 데이터 모델, 도구에도 타협하지 않습니다. -프로젝트에 가장 적합한 것을 선택할 수 있는 유연성을 제공합니다. +프로젝트에 가장 잘 맞는 것들을 선택할 수 있는 모든 유연성을 제공합니다. -그리고 `passlib` 와 `PyJWT` 처럼 잘 관리되고 널리 사용되는 패키지들을 바로 사용할 수 있습니다. **FastAPI** 는 외부 패키지 통합을 위해 복잡한 메커니즘이 필요하지 않기 때문입니다. +그리고 **FastAPI**는 외부 패키지를 통합하기 위해 복잡한 메커니즘을 요구하지 않기 때문에 `pwdlib`와 `PyJWT` 같은 잘 관리되고 널리 사용되는 패키지들을 바로 사용할 수 있습니다. -그러나 유연성, 견고성, 보안성을 해치지 않으면서 과정을 단순화할 수 있는 도구들을 제공합니다. +하지만 유연성, 견고성, 보안성을 해치지 않으면서 과정을 가능한 한 단순화할 수 있도록 도구들을 제공합니다. -그리고 OAuth2와 같은 표준 프로토콜을 비교적 간단한 방법으로 구현하고 사용할 수 있습니다. +또한 OAuth2 같은 안전한 표준 프로토콜을 비교적 간단한 방식으로 사용하고 구현할 수 있습니다. -더 세분화된 권한 체계를 위해 OAuth2의 "스코프"를 사용하는 방법은 **심화 사용자 안내서**에서 더 자세히 배울 수 있습니다. OAuth2의 스코프는 제3자 애플리케이션이 사용자를 대신해 그들의 API와 상호작용하도록 권한을 부여하기 위해, Facebook, Google, GitHub, Microsoft, X (Twitter) 등의 많은 대형 인증 제공업체들이 사용하는 메커니즘입니다. +더 세분화된 권한 시스템을 위해 OAuth2 "scopes"를 사용하는 방법은, 같은 표준을 따르는 방식으로 **심화 사용자 안내서**에서 더 자세히 배울 수 있습니다. 스코프를 사용하는 OAuth2는 Facebook, Google, GitHub, Microsoft, X (Twitter) 등 많은 대형 인증 제공업체들이 제3자 애플리케이션이 사용자 대신 그들의 API와 상호작용할 수 있도록 인가하는 데 사용하는 메커니즘입니다. diff --git a/docs/ko/docs/tutorial/security/simple-oauth2.md b/docs/ko/docs/tutorial/security/simple-oauth2.md index f10c4f588..189dd89f2 100644 --- a/docs/ko/docs/tutorial/security/simple-oauth2.md +++ b/docs/ko/docs/tutorial/security/simple-oauth2.md @@ -1,8 +1,8 @@ -# 패스워드와 Bearer를 이용한 간단한 OAuth2 +# 패스워드와 Bearer를 이용한 간단한 OAuth2 { #simple-oauth2-with-password-and-bearer } 이제 이전 장에서 빌드하고 누락된 부분을 추가하여 완전한 보안 흐름을 갖도록 하겠습니다. -## `username`와 `password` 얻기 +## `username`와 `password` 얻기 { #get-the-username-and-password } **FastAPI** 보안 유틸리티를 사용하여 `username` 및 `password`를 가져올 것입니다. @@ -14,11 +14,11 @@ OAuth2는 (우리가 사용하고 있는) "패스워드 플로우"을 사용할 그리고 데이터베이스 모델은 원하는 다른 이름을 사용할 수 있습니다. -그러나 로그인 *경로 작동*의 경우 사양과 호환되도록 이러한 이름을 사용해야 합니다(예를 들어 통합 API 문서 시스템을 사용할 수 있어야 합니다). +그러나 로그인 *경로 처리*의 경우 사양과 호환되도록 이러한 이름을 사용해야 합니다(예를 들어 통합 API 문서 시스템을 사용할 수 있어야 합니다). 사양에는 또한 `username`과 `password`가 폼 데이터로 전송되어야 한다고 명시되어 있습니다(따라서 여기에는 JSON이 없습니다). -### `scope` +### `scope` { #scope } 사양에는 클라이언트가 다른 폼 필드 "`scope`"를 보낼 수 있다고 나와 있습니다. @@ -44,15 +44,15 @@ OAuth2의 경우 문자열일 뿐입니다. /// -## `username`과 `password`를 가져오는 코드 +## `username`과 `password`를 가져오는 코드 { #code-to-get-the-username-and-password } 이제 **FastAPI**에서 제공하는 유틸리티를 사용하여 이를 처리해 보겠습니다. -### `OAuth2PasswordRequestForm` +### `OAuth2PasswordRequestForm` { #oauth2passwordrequestform } -먼저 `OAuth2PasswordRequestForm`을 가져와 `/token`에 대한 *경로 작동*에서 `Depends`의 의존성으로 사용합니다. +먼저 `OAuth2PasswordRequestForm`을 가져와 `/token`에 대한 *경로 처리*에서 `Depends`의 의존성으로 사용합니다. -{* ../../docs_src/security/tutorial003.py hl[4,76] *} +{* ../../docs_src/security/tutorial003_an_py310.py hl[4,78] *} `OAuth2PasswordRequestForm`은 다음을 사용하여 폼 본문을 선언하는 클래스 의존성입니다: @@ -84,7 +84,7 @@ OAuth2 사양은 실제로 `password`라는 고정 값이 있는 `grant_type` /// -### 폼 데이터 사용하기 +### 폼 데이터 사용하기 { #use-the-form-data } /// tip | 팁 @@ -100,9 +100,9 @@ OAuth2 사양은 실제로 `password`라는 고정 값이 있는 `grant_type` 오류의 경우 `HTTPException` 예외를 사용합니다: -{* ../../docs_src/security/tutorial003.py hl[3,77:79] *} +{* ../../docs_src/security/tutorial003_an_py310.py hl[3,79:81] *} -### 패스워드 확인하기 +### 패스워드 확인하기 { #check-the-password } 이 시점에서 데이터베이스의 사용자 데이터 형식을 확인했지만 암호를 확인하지 않았습니다. @@ -112,7 +112,7 @@ OAuth2 사양은 실제로 `password`라는 고정 값이 있는 `grant_type` 두 패스워드가 일치하지 않으면 동일한 오류가 반환됩니다. -#### 패스워드 해싱 +#### 패스워드 해싱 { #password-hashing } "해싱"은 일부 콘텐츠(이 경우 패스워드)를 횡설수설하는 것처럼 보이는 일련의 바이트(문자열)로 변환하는 것을 의미합니다. @@ -120,21 +120,15 @@ OAuth2 사양은 실제로 `password`라는 고정 값이 있는 `grant_type` 그러나 횡설수설에서 암호로 다시 변환할 수는 없습니다. -##### 패스워드 해싱을 사용해야 하는 이유 +##### 패스워드 해싱을 사용해야 하는 이유 { #why-use-password-hashing } 데이터베이스가 유출된 경우 해커는 사용자의 일반 텍스트 암호가 아니라 해시만 갖게 됩니다. 따라서 해커는 다른 시스템에서 동일한 암호를 사용하려고 시도할 수 없습니다(많은 사용자가 모든 곳에서 동일한 암호를 사용하므로 이는 위험할 수 있습니다). -//// tab | 파이썬 3.7 이상 +{* ../../docs_src/security/tutorial003_an_py310.py hl[82:85] *} -{* ../../docs_src/security/tutorial003.py hl[80:83] *} - -//// - -{* ../../docs_src/security/tutorial003_py310.py hl[78:81] *} - -#### `**user_dict`에 대해 +#### `**user_dict`에 대해 { #about-user-dict } `UserInDB(**user_dict)`는 다음을 의미한다: @@ -152,11 +146,11 @@ UserInDB( /// info | 정보 -`**user_dict`에 대한 자세한 설명은 [**추가 모델** 문서](../extra-models.md#about-user_indict){.internal-link target=_blank}를 다시 읽어봅시다. +`**user_dict`에 대한 자세한 설명은 [**추가 모델** 문서](../extra-models.md#about-user-in-dict){.internal-link target=_blank}를 다시 확인해보세요. /// -## 토큰 반환하기 +## 토큰 반환하기 { #return-the-token } `token` 엔드포인트의 응답은 JSON 객체여야 합니다. @@ -174,7 +168,7 @@ UserInDB( /// -{* ../../docs_src/security/tutorial003.py hl[85] *} +{* ../../docs_src/security/tutorial003_an_py310.py hl[87] *} /// tip | 팁 @@ -188,19 +182,19 @@ UserInDB( /// -## 의존성 업데이트하기 +## 의존성 업데이트하기 { #update-the-dependencies } 이제 의존성을 업데이트를 할 겁니다. 이 사용자가 활성화되어 있는 *경우에만* `current_user`를 가져올 겁니다. -따라서 `get_current_user`를 의존성으로 사용하는 추가 종속성 `get_current_active_user`를 만듭니다. +따라서 `get_current_user`를 의존성으로 사용하는 추가 의존성 `get_current_active_user`를 만듭니다. 이러한 의존성 모두, 사용자가 존재하지 않거나 비활성인 경우 HTTP 오류를 반환합니다. 따라서 엔드포인트에서는 사용자가 존재하고 올바르게 인증되었으며 활성 상태인 경우에만 사용자를 얻습니다: -{* ../../docs_src/security/tutorial003.py hl[58:66,69:72,90] *} +{* ../../docs_src/security/tutorial003_an_py310.py hl[58:66,69:74,94] *} /// info | 정보 @@ -220,11 +214,11 @@ UserInDB( /// -## 확인하기 +## 확인하기 { #see-it-in-action } 대화형 문서 열기: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. -### 인증하기 +### 인증하기 { #authenticate } "Authorize" 버튼을 눌러봅시다. @@ -240,7 +234,7 @@ UserInDB( <img src="/img/tutorial/security/image05.png"> -### 자신의 유저 데이터 가져오기 +### 자신의 유저 데이터 가져오기 { #get-your-own-user-data } 이제 `/users/me` 경로에 `GET` 작업을 진행합시다. @@ -266,7 +260,7 @@ UserInDB( } ``` -### 비활성된 유저 +### 비활성된 유저 { #inactive-user } 이제 비활성된 사용자로 시도하고, 인증해봅시다: @@ -284,7 +278,7 @@ UserInDB( } ``` -## 요약 +## 요약 { #recap } 이제 API에 대한 `username` 및 `password`를 기반으로 완전한 보안 시스템을 구현할 수 있는 도구가 있습니다. diff --git a/docs/ko/docs/tutorial/sql-databases.md b/docs/ko/docs/tutorial/sql-databases.md index 58c7017d6..3d64cf627 100644 --- a/docs/ko/docs/tutorial/sql-databases.md +++ b/docs/ko/docs/tutorial/sql-databases.md @@ -1,18 +1,18 @@ -# SQL (관계형) 데이터베이스 +# SQL (관계형) 데이터베이스 { #sql-relational-databases } -**FastAPI**에서 SQL(관계형) 데이터베이스 사용은 필수가 아닙니다. 여러분이 원하는 **어떤 데이터베이스든** 사용할 수 있습니다. +**FastAPI**에서 SQL(관계형) 데이터베이스 사용은 필수가 아닙니다. 하지만 여러분이 원하는 **어떤 데이터베이스든** 사용할 수 있습니다. 여기서는 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a>을 사용하는 예제를 살펴보겠습니다. -**SQLModel**은 <a href="https://www.sqlalchemy.org/" class="external-link" target="_blank">SQLAlchemy</a>와 Pydantic을 기반으로 구축되었습니다.SQLModel은 **SQL 데이터베이스**를 사용하는 FastAPI 애플리케이션에 완벽히 어울리도록 **FastAPI**의 제작자가 설계한 도구입니다. +**SQLModel**은 <a href="https://www.sqlalchemy.org/" class="external-link" target="_blank">SQLAlchemy</a>와 Pydantic을 기반으로 구축되었습니다. **SQL 데이터베이스**를 사용해야 하는 FastAPI 애플리케이션에 완벽히 어울리도록 **FastAPI**와 같은 제작자가 만든 도구입니다. /// tip | 팁 -다른 SQL 또는 NoSQL 데이터베이스 라이브러리를 사용할 수도 있습니다 (일부는 <abbr title="객체 관계 매퍼(Object Relational Mapper), SQL 테이블을 나타내는 클래스를 제공하고 테이블의 행을 인스턴스로 표현하는 라이브러리를 지칭하는 용어">"ORM"</abbr>이라고도 불립니다), FastAPI는 특정 라이브러리의 사용을 강요하지 않습니다. 😎 +다른 SQL 또는 NoSQL 데이터베이스 라이브러리를 사용할 수도 있습니다 (일부는 <abbr title="Object Relational Mapper: a fancy term for a library where some classes represent SQL tables and instances represent rows in those tables">"ORMs"</abbr>이라고도 불립니다), FastAPI는 특정 라이브러리의 사용을 강요하지 않습니다. 😎 /// -SQLModel은 SQLAlchemy를 기반으로 하므로, SQLAlchemy에서 **지원하는 모든 데이터베이스**를 손쉽게 사용할 수 있습니다(SQLModel에서도 동일하게 지원됩니다). 예를 들면: +SQLModel은 SQLAlchemy를 기반으로 하므로, SQLAlchemy에서 **지원하는 모든 데이터베이스**를 손쉽게 사용할 수 있습니다(이것들은 SQLModel에서도 지원됩니다). 예를 들면: * PostgreSQL * MySQL @@ -20,19 +20,19 @@ SQLModel은 SQLAlchemy를 기반으로 하므로, SQLAlchemy에서 **지원하 * Oracle * Microsoft SQL Server 등. -이 예제에서는 **SQLite**를 사용합니다. SQLite는 단일 파일을 사용하고 파이썬에서 기본적으로 지원하기 때문입니다. 따라서 이 예제를 그대로 복사하여 실행할 수 있습니다. +이 예제에서는 **SQLite**를 사용합니다. SQLite는 단일 파일을 사용하고 Python에서 통합 지원하기 때문입니다. 따라서 이 예제를 그대로 복사하여 실행할 수 있습니다. -나중에 실제 프로덕션 애플리케이션에서는 **PostgreSQL**과 같은 데이터베이스 서버를 사용하는 것이 좋습니다. +나중에 프로덕션 애플리케이션에서는 **PostgreSQL**과 같은 데이터베이스 서버를 사용하는 것이 좋습니다. /// tip | 팁 -**FastAPI**와 **PostgreSQL**를 포함하여 프론트엔드와 다양한 도구를 제공하는 공식 프로젝트 생성기가 있습니다: <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">https://github.com/fastapi/full-stack-fastapi-template</a> +프론트엔드와 더 많은 도구를 포함하여 **FastAPI**와 **PostgreSQL**을 포함한 공식 프로젝트 생성기가 있습니다: <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">https://github.com/fastapi/full-stack-fastapi-template</a> /// -이 튜토리얼은 매우 간단하고 짧습니다. 데이터베이스 기본 개념, SQL, 또는 더 복잡한 기능에 대해 배우고 싶다면, <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel 문서</a>를 참고하세요. +이 튜토리얼은 매우 간단하고 짧습니다. 데이터베이스 기본 개념, SQL, 또는 더 고급 기능에 대해 배우고 싶다면, <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel 문서</a>를 참고하세요. -## `SQLModel` 설치하기 +## `SQLModel` 설치하기 { #install-sqlmodel } 먼저, [가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고 활성화한 다음, `sqlmodel`을 설치하세요: @@ -45,13 +45,13 @@ $ pip install sqlmodel </div> -## 단일 모델로 애플리케이션 생성하기 +## 단일 모델로 애플리케이션 생성하기 { #create-the-app-with-a-single-model } 우선 단일 **SQLModel** 모델을 사용하여 애플리케이션의 가장 간단한 첫 번째 버전을 생성해보겠습니다. -이후 **다중 모델**을 추가하여 보안과 유연성을 강화할 것입니다. 🤓 +이후 아래에서 **여러 모델**로 보안과 유연성을 강화하며 개선하겠습니다. 🤓 -### 모델 생성하기 +### 모델 생성하기 { #create-models } `SQLModel`을 가져오고 데이터베이스 모델을 생성합니다: @@ -61,45 +61,45 @@ $ pip install sqlmodel 몇 가지 차이점이 있습니다: -* `table=True`는 SQLModel에 이 모델이 *테이블 모델*이며, 단순한 데이터 모델이 아니라 SQL 데이터베이스의 **테이블**을 나타낸다는 것을 알려줍니다. (다른 일반적인 Pydantic 클래스처럼) 단순한 *데이터 모델*이 아닙니다. +* `table=True`는 SQLModel에 이 모델이 *테이블 모델*이며, SQL 데이터베이스의 **테이블**을 나타내야 한다는 것을 알려줍니다. (다른 일반적인 Pydantic 클래스처럼) 단순한 *데이터 모델*이 아닙니다. * `Field(primary_key=True)`는 SQLModel에 `id`가 SQL 데이터베이스의 **기본 키**임을 알려줍니다 (SQL 기본 키에 대한 자세한 내용은 SQLModel 문서를 참고하세요). - `int | None` 유형으로 설정하면, SQLModel은 해당 열이 SQL 데이터베이스에서 `INTEGER` 유형이며 `NULLABLE` 값이어야 한다는 것을 알 수 있습니다. + **참고:** 기본 키 필드에 `int | None`을 사용하는 이유는, Python 코드에서 *`id` 없이 객체를 생성*할 수 있게 하기 위해서입니다(`id=None`). 데이터베이스가 *저장할 때 생성해 줄 것*이라고 가정합니다. SQLModel은 데이터베이스가 `id`를 제공한다는 것을 이해하고, 데이터베이스 스키마에서 *해당 열을 null이 아닌 `INTEGER`*로 정의합니다. 자세한 내용은 <a href="https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/#primary-key-id" class="external-link" target="_blank">기본 키에 대한 SQLModel 문서</a>를 참고하세요. -* `Field(index=True)`는 SQLModel에 해당 열에 대해 **SQL 인덱스**를 생성하도록 지시합니다. 이를 통해 데이터베이스에서 이 열으로 필터링된 데이터를 읽을 때 더 빠르게 조회할 수 있습니다. +* `Field(index=True)`는 SQLModel에 해당 열에 대해 **SQL 인덱스**를 생성하도록 지시합니다. 이를 통해 데이터베이스에서 이 열로 필터링된 데이터를 읽을 때 더 빠르게 조회할 수 있습니다. SQLModel은 `str`으로 선언된 항목이 SQL 데이터베이스에서 `TEXT` (또는 데이터베이스에 따라 `VARCHAR`) 유형의 열로 저장된다는 것을 인식합니다. -### 엔진 생성하기 +### 엔진 생성하기 { #create-an-engine } SQLModel의 `engine` (내부적으로는 SQLAlchemy `engine`)은 데이터베이스에 대한 **연결을 유지**하는 역할을 합니다. -**하나의 단일 engine 객체**를 통해 코드 전체에서 동일한 데이터베이스에 연결할 수 있습니다. +코드 전체에서 동일한 데이터베이스에 연결하기 위해 **하나의 단일 `engine` 객체**를 사용합니다. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *} -`check_same_thread=False`를 사용하면 FastAPI에서 여러 스레드에서 동일한 SQLite 데이터베이스를 사용할 수 있습니다. 이는 **하나의 단일 요청**이 **여러 스레드**를 사용할 수 있기 때문에 필요합니다(예: 의존성에서 사용되는 경우). +`check_same_thread=False`를 사용하면 FastAPI에서 여러 스레드에서 동일한 SQLite 데이터베이스를 사용할 수 있습니다. 이는 **하나의 단일 요청**이 **둘 이상의 스레드**를 사용할 수 있기 때문에 필요합니다(예: 의존성에서 사용되는 경우). -걱정하지 마세요. 코드가 구조화된 방식으로 인해, 이후에 **각 요청마다 단일 SQLModel *세션*을 사용**하도록 보장할 것입니다. 실제로 그것이 `check_same_thread`가 하려는 것입니다. +걱정하지 마세요. 코드가 구조화된 방식으로 인해, 이후에 **각 요청마다 단일 SQLModel *세션*을 사용**하도록 보장할 것입니다. 실제로 이것이 `check_same_thread`가 하려는 것입니다. -### 테이블 생성하기 +### 테이블 생성하기 { #create-the-tables } 그 다음 `SQLModel.metadata.create_all(engine)`을 사용하여 모든 *테이블 모델*의 **테이블을 생성**하는 함수를 추가합니다. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *} -### 세션 의존성 생성하기 +### 세션 의존성 생성하기 { #create-a-session-dependency } **`Session`**은 **메모리에 객체**를 저장하고 데이터에 필요한 모든 변경 사항을 추적한 후, **`engine`을 통해** 데이터베이스와 통신합니다. `yield`를 사용해 FastAPI의 **의존성**을 생성하여 각 요청마다 새로운 `Session`을 제공합니다. 이는 요청당 하나의 세션만 사용되도록 보장합니다. 🤓 -그런 다음 이 의존성을 사용하는 코드를 간소화하기 위해 `Annotated` 의존성 `SessionDep`을 생성합니다. +그런 다음 이 의존성을 사용하는 나머지 코드를 간소화하기 위해 `Annotated` 의존성 `SessionDep`을 생성합니다. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *} -### 시작 시 데이터베이스 테이블 생성하기 +### 시작 시 데이터베이스 테이블 생성하기 { #create-database-tables-on-startup } 애플리케이션 시작 시 데이터베이스 테이블을 생성합니다. @@ -115,9 +115,9 @@ SQLModel은 Alembic을 감싸는 마이그레이션 유틸리티를 제공할 /// -### Hero 생성하기 +### Hero 생성하기 { #create-a-hero } -각 SQLModel 모델은 Pydantic 모델이기도 하므로, Pydantic 모델을 사용할 수 있는 **타입 어노테이**션에서 동일하게 사용할 수 있습니다. +각 SQLModel 모델은 Pydantic 모델이기도 하므로, Pydantic 모델을 사용할 수 있는 동일한 **타입 어노테이션**에서 사용할 수 있습니다. 예를 들어, 파라미터를 `Hero` 타입으로 선언하면 **JSON 본문**에서 값을 읽어옵니다. @@ -125,31 +125,29 @@ SQLModel은 Alembic을 감싸는 마이그레이션 유틸리티를 제공할 {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *} -</details> +여기서는 `SessionDep` 의존성(`Session`)을 사용하여 새로운 `Hero`를 `Session` 인스턴스에 추가하고, 데이터베이스에 변경 사항을 커밋하고, `hero` 데이터의 최신 상태를 갱신한 다음 이를 반환합니다. -여기서 `SessionDep` 의존성 (즉, `Session`)을 사용하여 새로운 `Hero`를 `Session` 인스턴스에 추가하고, 데이터베이스에 변경 사항을 커밋하고, `hero` 데이터의 최신 상태를 갱신한 다음 이를 반환합니다. - -### Heroes 조회하기 +### Heroes 조회하기 { #read-heroes } `select()`를 사용하여 데이터베이스에서 `Hero`를 **조회**할 수 있습니다. 결과에 페이지네이션을 적용하기 위해 `limit`와 `offset`을 포함할 수 있습니다. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *} -### 단일 Hero 조회하기 +### 단일 Hero 조회하기 { #read-one-hero } 단일 `Hero`를 **조회**할 수도 있습니다. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *} -### Hero 삭제하기 +### Hero 삭제하기 { #delete-a-hero } `Hero`를 **삭제**하는 것도 가능합니다. {* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *} -### 애플리케이션 실행하기 +### 애플리케이션 실행하기 { #run-the-app } -애플리케이션을 실행하려면 다음 명령을 사용합니다: +애플리케이션을 실행할 수 있습니다: <div class="termy"> @@ -161,33 +159,33 @@ $ fastapi dev main.py </div> -그런 다음 `/docs` UI로 이동하면, **FastAPI**가 해당 **model들**을 사용하여 API **문서를 생성**하는 것으르 확인할 수 있습니다. 또한 이 모델들은 데이터를 직렬화하고 검증하는 데에도 사용됩니다. +그런 다음 `/docs` UI로 이동하면, **FastAPI**가 이 **모델**들을 사용해 API를 **문서화**하고, 데이터를 **직렬화**하고 **검증**하는 데에도 사용하는 것을 확인할 수 있습니다. <div class="screenshot"> <img src="/img/tutorial/sql-databases/image01.png"> </div> -## 여러 모델로 애플리케이션 업데이트 +## 여러 모델로 애플리케이션 업데이트 { #update-the-app-with-multiple-models } -이제 애플리케이션을 약간 **리팩토링**하여 **보안**과 **유연성**을 개선해 보겠습니다. +이제 이 애플리케이션을 약간 **리팩터링**하여 **보안**과 **유연성**을 개선해 보겠습니다. -이전 애플리케이션의 UI를 보면, 지금까지는 클라이언트가 생성할 `Hero`의 `id`를 직접 지정할 수 있다는 것을 알 수 있습니다. 😱 +이전 애플리케이션을 확인해 보면, 지금까지는 UI에서 클라이언트가 생성할 `Hero`의 `id`를 결정할 수 있게 되어 있는 것을 볼 수 있습니다. 😱 -이는 허용되어선 안 됩니다. 클라이언트가 이미 데이터베이스에 저장된 `id`를 덮어쓸 위험이 있기 때문입니다. `id`는 **백엔드** 또는 **데이터베이스**가 결정해야 하며, **클라이언트**가 결정해서는 안 됩니다. +이렇게 해서는 안 됩니다. 클라이언트가 DB에 이미 할당되어 있는 `id`를 덮어쓸 수 있기 때문입니다. `id`를 결정하는 것은 **백엔드** 또는 **데이터베이스**가 해야 하며, **클라이언트**가 해서는 안 됩니다. -또한 hero의 `secret_name`을 생성하긴 했지만, 지금까지는 이 값을 어디에서나 반환하고 있습니다. 이는 그다지 **비밀스럽지** 않습니다... 😅 +또한 hero에 대한 `secret_name`을 생성하지만, 지금까지는 이 값을 어디에서나 반환하고 있습니다. 이는 그다지 **비밀스럽지** 않습니다... 😅 -이러한 문제를 해결하기 위해 몇 가지 **추가 모델**을 추가할 것입니다. 바로 여기서 SQLModel이 빛을 발하게 됩니다. ✨ +이러한 문제는 몇 가지 **추가 모델**을 추가해 해결하겠습니다. 바로 여기서 SQLModel이 빛을 발하게 됩니다. ✨ -### 여러 모델 생성하기 +### 여러 모델 생성하기 { #create-multiple-models } **SQLModel**에서 `table=True`가 설정된 모델 클래스는 **테이블 모델**입니다. -`table=True`가 없는 모델 클래스는 **데이터 모델**로, 이는 실제로 몇 가지 추가 기능이 포함된 Pydantic 모델에 불과합니다. 🤓 +그리고 `table=True`가 없는 모델 클래스는 **데이터 모델**인데, 이것들은 실제로는 (몇 가지 작은 추가 기능이 있는) Pydantic 모델일 뿐입니다. 🤓 SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중복 선언하지 않아도** 됩니다. -#### `HeroBase` - 기본 클래스 +#### `HeroBase` - 기본 클래스 { #herobase-the-base-class } 모든 모델에서 **공유되는 필드**를 가진 `HeroBase` 모델을 시작해 봅시다: @@ -196,14 +194,14 @@ SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *} -#### `Hero` - *테이블 모델* +#### `Hero` - *테이블 모델* { #hero-the-table-model } 다음으로 실제 *테이블 모델*인 `Hero`를 생성합니다. 이 모델은 다른 모델에는 항상 포함되는 건 아닌 **추가 필드**를 포함합니다: * `id` * `secret_name` -`Hero`는 `HeroBase`를 상속하므로 `HeroBase`에 선언된 필드도 포함합니다. 따라서 `Hero`는 다음 **필드들도** 가지게 됩니다: +`Hero`는 `HeroBase`를 상속하므로 `HeroBase`에 선언된 필드도 **또한** 포함합니다. 따라서 `Hero`의 모든 필드는 다음과 같습니다: * `id` * `name` @@ -212,11 +210,11 @@ SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *} -#### `HeroPublic` - 공개 *데이터 모델* +#### `HeroPublic` - 공개 *데이터 모델* { #heropublic-the-public-data-model } 다음으로 `HeroPublic` 모델을 생성합니다. 이 모델은 API 클라이언트에 **반환**되는 모델입니다. -`HeroPublic`은 `HeroBase`와 동일한 필드를 가지며, `secret_name`은 포함하지 않습니다. +`HeroPublic`은 `HeroBase`와 동일한 필드를 가지므로, `secret_name`은 포함하지 않습니다. 마침내 우리의 heroes의 정체가 보호됩니다! 🥷 @@ -224,9 +222,9 @@ SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중 /// tip | 팁 -반환 모델이 값이 항상 존재하고 항상 `int`(`None`이 아님)를 보장하는 것은 API 클라이언트에게 매우 유용합니다. 이를 통해 API와 통신하는 개발자가 훨씬 더 간단한 코드를 작성할 수 있습니다. +반환 모델이 값이 항상 존재하고 항상 `int`(`None`이 아님)를 보장하는 것은 API 클라이언트에게 매우 유용합니다. 이를 통해 API 클라이언트는 이런 확신을 바탕으로 훨씬 더 간단한 코드를 작성할 수 있습니다. -또한 **자동으로 생성된 클라이언트**는 더 단순한 인터페이스를 제공하므로, API와 소통하는 개발자들이 훨씬 수월하게 작업할 수 있습니다. 😎 +또한 **자동으로 생성된 클라이언트**는 더 단순한 인터페이스를 제공하므로, API와 소통하는 개발자들이 API를 사용하면서 훨씬 더 좋은 경험을 할 수 있습니다. 😎 /// @@ -235,23 +233,22 @@ SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중 * `id` * `name` * `age` -* `secret_name` {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *} -#### `HeroCreate` - hero 생성용 *데이터 모델* +#### `HeroCreate` - hero 생성용 *데이터 모델* { #herocreate-the-data-model-to-create-a-hero } 이제 `HeroCreate` 모델을 생성합니다. 이 모델은 클라이언트로부터 받은 데이터를 **검증**하는 역할을 합니다. -`HeroCreate`는 `HeroBase와` 동일한 필드를 가지며, 추가로 `secret_name을` 포함합니다. +`HeroCreate`는 `HeroBase`와 동일한 필드를 가지며, `secret_name`도 포함합니다. -클라이언트가 **새 hero을 생성**할 때 `secret_name`을 보내고, 이는 데이터베이스에 저장되지만, 해당 비밀 이름은 API를 통해 클라이언트에게 반환되지 않습니다. +이제 클라이언트가 **새 hero를 생성**할 때 `secret_name`을 보내면, 데이터베이스에 저장되지만, 그 비밀 이름은 API를 통해 클라이언트에게 반환되지 않습니다. /// tip | 팁 이 방식은 **비밀번호**를 처리하는 방법과 동일합니다. 비밀번호를 받지만, 이를 API에서 반환하지는 않습니다. -비밀번호 값을 저장하기 전에 **해싱**하여 저장하고, **평문으로 저장하지 마십시오**. +또한 비밀번호 값을 저장하기 전에 **해싱**하여 저장하고, **평문으로 저장하지 마십시오**. /// @@ -263,15 +260,15 @@ SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *} -#### `HeroUpdate` - hero 수정용 *데이터 모델* +#### `HeroUpdate` - hero 수정용 *데이터 모델* { #heroupdate-the-data-model-to-update-a-hero } -이전 애플리케이션에서는 **hero를 수정**할 방법이 없었지만, 이제 **다중 모델**을 통해 수정 기능을 추가할 수 있습니다. 🎉 +이전 버전의 애플리케이션에서는 **hero를 수정**할 방법이 없었지만, 이제 **여러 모델**로 이를 할 수 있습니다. 🎉 -`HeroUpdate` *데이터 모델*은 약간 특별한데, 새 hero을 생성할 때 필요한 **모든 동일한 필드**를 가지지만, 모든 필드가 **선택적**(기본값이 있음)입니다. 이렇게 하면 hero을 수정할 때 수정하려는 필드만 보낼 수 있습니다. +`HeroUpdate` *데이터 모델*은 약간 특별한데, 새 hero를 생성할 때 필요한 **모든 동일한 필드**를 가지지만, 모든 필드가 **선택적**(기본값이 있음)입니다. 이렇게 하면 hero를 수정할 때 수정하려는 필드만 보낼 수 있습니다. -모든 **필드가 변경되기** 때문에(타입이 `None`을 포함하고, 기본값이 `None`으로 설정됨), 모든 필드를 **다시 선언**해야 합니다. +모든 **필드가 실제로 변경**되기 때문에(타입이 이제 `None`을 포함하고, 기본값도 이제 `None`이 됨), 우리는 필드를 **다시 선언**해야 합니다. -엄밀히 말하면 `HeroBase`를 상속할 필요는 없습니다. 모든 필드를 다시 선언하기 때문입니다. 일관성을 위해 상속을 유지하긴 했지만, 필수는 아닙니다. 이는 개인적인 취향의 문제입니다. 🤷 +`HeroBase`를 상속할 필요는 없습니다. 모든 필드를 다시 선언하기 때문입니다. 일관성을 위해 상속을 유지하긴 했지만, 필수는 아닙니다. 이는 개인적인 취향의 문제입니다. 🤷 `HeroUpdate`의 필드는 다음과 같습니다: @@ -281,61 +278,61 @@ SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *} -### `HeroCreate`로 생성하고 `HeroPublic` 반환하기 +### `HeroCreate`로 생성하고 `HeroPublic` 반환하기 { #create-with-herocreate-and-return-a-heropublic } -이제 **다중 모델**을 사용하므로 애플리케이션의 관련 부분을 업데이트할 수 있습니다. +이제 **여러 모델**을 사용하므로 애플리케이션의 관련 부분을 업데이트할 수 있습니다. 요청에서 `HeroCreate` *데이터 모델*을 받아 이를 기반으로 `Hero` *테이블 모델*을 생성합니다. 이 새 *테이블 모델* `Hero`는 클라이언트에서 보낸 필드를 가지며, 데이터베이스에서 생성된 `id`도 포함합니다. -그런 다음 함수를 통해 동일한 *테이블 모델* `Hero`를 반환합니다. 하지만 `response_model`로 `HeroPublic` *데이터 모델*을 선언했기 때문에, **FastAPI**는 `HeroPublic`을 사용하여 데이터를 검증하고 직렬화합니다. +그런 다음 함수를 통해 동일한 *테이블 모델* `Hero`를 그대로 반환합니다. 하지만 `response_model`로 `HeroPublic` *데이터 모델*을 선언했기 때문에, **FastAPI**는 `HeroPublic`을 사용하여 데이터를 검증하고 직렬화합니다. {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *} /// tip | 팁 -이제 **반환 타입 주석** `-> HeroPublic` 대신 `response_model=HeroPublic`을 사용합니다. 반환하는 값이 실제로 `HeroPublic`이 *아니기* 때문입니다. +이제 **반환 타입 어노테이션** `-> HeroPublic` 대신 `response_model=HeroPublic`을 사용합니다. 반환하는 값이 실제로 `HeroPublic`이 *아니기* 때문입니다. -만약 `-> HeroPublic`으로 선언했다면, 에디터와 린터에서 반환값이 `HeroPublic`이 아니라 `Hero`라고 경고했을 것입니다. 이는 적절한 경고입니다. +만약 `-> HeroPublic`으로 선언했다면, 에디터와 린터에서 `HeroPublic` 대신 `Hero`를 반환한다고 (당연히) 불평할 것입니다. -`response_model`에 선언함으로써 **FastAPI**가 이를 처리하도록 하고, 타입 어노테이션과 에디터 및 다른 도구의 도움에는 영향을 미치지 않도록 설정합니다. +`response_model`에 선언함으로써 **FastAPI**가 처리하도록 하고, 타입 어노테이션과 에디터 및 다른 도구의 도움에는 영향을 미치지 않도록 합니다. /// -### `HeroPublic`으로 Heroes 조회하기 +### `HeroPublic`으로 Heroes 조회하기 { #read-heroes-with-heropublic } 이전과 동일하게 `Hero`를 **조회**할 수 있습니다. 이번에도 `response_model=list[HeroPublic]`을 사용하여 데이터가 올바르게 검증되고 직렬화되도록 보장합니다. {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *} -### `HeroPublic`으로 단일 Hero 조회하기 +### `HeroPublic`으로 단일 Hero 조회하기 { #read-one-hero-with-heropublic } -단일 hero을 **조회**할 수도 있습니다: +단일 hero를 **조회**할 수도 있습니다: {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *} -### `HeroUpdate`로 Hero 수정하기 +### `HeroUpdate`로 Hero 수정하기 { #update-a-hero-with-heroupdate } **hero를 수정**할 수도 있습니다. 이를 위해 HTTP `PATCH` 작업을 사용합니다. -코드에서는 클라이언트가 보낸 데이터를 딕셔너리 형태(`dict`)로 가져옵니다. 이는 **클라이언트가 보낸 데이터만 포함**하며, 기본값으로 들어가는 값은 제외합니다. 이를 위해 `exclude_unset=True`를 사용합니다. 이것이 주요 핵심입니다. 🪄 +그리고 코드에서는 클라이언트가 보낸 모든 데이터가 담긴 `dict`를 가져오는데, **클라이언트가 보낸 데이터만** 포함하고, 기본값이어서 들어가 있는 값은 제외합니다. 이를 위해 `exclude_unset=True`를 사용합니다. 이것이 주요 핵심입니다. 🪄 -그런 다음, `hero_db.sqlmodel_update(hero_data)`를 사용하여 `hero_data`의 데이터를 `hero_db`에 업데이트합니다. +그런 다음, `hero_db.sqlmodel_update(hero_data)`를 사용하여 `hero_data`의 데이터로 `hero_db`를 업데이트합니다. {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *} -### Hero 다시 삭제하기 +### Hero 다시 삭제하기 { #delete-a-hero-again } hero **삭제**는 이전과 거의 동일합니다. -이번에는 모든 것을 리팩토링하고 싶은 욕구를 만족시키지 못할 것 같습니다. 😅 +이번에는 모든 것을 리팩터링하고 싶은 욕구를 만족시키지 못하겠습니다. 😅 {* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *} -### 애플리케이션 다시 실행하기 +### 애플리케이션 다시 실행하기 { #run-the-app-again } -다음 명령을 사용해 애플리케이션을 다시 실행할 수 있습니다: +애플리케이션을 다시 실행할 수 있습니다: <div class="termy"> @@ -347,14 +344,14 @@ $ fastapi dev main.py </div> -`/docs` API UI로 이동하면 업데이트된 것을 확인할 수 있습니다. 클라이언트가 영웅을 생성할 때 `id`를 제공할 필요가 없게 되는 등의 변화도 보입니다. +`/docs` API UI로 이동하면 이제 업데이트되어 있고, hero를 생성할 때 클라이언트가 `id`를 보낼 것이라고 기대하지 않는 것 등을 확인할 수 있습니다. <div class="screenshot"> <img src="/img/tutorial/sql-databases/image02.png"> </div> -## 요약 +## 요약 { #recap } <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">**SQLModel**</a>을 사용하여 SQL 데이터베이스와 상호작용하고, *데이터 모델* 및 *테이블 모델*로 코드를 간소화할 수 있습니다. -더 많은 내용을 배우고 싶다면, **SQLModel** 문서를 참고하세요. <a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/" class="external-link" target="_blank">SQLModel을 **FastAPI**와 함께 사용하는 것에 대한 더 긴 미니 튜토리얼</a>도 제공합니다. 🚀 +더 많은 내용을 배우려면 **SQLModel** 문서를 참고하세요. **FastAPI**와 함께 SQLModel을 사용하는 더 긴 미니 <a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/" class="external-link" target="_blank">튜토리얼</a>도 있습니다. 🚀 diff --git a/docs/ko/docs/tutorial/static-files.md b/docs/ko/docs/tutorial/static-files.md index 4f3e3ab28..aa4c57179 100644 --- a/docs/ko/docs/tutorial/static-files.md +++ b/docs/ko/docs/tutorial/static-files.md @@ -1,41 +1,40 @@ -# 정적 파일 +# 정적 파일 { #static-files } -'StaticFiles'를 사용하여 디렉토리에서 정적 파일을 자동으로 제공할 수 있습니다. +`StaticFiles`를 사용하면 디렉터리에서 정적 파일을 자동으로 제공할 수 있습니다. -## `StaticFiles` 사용 +## `StaticFiles` 사용 { #use-staticfiles } -* `StaticFiles` 임포트합니다. -* 특정 경로에 `StaticFiles()` 인스턴스를 "마운트" 합니다. +* `StaticFiles`를 임포트합니다. +* 특정 경로에 `StaticFiles()` 인스턴스를 "마운트"합니다. -{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} +{* ../../docs_src/static_files/tutorial001_py39.py hl[2,6] *} -/// note | 기술적 세부사항 +/// note | 기술 세부사항 -`from starlette.staticfiles import StaticFiles` 를 사용할 수도 있습니다. +`from starlette.staticfiles import StaticFiles`를 사용할 수도 있습니다. -**FastAPI**는 단지 개발자인, 당신에게 편의를 제공하기 위해 `fastapi.static files` 와 동일한 `starlett.static files`를 제공합니다. 하지만 사실 이것은 Starlett에서 직접 온 것입니다. +**FastAPI**는 개발자인 여러분의 편의를 위해 `fastapi.staticfiles`로 `starlette.staticfiles`와 동일한 것을 제공합니다. 하지만 실제로는 Starlette에서 직접 가져온 것입니다. /// -### "마운팅" 이란 +### "마운팅"이란 { #what-is-mounting } -"마운팅"은 특정 경로에 완전히 "독립적인" 애플리케이션을 추가하는 것을 의미하는데, 그 후 모든 하위 경로에 대해서도 적용됩니다. +"마운팅"은 특정 경로에 완전한 "독립적인" 애플리케이션을 추가하고, 그 애플리케이션이 모든 하위 경로를 처리하도록 하는 것을 의미합니다. -마운트된 응용 프로그램은 완전히 독립적이기 때문에 `APIRouter`를 사용하는 것과는 다릅니다. OpenAPI 및 응용 프로그램의 문서는 마운트된 응용 프로그램 등에서 어떤 것도 포함하지 않습니다. +마운트된 애플리케이션은 완전히 독립적이므로 `APIRouter`를 사용하는 것과는 다릅니다. 메인 애플리케이션의 OpenAPI 및 문서에는 마운트된 애플리케이션의 내용 등이 포함되지 않습니다. -자세한 내용은 **숙련된 사용자 안내서**에서 확인할 수 있습니다. +자세한 내용은 [고급 사용자 가이드](../advanced/index.md){.internal-link target=_blank}에서 확인할 수 있습니다. -## 세부사항 +## 세부사항 { #details } -첫 번째 `"/static"`은 이 "하위 응용 프로그램"이 "마운트"될 하위 경로를 가리킵니다. 따라서 `"/static"`으로 시작하는 모든 경로는 `"/static"`으로 처리됩니다. +첫 번째 `"/static"`은 이 "하위 애플리케이션"이 "마운트"될 하위 경로를 가리킵니다. 따라서 `"/static"`으로 시작하는 모든 경로는 이 애플리케이션이 처리합니다. -`'directory="static"`은 정적 파일이 들어 있는 디렉토리의 이름을 나타냅니다. +`directory="static"`은 정적 파일이 들어 있는 디렉터리의 이름을 나타냅니다. `name="static"`은 **FastAPI**에서 내부적으로 사용할 수 있는 이름을 제공합니다. -이 모든 매개변수는 "`static`"과 다를 수 있으며, 사용자 응용 프로그램의 요구 사항 및 구체적인 세부 정보에 따라 매개변수를 조정할 수 있습니다. +이 모든 매개변수는 "`static`"과 다를 수 있으며, 여러분의 애플리케이션 요구 사항 및 구체적인 세부 정보에 맞게 조정하세요. +## 추가 정보 { #more-info } -## 추가 정보 - -자세한 내용과 선택 사항을 보려면 <a href="https://www.starlette.dev/staticfiles/" class="external-link" target="_blank">Starlette의 정적 파일에 관한 문서</a>를 확인하십시오. +자세한 내용과 옵션은 <a href="https://www.starlette.dev/staticfiles/" class="external-link" target="_blank">Starlette의 정적 파일 문서</a>를 확인하세요. diff --git a/docs/ko/docs/tutorial/testing.md b/docs/ko/docs/tutorial/testing.md index 915ff6d22..db7fb17ea 100644 --- a/docs/ko/docs/tutorial/testing.md +++ b/docs/ko/docs/tutorial/testing.md @@ -1,18 +1,18 @@ -# 테스팅 +# 테스팅 { #testing } -<a href="https://www.starlette.dev/testclient/" class="external-link" target="_blank">Starlette</a> 덕분에 **FastAPI** 를 테스트하는 일은 쉽고 즐거운 일이 되었습니다. +<a href="https://www.starlette.dev/testclient/" class="external-link" target="_blank">Starlette</a> 덕분에 **FastAPI** 애플리케이션을 테스트하는 일은 쉽고 즐거운 일이 되었습니다. -Starlette는 <a href="https://www.python-httpx.org\" class="external-link" target="_blank">HTTPX</a>를 기반으로 하며, 이는 Requests를 기반으로 설계되었기 때문에 매우 친숙하고 직관적입니다. +Starlette는 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>를 기반으로 하며, 이는 Requests를 기반으로 설계되었기 때문에 매우 친숙하고 직관적입니다. -이를 사용하면 FastAPI에서 <a href="https://docs.pytest.org/" class="external-link" target="_blank">pytest</a>를 직접 사용할 수 있습니다. +이를 사용하면 **FastAPI**에서 <a href="https://docs.pytest.org/" class="external-link" target="_blank">pytest</a>를 직접 사용할 수 있습니다. -## `TestClient` 사용하기 +## `TestClient` 사용하기 { #using-testclient } /// info | 정보 -`TestClient` 사용하려면, 우선 <a href="https://www.python-httpx.org" class="external-link" target="_blank">`httpx`</a> 를 설치해야 합니다. +`TestClient` 사용하려면, 우선 <a href="https://www.python-httpx.org" class="external-link" target="_blank">`httpx`</a>를 설치해야 합니다. -[virtual environment](../virtual-environments.md){.internal-link target=_blank} 를 만들고, 활성화 시킨 뒤에 설치하세요. 예시: +[virtual environment](../virtual-environments.md){.internal-link target=_blank}를 만들고, 활성화 시킨 뒤에 설치하세요. 예시: ```console $ pip install httpx @@ -20,52 +20,51 @@ $ pip install httpx /// -`TestClient` 를 임포트하세요. +`TestClient`를 임포트하세요. -**FastAPI** 어플리케이션을 전달하여 `TestClient` 를 만드세요. +**FastAPI** 애플리케이션을 전달하여 `TestClient`를 만드세요. -이름이 `test_` 로 시작하는 함수를 만드세요(`pytest` 의 표준적인 관례입니다). +이름이 `test_`로 시작하는 함수를 만드세요(`pytest`의 표준적인 관례입니다). -`httpx` 를 사용하는 것과 같은 방식으로 `TestClient` 객체를 사용하세요. +`httpx`를 사용하는 것과 같은 방식으로 `TestClient` 객체를 사용하세요. -표준적인 파이썬 문법을 이용하여 확인이 필요한 곳에 간단한 `assert` 문장을 작성하세요(역시 표준적인 `pytest` 관례입니다). +표준적인 파이썬 표현식으로 확인이 필요한 곳에 간단한 `assert` 문장을 작성하세요(역시 표준적인 `pytest` 관례입니다). -{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *} +{* ../../docs_src/app_testing/tutorial001_py39.py hl[2,12,15:18] *} /// tip | 팁 -테스트를 위한 함수는 `async def` 가 아니라 `def` 로 작성됨에 주의하세요. +테스트를 위한 함수는 `async def`가 아니라 `def`로 작성됨에 주의하세요. -그리고 클라이언트에 대한 호출도 `await` 를 사용하지 않는 일반 호출입니다. +그리고 클라이언트에 대한 호출도 `await`를 사용하지 않는 일반 호출입니다. -이렇게 하여 복잡한 과정 없이 `pytest` 를 직접적으로 사용할 수 있습니다. +이렇게 하여 복잡한 과정 없이 `pytest`를 직접적으로 사용할 수 있습니다. /// -/// note | 기술 세부사항 +/// note Technical Details | 기술 세부사항 `from starlette.testclient import TestClient` 역시 사용할 수 있습니다. -**FastAPI** 는 개발자의 편의를 위해 `starlette.testclient` 를 `fastapi.testclient` 로도 제공할 뿐입니다. 이는 단지 `Starlette` 에서 직접 가져오는지의 차이일 뿐입니다. +**FastAPI**는 개발자의 편의를 위해 `starlette.testclient`를 `fastapi.testclient`로도 제공할 뿐입니다. 하지만 이는 Starlette에서 직접 가져옵니다. /// /// tip | 팁 -FastAPI 애플리케이션에 요청을 보내는 것 외에도 테스트에서 `async` 함수를 호출하고 싶다면 (예: 비동기 데이터베이스 함수), 심화 튜토리얼의 [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} 를 참조하세요. +FastAPI 애플리케이션에 요청을 보내는 것 외에도 테스트에서 `async` 함수를 호출하고 싶다면 (예: 비동기 데이터베이스 함수), 심화 튜토리얼의 [Async Tests](../advanced/async-tests.md){.internal-link target=_blank}를 참조하세요. /// -## 테스트 분리하기 +## 테스트 분리하기 { #separating-tests } 실제 애플리케이션에서는 테스트를 별도의 파일로 나누는 경우가 많습니다. - 그리고 **FastAPI** 애플리케이션도 여러 파일이나 모듈 등으로 구성될 수 있습니다. -### **FastAPI** app 파일 +### **FastAPI** app 파일 { #fastapi-app-file } -[Bigger Applications](bigger-applications.md){.internal-link target=_blank} 에 묘사된 파일 구조를 가지고 있는 것으로 가정해봅시다. +[Bigger Applications](bigger-applications.md){.internal-link target=_blank}에 묘사된 파일 구조를 가지고 있는 것으로 가정해봅시다. ``` . @@ -76,11 +75,12 @@ FastAPI 애플리케이션에 요청을 보내는 것 외에도 테스트에서 `main.py` 파일 안에 **FastAPI** app 을 만들었습니다: -{* ../../docs_src/app_testing/main.py *} -### 테스트 파일 +{* ../../docs_src/app_testing/app_a_py39/main.py *} -테스트를 위해 `test_main.py` 라는 파일을 생성할 수 있습니다. 이 파일은 동일한 Python 패키지(즉, `__init__.py` 파일이 있는 동일한 디렉터리)에 위치할 수 있습니다. +### 테스트 파일 { #testing-file } + +테스트를 위해 `test_main.py`라는 파일을 생성할 수 있습니다. 이 파일은 동일한 Python 패키지(즉, `__init__.py` 파일이 있는 동일한 디렉터리)에 위치할 수 있습니다. ``` hl_lines="5" . @@ -90,18 +90,18 @@ FastAPI 애플리케이션에 요청을 보내는 것 외에도 테스트에서 │   └── test_main.py ``` -파일들이 동일한 패키지에 위치해 있으므로, 상대 참조를 사용하여 `main` 에서 `app` 객체를 임포트 해올 수 있습니다. +파일들이 동일한 패키지에 위치해 있으므로, 상대 임포트를 사용하여 `main` 모듈(`main.py`)에서 `app` 객체를 임포트 해올 수 있습니다. -{* ../../docs_src/app_testing/test_main.py hl[3] *} +{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *} ...그리고 이전에 작성했던 것과 같은 테스트 코드를 작성할 수 있습니다. -## 테스트: 확장된 예시 +## 테스트: 확장된 예시 { #testing-extended-example } 이제 위의 예시를 확장하고 더 많은 세부 사항을 추가하여 다양한 부분을 어떻게 테스트하는지 살펴보겠습니다. -### 확장된 FastAPI 애플리케이션 파일 +### 확장된 **FastAPI** app 파일 { #extended-fastapi-app-file } 이전과 같은 파일 구조를 계속 사용해 보겠습니다. @@ -113,100 +113,50 @@ FastAPI 애플리케이션에 요청을 보내는 것 외에도 테스트에서 │   └── test_main.py ``` -이제 **FastAPI** 앱이 있는 `main.py` 파일에 몇 가지 다른 **경로 작업** 이 추가된 경우를 생각해봅시다. +이제 **FastAPI** 앱이 있는 `main.py` 파일에 몇 가지 다른 **경로 처리**가 추가된 경우를 생각해봅시다. 단일 오류를 반환할 수 있는 `GET` 작업이 있습니다. 여러 다른 오류를 반환할 수 있는 `POST` 작업이 있습니다. -두 *경로 작업* 모두 `X-Token` 헤더를 요구합니다. +두 *경로 처리* 모두 `X-Token` 헤더를 요구합니다. -//// tab | Python 3.10+ +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} -```Python -{!> ../../docs_src/app_testing/app_b_an_py310/main.py!} -``` +### 확장된 테스트 파일 { #extended-testing-file } -//// +이제는 `test_main.py`를 확장된 테스트들로 수정할 수 있습니다: -//// tab | Python 3.9+ - -```Python -{!> ../../docs_src/app_testing/app_b_an_py39/main.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python -{!> ../../docs_src/app_testing/app_b_an/main.py!} -``` - -//// - -//// tab | Python 3.10+ non-Annotated - -/// tip | 팁 - -될 수 있으면 `Annotated` 버전 사용을 권장합나다. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b_py310/main.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated - -/// tip | 팁 - -될 수 있으면 `Annotated` 버전 사용을 권장합나다. - -/// - -```Python -{!> ../../docs_src/app_testing/app_b/main.py!} -``` - -//// - -### 확장된 테스트 파일 - -이제는 `test_main.py` 를 확장된 테스트들로 수정할 수 있습니다: - -{* ../../docs_src/app_testing/app_b/test_main.py *} +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} -클라이언트가 요청에 정보를 전달해야 하는데 방법을 모르겠다면, `httpx`에서 해당 작업을 수행하는 방법을 검색(Google)하거나, `requests`에서의 방법을 검색해보세요. HTTPX는 Requests의 디자인을 기반으로 설계되었습니다. +클라이언트가 요청에 정보를 전달해야 하는데 방법을 모르겠다면, Requests의 디자인을 기반으로 설계된 HTTPX처럼 `httpx`에서 해당 작업을 수행하는 방법을 검색(Google)하거나, `requests`에서의 방법을 검색해보세요. 그 후, 테스트에서도 동일하게 적용하면 됩니다. 예시: * *경로* 혹은 *쿼리* 매개변수를 전달하려면, URL 자체에 추가한다. -* JSON 본문을 전달하려면, 파이썬 객체 (예를들면 `dict`) 를 `json` 파라미터로 전달한다. -* JSON 대신 *폼 데이터* 를 보내야한다면, `data` 파라미터를 대신 전달한다. -* *헤더* 를 전달하려면, `headers` 파라미터에 `dict` 를 전달한다. -* *쿠키* 를 전달하려면, `cookies` 파라미터에 `dict` 를 전달한다. +* JSON 본문을 전달하려면, 파이썬 객체 (예를들면 `dict`)를 `json` 파라미터로 전달한다. +* JSON 대신 *폼 데이터*를 보내야한다면, `data` 파라미터를 대신 전달한다. +* *헤더*를 전달하려면, `headers` 파라미터에 `dict`를 전달한다. +* *쿠키*를 전달하려면, `cookies` 파라미터에 `dict`를 전달한다. -백엔드로 데이터를 어떻게 보내는지 정보를 더 얻으려면 (`httpx` 혹은 `TestClient` 를 이용해서) <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX documentation</a> 를 확인하세요. +백엔드로 데이터를 어떻게 보내는지 정보를 더 얻으려면 (`httpx` 혹은 `TestClient`를 이용해서) <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX documentation</a>를 확인하세요. /// info | 정보 -`TestClient` 는 Pydantic 모델이 아니라 JSON 으로 변환될 수 있는 데이터를 받습니다. +`TestClient`는 Pydantic 모델이 아니라 JSON으로 변환될 수 있는 데이터를 받습니다. -만약 테스트중 Pydantic 모델을 어플리케이션으로에 보내고 싶다면, [JSON 호환 가능 인코더](encoder.md){.internal-link target=_blank} 에 설명되어 있는 `jsonable_encoder` 를 사용할 수 있습니다. +만약 테스트 중 Pydantic 모델을 가지고 있고 테스트 중에 애플리케이션으로 해당 데이터를 보내고 싶다면, [JSON Compatible Encoder](encoder.md){.internal-link target=_blank}에 설명되어 있는 `jsonable_encoder`를 사용할 수 있습니다. /// -## 실행하기 +## 실행하기 { #run-it } -테스트 코드를 작성하고, `pytest` 를 설치해야합니다. +그 후에는 `pytest`를 설치하기만 하면 됩니다. -[virtual environment](../virtual-environments.md){.internal-link target=_blank} 를 만들고, 활성화 시킨 뒤에 설치하세요. 예시: +[virtual environment](../virtual-environments.md){.internal-link target=_blank}를 만들고, 활성화 시킨 뒤에 설치하세요. 예시: <div class="termy"> @@ -218,7 +168,7 @@ $ pip install pytest </div> -`pytest` 파일과 테스트를 자동으로 감지하고 실행한 다음, 결과를 보고할 것입니다. +`pytest`는 파일과 테스트를 자동으로 감지하고 실행한 다음, 결과를 보고할 것입니다. 테스트를 다음 명령어로 실행하세요. diff --git a/docs/ko/docs/virtual-environments.md b/docs/ko/docs/virtual-environments.md index 0d10c3200..b639f8a3e 100644 --- a/docs/ko/docs/virtual-environments.md +++ b/docs/ko/docs/virtual-environments.md @@ -1,16 +1,16 @@ -# 가상 환경 +# 가상 환경 { #virtual-environments } -Python 프로젝트를 작업할 때는 **가상 환경** (또는 이와 유사한 도구)을 사용하는 것이 좋습니다. 각 프로젝트 마다 설치하는 패키지를 분리하여 관리할 수 있습니다. +Python 프로젝트를 작업할 때는 **가상 환경**(또는 이와 유사한 메커니즘)을 사용해 각 프로젝트마다 설치하는 패키지를 분리하는 것이 좋습니다. -/// info | 정보 +/// info -이미 가상 환경에 대해 잘 알고 있다면, 이 섹션은 건너 뛰어도 괜찮습니다. 🤓 +이미 가상 환경에 대해 알고 있고, 어떻게 생성하고 사용하는지도 알고 있다면, 이 섹션은 건너뛰어도 괜찮습니다. 🤓 /// -/// tip | 팁 +/// tip -**가상 환경(Virtual Environment)** 은 **환경 변수(Environment Variable)** 와 다릅니다. +**가상 환경**은 **환경 변수**와 다릅니다. **환경 변수**는 시스템에 존재하며, 프로그램이 사용할 수 있는 변수입니다. @@ -18,50 +18,52 @@ Python 프로젝트를 작업할 때는 **가상 환경** (또는 이와 유사 /// -/// info | 정보 +/// info -이 페이지에서는 **가상 환경**의 사용 방법과 작동 방식을 설명합니다. +이 페이지에서는 **가상 환경**을 사용하는 방법과 작동 방식을 알려드립니다. -만약 **모든 것을 관리해주는 도구** (Python 설치까지 포함)를 사용하고 싶다면 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>를 사용해보세요. +Python 설치까지 포함해 **모든 것을 관리해주는 도구**를 도입할 준비가 되었다면 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>를 사용해 보세요. /// -## 프로젝트 생성 +## 프로젝트 생성 { #create-a-project } 먼저, 프로젝트를 위한 디렉터리를 하나 생성합니다. -보통 사용자 홈 디렉터리 안에 `code`라는 디렉터리를 만들고, 그 안에 프로젝트마다 하나씩 디렉터리를 만들어 관리합니다. +제가 보통 하는 방법은 사용자 홈/유저 디렉터리 안에 `code`라는 디렉터리를 만드는 것입니다. + +그리고 그 안에 프로젝트마다 디렉터리를 하나씩 만듭니다. <div class="termy"> ```console -// 홈 디렉터리로 이동 +// Go to the home directory $ cd -// 모든 코드 프로젝트를 위한 디렉터리 생성 +// Create a directory for all your code projects $ mkdir code -// code 디렉터리로 이동 +// Enter into that code directory $ cd code -// 이번 프로젝트를 위한 디렉터리 생성 +// Create a directory for this project $ mkdir awesome-project -// 해당 프로젝트 디렉터리로 이동 +// Enter into that project directory $ cd awesome-project ``` </div> -## 가상 환경 생성 +## 가상 환경 생성 { #create-a-virtual-environment } -Python 프로젝트를 **처음 시작할 때**, 가상 환경을 **<abbr title="다른 방법들도 있지만, 이건 간단한 가이드라인입니다">프로젝트 내부</abbr>**에 생성합니다. +Python 프로젝트를 **처음 시작할 때**, **<abbr title="다른 옵션도 있지만, 이것은 간단한 가이드라인입니다">프로젝트 내부</abbr>**에 가상 환경을 생성하세요. -/// tip | 팁 +/// tip -이 작업은 **프로젝트를 처음 설정할 때 한번만** 해주면 됩니다. 이후 작업할 때 반복할 필요는 없습니다. +이 작업은 **프로젝트당 한 번만** 하면 되며, 작업할 때마다 할 필요는 없습니다. /// //// tab | `venv` -Python 표준 라이브러리에 포함된 venv 모듈을 사용해 가상 환경을 생성할 수 있습니다. +가상 환경을 만들려면 Python에 포함된 `venv` 모듈을 사용할 수 있습니다. <div class="termy"> @@ -71,12 +73,12 @@ $ python -m venv .venv </div> -/// details | 명령어 상세 설명 +/// details | 명령어 의미 -* `python`: `python` 프로그램을 실행합니다. -* `-m`: 특정 모듈을 스크립트처럼 실행합니다. 대상 모듈을 바로 뒤에 지정합니다. -* `venv`: Python 표준 라이브러리에 포함된 `venv` 모듈을 실행합니다. -* `.venv`: 가상 환경을 `.venv` 디렉터리에 생성합니다. +* `python`: `python`이라는 프로그램을 사용합니다 +* `-m`: 모듈을 스크립트로 호출합니다. 다음에 어떤 모듈인지 지정합니다 +* `venv`: 보통 Python에 기본으로 설치되어 있는 `venv` 모듈을 사용합니다 +* `.venv`: 새 디렉터리인 `.venv`에 가상 환경을 생성합니다 /// @@ -84,7 +86,7 @@ $ python -m venv .venv //// tab | `uv` -<a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>가 설치되어 있다면, uv를 통해 가상 환경을 생성할 수 있습니다. +<a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>가 설치되어 있다면, 이를 사용해 가상 환경을 생성할 수 있습니다. <div class="termy"> @@ -94,31 +96,31 @@ $ uv venv </div> -/// tip | 팁 +/// tip -`uv`는 기본적으로 `.venv` 디렉터리에 가상 환경을 생성합니다. +기본적으로 `uv`는 `.venv`라는 디렉터리에 가상 환경을 생성합니다. -별도로 디렉터리 이름을 추가 인자로 넘겨 주면 경로를 지정 할 수 있습니다. +하지만 디렉터리 이름을 추가 인자로 전달해 이를 커스터마이즈할 수 있습니다. /// //// -해당 명령어는 `.venv` 디렉터리에 새로운 가상 환경을 생성합니다. +해당 명령어는 `.venv`라는 디렉터리에 새로운 가상 환경을 생성합니다. /// details | `.venv` 또는 다른 이름 -가상 환경을 다른 디렉터리에 생성할 수도 있지만, 관례적으로 `.venv` 디렉터리 이름을 사용합니다. +가상 환경을 다른 디렉터리에 생성할 수도 있지만, 관례적으로 `.venv`라는 이름을 사용합니다. /// -## 가상 환경 활성화 +## 가상 환경 활성화 { #activate-the-virtual-environment } -이후 실행하는 Python 명령어와 패키지 설치가 가상 환경을 따르도록, 가상 환경을 활성화하세요. +이후 실행하는 Python 명령어와 설치하는 패키지가 새 가상 환경을 사용하도록, 새 가상 환경을 활성화하세요. -/// tip | 팁 +/// tip -**터미널을 새로 열고** 프로젝트 작업을 시작할 때는, **항상 이 작업을** 해주세요. +프로젝트 작업을 위해 **새 터미널 세션**을 시작할 때마다 **매번** 이 작업을 하세요. /// @@ -148,7 +150,7 @@ $ .venv\Scripts\Activate.ps1 //// tab | Windows Bash -Windows에서 Bash(예: <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>)를 사용하는 경우: +또는 Windows에서 Bash(예: <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>)를 사용하는 경우: <div class="termy"> @@ -160,21 +162,21 @@ $ source .venv/Scripts/activate //// -/// tip | 팁 +/// tip -가상 환경에 새로운 패키지를 설치할 때마다, 해당 환경을 다시 활성화하세요. +해당 환경에 **새 패키지**를 설치할 때마다, 환경을 다시 **활성화**하세요. -이렇게 하면 해당 패키지로 설치된 **터미널(<abbr title="command line interface">CLI</abbr>) 프로그램**을 사용할 때, 전역에 설치된 다른 버전이 아니라, 가상 환경 안에 설치된 정확한 버전을 사용합니다. +이렇게 하면 해당 패키지가 설치한 **터미널(<abbr title="command line interface">CLI</abbr>) 프로그램**을 사용할 때, 전역으로 설치되어 있을 수도 있는(아마 필요한 버전과는 다른 버전인) 다른 프로그램이 아니라 가상 환경에 있는 것을 사용하게 됩니다. /// -## 가상 환경이 활성화 여부 확인 +## 가상 환경 활성화 여부 확인 { #check-the-virtual-environment-is-active } -가상 환경이 활성화되었는지 확인합니다. (이전 명령어가 제대로 작동했는지 확인합니다). +가상 환경이 활성화되어 있는지(이전 명령어가 작동했는지) 확인합니다. -/// tip | 팁 +/// tip -이 단계는 **선택 사항**이지만, 모든 것이 예상대로 작동하고 있는지, 그리고 의도한 가상 환경이 활성화 되었는 지 **확인**하는 좋은 방법입니다. +이 단계는 **선택 사항**이지만, 모든 것이 예상대로 작동하고 있는지, 그리고 의도한 가상 환경을 사용하고 있는지 **확인**하는 좋은 방법입니다. /// @@ -190,7 +192,7 @@ $ which python </div> -`python` 위치가 프로젝트 내부(이 예시에서는 `awesome-project`)의 `.venv/bin/python` 경로로 표시된다면 성공입니다. 🎉 +프로젝트 내부(이 경우 `awesome-project`)의 `.venv/bin/python`에 있는 `python` 바이너리가 표시된다면, 정상적으로 작동한 것입니다. 🎉 //// @@ -206,29 +208,29 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python </div> -`python` 위치가 프로젝트 내부(이 예시에서는 `awesome-project`)의 `.venv\bin\python` 경로로 표시된다면 성공입니다. 🎉 +프로젝트 내부(이 경우 `awesome-project`)의 `.venv\Scripts\python`에 있는 `python` 바이너리가 표시된다면, 정상적으로 작동한 것입니다. 🎉 //// -## pip 업그레이드 +## `pip` 업그레이드 { #upgrade-pip } -/// tip | 팁 +/// tip -<a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>를 사용한다면, `pip` 대신 `uv`로 패키지를 설치하게 되므로 `pip`을 업그레이드할 필요가 없습니다. 😎 +<a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>를 사용한다면, `pip` 대신 `uv`로 설치하게 되므로 `pip`을 업그레이드할 필요가 없습니다. 😎 /// -`pip`을 사용하여 패키지를 설치하는 경우 (Python 표준 라이브러리에 포함되어 있습니다), **최신 버전으로 업그레이드**하는 것이 좋습니다. +`pip`로 패키지를 설치한다면(Python에 기본으로 포함되어 있습니다) 최신 버전으로 **업그레이드**하는 것이 좋습니다. -패키지 설치 중 발생하는 다양하고 특이한 에러들은 `pip` 업그레이드로 쉽게 해결되는 경우가 많습니다. +패키지 설치 중 발생하는 다양한 특이한 오류는 먼저 `pip`를 업그레이드하는 것만으로 해결되는 경우가 많습니다. -/// tip | 팁 +/// tip -이 작업은 보통 가상 환경을 생성한 **직후 한 번만** 하면 됩니다. +보통 이 작업은 가상 환경을 만든 직후 **한 번만** 하면 됩니다. /// -가상 환경이 활성화된 상태인지 확인한 후(앞서 설명한 명령어 사용), 아래 명령어를 실행하세요: +가상 환경이 활성화된 상태인지 확인한 다음(위의 명령어 사용) 아래를 실행하세요: <div class="termy"> @@ -240,19 +242,39 @@ $ python -m pip install --upgrade pip </div> -## `.gitignore` 추가하기 +/// tip -**Git**을 사용하고 있다면 (사용하는 것이 좋습니다), `.gitignore` 파일을 추가해서 `.venv` 디렉터리 전체를 Git에서 제외하세요. +때로는 pip를 업그레이드하려고 할 때 **`No module named pip`** 오류가 발생할 수 있습니다. -/// tip | 팁 +이 경우 아래 명령어로 pip를 설치하고 업그레이드하세요: -<a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>를 사용해 가상 환경을 생성했다면, 이미 이 작업이 자동으로 처리되어 있으므로 이 단계는 건너뛰어도 됩니다. 😎 +<div class="termy"> + +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +</div> + +이 명령어는 pip가 아직 설치되어 있지 않다면 설치하며, 설치된 pip 버전이 `ensurepip`에서 제공 가능한 버전만큼 최신임을 보장합니다. /// -/// tip | 팁 +## `.gitignore` 추가하기 { #add-gitignore } -이 작업도 마찬가지로, 가상 환경을 생성한 **직후 한 번만** 하면 됩니다. +**Git**을 사용하고 있다면(사용하는 것이 좋습니다), `.venv`의 모든 내용을 Git에서 제외하도록 `.gitignore` 파일을 추가하세요. + +/// tip + +<a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>로 가상 환경을 만들었다면, 이미 자동으로 처리되어 있으므로 이 단계는 건너뛰어도 됩니다. 😎 + +/// + +/// tip + +가상 환경을 만든 직후 **한 번만** 하면 됩니다. /// @@ -264,16 +286,15 @@ $ echo "*" > .venv/.gitignore </div> -/// details | 명령어 상세 설명 +/// details | 명령어 의미 -* `echo "*"`: 터미널에 `*` 텍스트를 "출력"합니다 (다음 설명에서 조금 바뀝니다) -* `>`: 왼쪽 명령어의 출력 내용을 터미널에 출력하지 않고, 오른쪽에 지정된 파일로 **기록(write)** 하라는 의미입니다. -* `.gitignore`: 출력된 텍스트가 기록될 파일 이름입니다. +* `echo "*"`: 터미널에 `*` 텍스트를 "출력"합니다(다음 부분이 이를 약간 변경합니다) +* `>`: `>` 왼쪽 명령어가 터미널에 출력한 내용을 터미널에 출력하지 않고, `>` 오른쪽에 있는 파일에 기록하라는 의미입니다 +* `.gitignore`: 텍스트가 기록될 파일 이름입니다 -그리고 Git에서 `*`는 "모든 것"을 의미합니다. 따라서 `.venv` 디렉터리 안의 모든 것을 무시하게 됩니다. - -이 명령어는 다음과 같은 내용을 가진 `.gitignore` 파일을 생성합니다: +그리고 Git에서 `*`는 "모든 것"을 의미합니다. 따라서 `.venv` 디렉터리 안의 모든 것을 무시합니다. +이 명령어는 다음 내용을 가진 `.gitignore` 파일을 생성합니다: ```gitignore * @@ -281,25 +302,25 @@ $ echo "*" > .venv/.gitignore /// -## 패키지 설치 +## 패키지 설치 { #install-packages } -가상 환경을 활성화한 후, 그 안에 필요한 패키지들을 설치할 수 있습니다. +환경을 활성화한 뒤, 그 안에 패키지를 설치할 수 있습니다. -/// tip | 팁 +/// tip -프로젝트에서 필요한 패키지를 설치하거나 업그레이드할 때는 이 작업을 **한 번만** 하면 됩니다. +프로젝트에 필요한 패키지를 설치하거나 업그레이드할 때는 **한 번**만 하면 됩니다. -만약 특정 패키지의 버전을 업그레이드하거나, 새로운 패키지를 추가할 필요가 생기면 **다시 이 작업을 반복**하면 됩니다. +버전을 업그레이드하거나 새 패키지를 추가해야 한다면 **다시 이 작업을** 하게 됩니다. /// -### 패키지 직접 설치 +### 패키지 직접 설치 { #install-packages-directly } -급하게 작업하거나, 프로젝트에 필요한 패키지 목록을 따로 파일로 관리하고 싶지 않은 경우, 패키지를 직접 설치할 수도 있습니다. +급하게 작업 중이고 프로젝트의 패키지 요구사항을 선언하는 파일을 사용하고 싶지 않다면, 패키지를 직접 설치할 수 있습니다. -/// tip | 팁 +/// tip -패키지 이름과 버전 정보를 파일에 정리해두는 것(예: `requirements.txt` 또는 `pyproject.toml`)은 (매우) 좋은 생각입니다. +프로그램에 필요한 패키지와 버전을 파일(예: `requirements.txt` 또는 `pyproject.toml`)에 적어두는 것은 (매우) 좋은 생각입니다. /// @@ -319,7 +340,7 @@ $ pip install "fastapi[standard]" //// tab | `uv` -<a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>를 사용하는 경우: +<a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>가 있다면: <div class="termy"> @@ -332,9 +353,9 @@ $ uv pip install "fastapi[standard]" //// -### `requirements.txt`에서 설치 +### `requirements.txt`에서 설치 { #install-from-requirements-txt } -`requirements.txt` 파일이 있다면, 그 안에 명시된 패키지들을 한 번에 설치할 수 있습니다. +`requirements.txt`가 있다면, 이제 이를 사용해 그 안의 패키지를 설치할 수 있습니다. //// tab | `pip` @@ -351,7 +372,7 @@ $ pip install -r requirements.txt //// tab | `uv` -<a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>를 사용하는 경우: +<a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>가 있다면: <div class="termy"> @@ -366,7 +387,7 @@ $ uv pip install -r requirements.txt /// details | `requirements.txt` -다음은 몇 가지 패키지를 포함한 `requirements.txt`의 예시입니다: +일부 패키지가 있는 `requirements.txt`는 다음과 같이 생겼을 수 있습니다: ```requirements.txt fastapi[standard]==0.113.0 @@ -375,9 +396,9 @@ pydantic==2.8.0 /// -## 프로그램 실행 +## 프로그램 실행 { #run-your-program } -가상 환경을 활성화한 후에는 프로그램을 실행할 수 있습니다. 이때 해당 가상 환경에 설치된 Python과 패키지들이 사용됩니다. +가상 환경을 활성화한 뒤에는 프로그램을 실행할 수 있으며, 설치한 패키지가 들어있는 가상 환경 내부의 Python을 사용하게 됩니다. <div class="termy"> @@ -389,25 +410,24 @@ Hello World </div> -## 에디터 설정 +## 에디터 설정 { #configure-your-editor } -에디터를 사용할 경우, 앞서 만든 가상 환경을 사용하도록 설정하는 것이 좋습니다. (대부분의 에디터는 자동으로 감지하기도 합니다.) -이렇게 하면 자동 완성 기능이나 코드 내 오류 표시 기능을 제대로 사용할 수 있습니다. +아마 에디터를 사용할 텐데, 자동 완성과 인라인 오류 표시를 받을 수 있도록 생성한 가상 환경을 사용하도록 설정하세요(대부분 자동 감지합니다). -예시: +예를 들면: * <a href="https://code.visualstudio.com/docs/python/environments#_select-and-activate-an-environment" class="external-link" target="_blank">VS Code</a> * <a href="https://www.jetbrains.com/help/pycharm/creating-virtual-environment.html" class="external-link" target="_blank">PyCharm</a> -/// tip | 팁 +/// tip -이 설정은 보통 가상 환경을 **처음 만들었을 때 한 번만** 해주면 됩니다. +보통 이 설정은 가상 환경을 만들 때 **한 번만** 하면 됩니다. /// -## 가상 환경 비활성화 +## 가상 환경 비활성화 { #deactivate-the-virtual-environment } -프로젝트 작업이 끝났다면, 가상 환경을 **비활성화**할 수 있습니다. +프로젝트 작업을 마쳤다면 가상 환경을 **비활성화**할 수 있습니다. <div class="termy"> @@ -417,54 +437,55 @@ $ deactivate </div> -이렇게 하면 이후에 `python` 명령어를 실행했을 때, 가상 환경의 Python이나 그 안에 설치된 패키지들을 사용하지 않게 됩니다. +이렇게 하면 `python`을 실행할 때, 해당 가상 환경과 그 안에 설치된 패키지에서 실행하려고 하지 않습니다. -## 이제 작업할 준비가 되었습니다 +## 작업할 준비 완료 { #ready-to-work } -이제 프로젝트 작업을 시작할 준비가 완료되었습니다. +이제 프로젝트 작업을 시작할 준비가 되었습니다. -/// tip | 팁 -위 내용을 더 깊이 이해하고 싶으신가요? +/// tip -그렇다면 계속 읽어 주세요. 👇🤓 +위의 내용이 무엇인지 더 이해하고 싶으신가요? + +계속 읽어보세요. 👇🤓 /// -## 가상 환경을 왜 사용하는가 +## 가상 환경을 왜 사용하나요 { #why-virtual-environments } -FastAPI를 사용하려면 먼저 <a href="https://www.python.org/" class="external-link" target="_blank">Python</a>을 설치해야 합니다. +FastAPI로 작업하려면 <a href="https://www.python.org/" class="external-link" target="_blank">Python</a>을 설치해야 합니다. -그 후에는 FastAPI와 함께 사용할 **기타 패키지들**을 **설치**해야 합니다. +그 다음 FastAPI와 사용하려는 다른 **패키지**를 **설치**해야 합니다. -패키지를 설치할 때 보통 Python에 기본 포함된 `pip` 명령어(또는 유사한 도구)를 사용합니다. +패키지를 설치할 때는 보통 Python에 포함된 `pip` 명령어(또는 유사한 대안)를 사용합니다. -하지만 `pip`을 그냥 직접 사용하면, 해당 패키지들은 **전역 Python 환경**(시스템 전체에 설치된 Python)에 설치됩니다. +하지만 `pip`를 그대로 직접 사용하면, 패키지는 **전역 Python 환경**(전역 Python 설치)에 설치됩니다. -### 문제점 +### 문제점 { #the-problem } -그렇다면, 전역 Python 환경에 패키지를 설치하면 어떤 문제가 발생할까요? +그렇다면, 전역 Python 환경에 패키지를 설치하면 어떤 문제가 있을까요? -어느 시점이 되면, **서로 다른 패키지들**에 의존하는 여러 개의 프로그램을 작성하게 될 것입니다. 그리고 이들 중 일부는 **같은 패키지의 서로 다른 버전**을 필요로 할 수 있습니다. 😱 +어느 시점이 되면 **서로 다른 패키지**에 의존하는 다양한 프로그램을 작성하게 될 것입니다. 그리고 작업하는 프로젝트 중 일부는 같은 패키지의 **서로 다른 버전**에 의존할 수도 있습니다. 😱 -예를 들어, `마법사의 돌(philosophers-stone)` 프로젝트를 만들었다고 가정해봅시다. 이 프로그램은 `해리 포터(harry)`라는 패키지의 `v1` 버전을 **의존**합니다. 따라서 `harry`를 설치해야 합니다. +예를 들어 `philosophers-stone`이라는 프로젝트를 만들 수 있습니다. 이 프로그램은 **`harry`라는 다른 패키지의 버전 `1`**에 의존합니다. 그래서 `harry`를 설치해야 합니다. ```mermaid flowchart LR stone(philosophers-stone) -->|requires| harry-1[harry v1] ``` -그런데 나중에 `아즈카반의 죄수(prisoner-of-azkaban)`이라는 또 다른 프로젝트를 만들게 되었고, 이 프로젝트도 역시 `harry` 패키지를 사용합니다. 그런데 이 프로젝트는 `harry`의 `v3` 버전이 필요합니다. +그다음, 나중에 `prisoner-of-azkaban`이라는 또 다른 프로젝트를 만들고, 이 프로젝트도 `harry`에 의존하지만, 이 프로젝트는 **`harry` 버전 `3`**이 필요합니다. ```mermaid flowchart LR azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3] ``` -하지만 이제 문제가 생깁니다. 로컬 가상 환경 대신에 전역 환경에 패키지를 설치하게 되면, 어떤 버전의 `harry`를 설치할지를 선택해야 하기 때문입니다. +하지만 이제 문제가 생깁니다. 로컬 **가상 환경**이 아니라 전역(전역 환경)에 패키지를 설치한다면, 어떤 버전의 `harry`를 설치할지 선택해야 합니다. -예를 들어, `마법사의 돌(philosophers-stone)`을 실행하고 싶다면 먼저 `harry` `v1` 버전을 다음과 같이 설치 해야 합니다: +`philosophers-stone`을 실행하고 싶다면, 먼저 `harry` 버전 `1`을 다음과 같이 설치해야 합니다: <div class="termy"> @@ -474,7 +495,7 @@ $ pip install "harry==1" </div> -그러면 결국 전역 Python 환경에는 `harry` `v1`버전이 설치된 상태가 됩니다. +그리고 전역 Python 환경에 `harry` 버전 `1`이 설치된 상태가 됩니다. ```mermaid flowchart LR @@ -486,7 +507,7 @@ flowchart LR end ``` -하지만 이제 `아즈카반의 죄수(prisoner-of-azkaban)`을 실행하고 싶다면, `harry` `v1`버전을 제거하고 `harry` `v3`버전을 설치해야 합니다. (또는 단순히 `v3`버전을 설치하는 것만으로도 기존의 `v1`버전이 자동으로 제거됩니다.) +하지만 `prisoner-of-azkaban`을 실행하려면 `harry` 버전 `1`을 제거하고 `harry` 버전 `3`을 설치해야 합니다(또는 버전 `3`을 설치하기만 해도 버전 `1`이 자동으로 제거됩니다). <div class="termy"> @@ -496,9 +517,9 @@ $ pip install "harry==3" </div> -그렇게 하면 이제 전역 Python 환경에는 `harry` `v3`버전이 설치된 상태가 됩니다. +그러면 전역 Python 환경에 `harry` 버전 `3`이 설치된 상태가 됩니다. -그리고 다시 `마법사의 돌(philosophers-stone)`을 실행하려고 하면, **작동하지** 않을 수 있습니다. 왜냐하면 이 프로그램은 `harry` `v1`버전을 필요로 하기 때문입니다. +그리고 `philosophers-stone`을 다시 실행하려고 하면, `harry` 버전 `1`이 필요하기 때문에 **작동하지 않을** 가능성이 있습니다. ```mermaid flowchart LR @@ -515,50 +536,49 @@ flowchart LR end ``` -/// tip | 팁 +/// tip -Python 패키지들은 **새 버전**에서 **호환성 문제(breaking changes)**가 발생하지 않도록 최대한 노력하는 것이 일반적입니다. 하지만 그래도 안전하게 작업하려면, 테스트를 실행해보면서 새 버전을 의도적으로 설치하는 것이 좋습니다. +Python 패키지에서는 **새 버전**에서 **호환성을 깨뜨리는 변경(breaking changes)**을 **피하려고** 최선을 다하는 것이 매우 일반적이지만, 안전을 위해 더 최신 버전은 의도적으로 설치하고, 테스트를 실행해 모든 것이 올바르게 작동하는지 확인할 수 있을 때 설치하는 것이 좋습니다. /// -이제, 이런 일이 여러분의 **모든 프로젝트**가 사용하는 **수많은 패키지들**에서 동시에 발생한다고 상상해보세요. 이는 매우 관리하기 어려우며, 결국 **서로 호환되지 않는 버전**의 패키지로 프로젝트를 실행하게 될 가능성이 높고, 그로 인해 어떤 문제가 왜 발생하는지 알 수 없게 될 수 있습니다. +이제 이런 일이 여러분의 **모든 프로젝트가 의존하는** **많은** 다른 **패키지**에서도 일어난다고 상상해 보세요. 이는 관리하기가 매우 어렵습니다. 그리고 결국 일부 프로젝트는 패키지의 **호환되지 않는 버전**으로 실행하게 될 가능성이 높으며, 왜 무언가가 작동하지 않는지 알지 못하게 될 수 있습니다. -또한 사용하는 운영체제(Linux, Windows, macOS 등)에 따라 Python이 **미리 설치되어 있을 수도** 있습니다. 이런 경우에는 운영체제의 동작에 필요한 특정 버전의 패키지들이 함께 설치되어 있을 수 있습니다. 이 상태에서 전역 Python 환경에 임의의 패키지를 설치하면, 운영체제에 포함된 프로그램 일부가 **깨질 위험**도 있습니다. +또한 운영체제(Linux, Windows, macOS 등)에 따라 Python이 이미 설치되어 있을 수도 있습니다. 그런 경우에는 시스템에 **필요한 특정 버전**의 패키지가 일부 미리 설치되어 있을 가능성이 큽니다. 전역 Python 환경에 패키지를 설치하면, 운영체제에 포함된 프로그램 일부가 **깨질** 수 있습니다. -## 패키지들은 어디에 설치되는가 +## 패키지는 어디에 설치되나요 { #where-are-packages-installed } -Python을 설치하면, 컴퓨터에 여러 디렉터리와 파일들이 생성됩니다. +Python을 설치하면 컴퓨터에 몇몇 파일이 들어 있는 디렉터리가 생성됩니다. -이 중 일부 디렉터리는 사용자가 설치한 패키지들을 보관하는 역할을 합니다. +이 디렉터리 중 일부는 설치한 모든 패키지를 담는 역할을 합니다. -예를 들어, 아래 명령어를 실행하면: +다음을 실행하면: <div class="termy"> ```console -// 지금 실행하지 않아도 됩니다, 그냥 예제일 뿐이에요 🤓 +// Don't run this now, it's just an example 🤓 $ pip install "fastapi[standard]" ---> 100% ``` </div> -해당 명령어는 FastAPI 코드를 포함한 압축 파일을 다운로드합니다. 이 파일은 보통 <a href="https://pypi.org/project/fastapi/" class="external-link" target="_blank">PyPI</a>에서 받아옵니다. +FastAPI 코드를 담은 압축 파일을 다운로드합니다. 보통 <a href="https://pypi.org/project/fastapi/" class="external-link" target="_blank">PyPI</a>에서 받습니다. -또한 FastAPI가 의존하는 다른 패키지들도 함께 **다운로드**됩니다. +또한 FastAPI가 의존하는 다른 패키지들의 파일도 **다운로드**합니다. -그리고 그 모든 파일들을 **압축 해제**한 뒤, 컴퓨터의 특정 디렉터리에 저장합니다. +그 다음 모든 파일을 **압축 해제**하고 컴퓨터의 한 디렉터리에 넣습니다. -기본적으로 이 파일들은 Python이 설치된 디렉터리 안, 즉 **전역 환경**에 내의 디렉터리에 저장됩니다. +기본적으로, 다운로드하고 압축 해제한 파일들은 Python 설치와 함께 제공되는 디렉터리, 즉 **전역 환경**에 저장됩니다. -## 가상 환경이란 +## 가상 환경이란 무엇인가요 { #what-are-virtual-environments } -전역 환경에 모든 패키지를 설치하면서 발생하는 문제에 대한 해결책은, 작업하는 **각 프로젝트마다 가상 환경**을 사용하는 것입니다. +전역 환경에 모든 패키지를 두는 문제에 대한 해결책은 작업하는 **각 프로젝트마다 가상 환경**을 사용하는 것입니다. -가상 환경은 전역 환경과 매우 유사한 하나의 **디렉터리**이며, 그 안에 해당 프로젝트를 위한 패키지들을 설치할 수 있습니다. - -이렇게 하면 각 프로젝트는 자체적인 가상 환경(`.venv` 디렉터리)을 가지게 되며, 그 안에 해당 프로젝트 전용 패키지들을 보유하게 됩니다. +가상 환경은 전역 환경과 매우 유사한 하나의 **디렉터리**이며, 프로젝트의 패키지를 설치할 수 있습니다. +이렇게 하면 각 프로젝트는 자체 가상 환경(`.venv` 디렉터리)과 자체 패키지를 갖게 됩니다. ```mermaid flowchart TB @@ -577,9 +597,9 @@ flowchart TB stone-project ~~~ azkaban-project ``` -## 가상 환경 활성화 의미 +## 가상 환경을 활성화한다는 것은 무엇을 의미하나요 { #what-does-activating-a-virtual-environment-mean } -가상 환경을 활성화한다는 것은, 예를 들어 다음과 같은 명령어를 실행하는 것을 의미합니다: +가상 환경을 활성화한다는 것은, 예를 들어 다음과 같은 명령어로: //// tab | Linux, macOS @@ -607,7 +627,7 @@ $ .venv\Scripts\Activate.ps1 //// tab | Windows Bash -Windows에서 Bash(예: <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>)를 사용하는 경우: +또는 Windows에서 Bash(예: <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>)를 사용하는 경우: <div class="termy"> @@ -619,19 +639,19 @@ $ source .venv/Scripts/activate //// -이 명령어는 이후에 실행될 명령어에서 사용될 [환경 변수](environment-variables.md){.internal-link target=_blank} 몇 개를 생성하거나 수정합니다. +다음 명령어들에서 사용할 수 있는 몇몇 [환경 변수](environment-variables.md){.internal-link target=_blank}를 생성하거나 수정하는 것을 의미합니다. -이 변수들 중 하나가 바로 `PATH` 변수입니다. +그 변수 중 하나가 `PATH` 변수입니다. -/// tip | 팁 +/// tip -`PATH` 환경 변수에 대해 더 알고 싶다면 [환경 변수 문서의 PATH 환경 변수 섹션](environment-variables.md#path-environment-variable){.internal-link target=_blank}을 참고하세요. +`PATH` 환경 변수에 대해 더 알아보려면 [환경 변수](environment-variables.md#path-environment-variable){.internal-link target=_blank} 섹션을 참고하세요. /// -가상 환경을 활성화하면, 가상 환경의 경로인 `.venv/bin` (Linux와 macOS) 또는 `.venv\Scripts`(Windows)를 `PATH` 환경 변수에 추가됩니다. +가상 환경을 활성화하면 가상 환경의 경로인 `.venv/bin`(Linux와 macOS) 또는 `.venv\Scripts`(Windows)를 `PATH` 환경 변수에 추가합니다. -예를 들어, 가상 환경을 활성화하기 전의 `PATH` 변수는 다음과 같았다고 가정해봅시다: +가령 환경을 활성화하기 전에는 `PATH` 변수가 다음과 같았다고 해보겠습니다: //// tab | Linux, macOS @@ -639,7 +659,7 @@ $ source .venv/Scripts/activate /usr/bin:/bin:/usr/sbin:/sbin ``` -시스템은 다음 경로들에서 프로그램을 찾게 됩니다: +이는 시스템이 다음 위치에서 프로그램을 찾는다는 뜻입니다: * `/usr/bin` * `/bin` @@ -654,13 +674,13 @@ $ source .venv/Scripts/activate C:\Windows\System32 ``` -시스템은 다음 경로들에서 프로그램을 찾게 됩니다: +이는 시스템이 다음 위치에서 프로그램을 찾는다는 뜻입니다: * `C:\Windows\System32` //// -가상 환경을 활성화한 후에는, `PATH` 변수는 다음과 같은 형태가 됩니다: +가상 환경을 활성화한 뒤에는 `PATH` 변수가 다음과 같이 보일 수 있습니다: //// tab | Linux, macOS @@ -668,21 +688,21 @@ C:\Windows\System32 /home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin ``` -시스템은 가장 먼저 다음 경로에서 프로그램을 찾기 시작합니다: +이는 시스템이 이제 다음 위치에서 프로그램을 가장 먼저 찾기 시작한다는 뜻입니다: ```plaintext /home/user/code/awesome-project/.venv/bin ``` -그 후에 다른 디렉터리들을 탐색합니다. +그리고 나서 다른 디렉터리들을 탐색합니다. -따라서 터미널에 `python`을 입력하면, 시스템은 다음 위치에 있는 Python 프로그램을 찾게 됩니다: +따라서 터미널에 `python`을 입력하면, 시스템은 다음 위치에서 Python 프로그램을 찾고: ```plaintext /home/user/code/awesome-project/.venv/bin/python ``` -그리고 해당 Python을 사용하게 됩니다. +그것을 사용하게 됩니다. //// @@ -692,31 +712,31 @@ C:\Windows\System32 C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 ``` -시스템은 가장 먼저 다음 경로에서 프로그램을 찾기 시작합니다: +이는 시스템이 이제 다음 위치에서 프로그램을 가장 먼저 찾기 시작한다는 뜻입니다: ```plaintext C:\Users\user\code\awesome-project\.venv\Scripts ``` -그 후에 다른 디렉터리들을 탐색합니다. +그리고 나서 다른 디렉터리들을 탐색합니다. -따라서 터미널에 `python`을 입력하면, 시스템은 다음 경로에 있는 Python 프로그램을 찾게 됩니다: +따라서 터미널에 `python`을 입력하면, 시스템은 다음 위치에서 Python 프로그램을 찾고: ```plaintext C:\Users\user\code\awesome-project\.venv\Scripts\python ``` -그리고 해당 Python을 사용하게 됩니다. +그것을 사용하게 됩니다. //// -중요한 세부 사항 중 하나는, 가상 환경의 경로가 `PATH` 변수의 가장 **앞**에 추가된다는 점입니다. 시스템은 사용 가능한 다른 Python들보다 **먼저** 이 경로를 찾습니다. 그래서 터미널에서 `python`을 실행하면, 전역 환경의 Python이 아닌 **가상 환경에 있는** Python이 사용됩니다. (예: 전역 환경에 설치된 `python`이 있더라도 그보다 우선합니다.) +중요한 세부 사항은 가상 환경 경로가 `PATH` 변수의 **맨 앞**에 들어간다는 점입니다. 시스템은 다른 어떤 Python보다도 **먼저** 이를 찾게 됩니다. 이렇게 하면 `python`을 실행할 때, 다른 어떤 `python`(예: 전역 환경의 `python`)이 아니라 **가상 환경의 Python**을 사용하게 됩니다. -가상 환경을 활성화하면 이 외에도 몇 가지 다른 것들이 변경되지만, 이는 그중에서도 가장 중요한 변화 중 하나입니다. +가상 환경을 활성화하면 다른 몇 가지도 변경되지만, 이것이 그중 가장 중요한 것 중 하나입니다. -## 가상 환경 확인하기 +## 가상 환경 확인하기 { #checking-a-virtual-environment } -가상 환경이 활성화 되었는지 확인하려면, 아래 명령어를 사용할 수 있습니다: +가상 환경이 활성화되어 있는지 확인할 때는, 예를 들어 다음을 사용합니다: //// tab | Linux, macOS, Windows Bash @@ -746,34 +766,33 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python //// -즉, 현재 사용되는 `python` 프로그램은 **가상 환경 내부에 있는 것**입니다. +이는 사용될 `python` 프로그램이 **가상 환경 내부에 있는 것**이라는 뜻입니다. -Linux와 macOS에서는 `which`, Windows PowerShell에서는 `Get-Command` 명령어를 사용합니다. +Linux와 macOS에서는 `which`, Windows PowerShell에서는 `Get-Command`를 사용합니다. -이 명령어는 `PATH` 환경 변수에 지정된 경로들을 **순서대로 탐색**하면서 `python`이라는 이름의 프로그램을 찾습니다. -찾는 즉시, 해당 프로그램의 **경로를 출력**합니다. +이 명령어는 `PATH` 환경 변수에 있는 경로를 **순서대로** 확인하면서 `python`이라는 프로그램을 찾습니다. 찾는 즉시, 그 프로그램의 **경로를 보여줍니다**. -중요한 점은 터미널에서 `python`을 실행했을 때, 실제로 실행되는 "`python`"이 어떤 것인지 정확히 알 수 있다는 것입니다. +가장 중요한 부분은 `python`을 호출했을 때, 실행될 정확한 "`python`"이 무엇인지 알 수 있다는 점입니다. -따라서 현재 올바른 가상 환경에 있는지 확인할 수 있습니다. +따라서 올바른 가상 환경에 있는지 확인할 수 있습니다. -/// tip | 팁 +/// tip -하나의 가상 환경을 활성화한 뒤, 해당 Python을 가진 상태에서 **또 다른 프로젝트**로 이동하는 것은 흔히 발생합니다. +가상 환경을 하나 활성화해서 Python을 사용한 다음, **다른 프로젝트로 이동**하기 쉽습니다. -하지만 이때 이전 프로젝트의 가상 환경에 있는 **잘못된 Python 실행 파일**을 사용하게 되어 새 프로젝트가 **정상 작동하지 않을 수 있습니다.** +그리고 두 번째 프로젝트는 다른 프로젝트의 가상 환경에서 온 **잘못된 Python**을 사용하고 있기 때문에 **작동하지 않을** 수 있습니다. -그래서 현재 어떤 `python`이 사용되고 있는지 확인할 수 있는 능력은 매우 유용합니다. 🤓 +어떤 `python`이 사용되고 있는지 확인할 수 있으면 유용합니다. 🤓 /// -## 가상 환경을 비활성화하는 이유 +## 가상 환경을 왜 비활성화하나요 { #why-deactivate-a-virtual-environment } -예를 들어 `마법사의 돌(philosophers-stone)`이라는 프로젝트에서 작업 중이라고 해보겠습니다. 이때 해당 **가상 환경을 활성화**하고, 필요한 패키지를 설치하며 작업을 진행합니다. +예를 들어 `philosophers-stone` 프로젝트에서 작업하면서, **그 가상 환경을 활성화**하고, 패키지를 설치하고, 그 환경으로 작업하고 있다고 해보겠습니다. -그런데 이제는 **다른 프로젝트**인 `아즈카반의 죄수(prisoner-of-azkaban)`을 작업하고 싶어졌습니다. +그런데 이제 **다른 프로젝트**인 `prisoner-of-azkaban`에서 작업하고 싶습니다. -그래서 그 프로젝트 디렉터리로 이동합니다: +해당 프로젝트로 이동합니다: <div class="termy"> @@ -783,7 +802,7 @@ $ cd ~/code/prisoner-of-azkaban </div> -만약 `마법사의 돌(philosophers-stone)`의 가상 환경을 비활성화하지 않았다면, 터미널에서 `python`을 실행할 때 여전히 `마법사의 돌(philosophers-stone)` 가상 환경의 Python을 사용하게 됩니다. +`philosophers-stone`의 가상 환경을 비활성화하지 않으면, 터미널에서 `python`을 실행할 때 `philosophers-stone`의 Python을 사용하려고 할 것입니다. <div class="termy"> @@ -792,7 +811,7 @@ $ cd ~/code/prisoner-of-azkaban $ python main.py -// sirius를 임포트하는 데 실패했습니다. 설치되어 있지 않아요 😱 +// Error importing sirius, it's not installed 😱 Traceback (most recent call last): File "main.py", line 1, in <module> import sirius @@ -800,47 +819,46 @@ Traceback (most recent call last): </div> -하지만 `마법사의 돌(philosophers-stone)`의 가상 환경을 비활성화한 다음, `아즈카반의 죄수(prisoner-of-azkaban)` 프로젝트의 가상 환경을 활성화하면, 이제 `python` 명령어는 `아즈카반의 죄수(prisoner-of-azkaban)` 가상 환경의 Python을 사용하게 됩니다. +하지만 가상 환경을 비활성화하고 `prisoner-of-askaban`에 대한 새 가상 환경을 활성화하면, `python`을 실행할 때 `prisoner-of-azkaban`의 가상 환경에 있는 Python을 사용하게 됩니다. <div class="termy"> ```console $ cd ~/code/prisoner-of-azkaban -// 이전 디렉터리에 있을 필요 없이, 어디서든 가상 환경을 비활성화할 수 있습니다. 다른 프로젝트 디렉터리로 이동한 후에도 괜찮아요 😎 +// You don't need to be in the old directory to deactivate, you can do it wherever you are, even after going to the other project 😎 $ deactivate -// prisoner-of-azkaban/.venv 가상 환경을 활성화합니다 🚀 +// Activate the virtual environment in prisoner-of-azkaban/.venv 🚀 $ source .venv/bin/activate -// 이제 python을 실행하면, 이 가상 환경에 설치된 sirius 패키지를 찾게 됩니다 ✨ +// Now when you run python, it will find the package sirius installed in this virtual environment ✨ $ python main.py -못된 짓을 꾸미고 있음을 엄숙히 맹세합니다.🧙 -ImportError는 이제 없습니다. 🐺 +I solemnly swear 🐺 ``` </div> -## 대안들 +## 대안들 { #alternatives } -이 문서는 여러분이 Python 프로젝트를 시작하고, **그 내부에서** 어떻게 돌아가는지 알려주는 간단한 가이드입니다. +이 문서는 시작을 돕고, 내부에서 모든 것이 어떻게 작동하는지 알려주는 간단한 가이드입니다. -가상 환경, 패키지 의존성(Requirements), 프로젝트를 관리하는 방법에는 이 외에도 다양한 **대안**들이 존재합니다. +가상 환경, 패키지 의존성(requirements), 프로젝트를 관리하는 방법에는 많은 **대안**이 있습니다. -만약 준비가 되었다면, **프로젝트 전체**, 패키지 의존성, 가상 환경 등을 통합적으로 **관리**할 수 있는 도구를 써보는 것도 좋습니다. 그럴 때 추천하는 도구가 바로 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>입니다. +준비가 되었고 **프로젝트 전체**, 패키지 의존성, 가상 환경 등을 **관리**하는 도구를 사용하고 싶다면 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>를 사용해 보시길 권합니다. -`uv`는 다양한 기능을 지원합니다: +`uv`는 많은 일을 할 수 있습니다. 예를 들어: -* 다양한 버전의 **Python 설치** -* 각 프로젝트 별 **가상 환경 관리** -* **패키지 설치** -* 프로젝트의 **의존성과 버전** 관리 -* 설치된 패키지들과 그 버전을 **정확히 고정(lock)**해서,개발 환경과 운영 환경이 완전히 동일하게 작동할 수 있도록 보장 -* 이 외에도 다양한 기능을 지원 +* 여러 버전을 포함해 **Python을 설치** +* 프로젝트의 **가상 환경** 관리 +* **패키지** 설치 +* 프로젝트의 패키지 **의존성과 버전** 관리 +* 의존성을 포함해 설치할 패키지와 버전의 **정확한** 세트를 보장하여, 개발 중인 컴퓨터와 동일하게 프로덕션에서 실행할 수 있도록 합니다. 이를 **locking**이라고 합니다 +* 그 외에도 많은 기능이 있습니다 -## 결론 +## 결론 { #conclusion } -여기까지 모두 읽고 이해했다면, 이제 많은 개발자들보다 가상 환경을 **훨씬 더 깊이 있게 이해**하게 되셨습니다. 🤓 +여기까지 모두 읽고 이해했다면, 이제 많은 개발자들보다 가상 환경에 대해 **훨씬 더 많이** 알게 된 것입니다. 🤓 -이런 세부적인 내용을 알고 있으면, 언젠가 복잡해 보이는 문제를 디버깅할 때 분명히 큰 도움이 될 것입니다. 이제는 **이 모든 것들이 내부에서 어떻게 작동하는지** 알고 있기 때문입니다. 😎 +이 세부 사항을 알고 있으면, 나중에 복잡해 보이는 무언가를 디버깅할 때 아마도 도움이 될 것입니다. **내부에서 어떻게 작동하는지** 알고 있기 때문입니다. 😎 diff --git a/scripts/docs.py b/scripts/docs.py index a9abce56b..c350ee831 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -23,6 +23,7 @@ SUPPORTED_LANGS = { "en", "de", "es", + "ko", "pt", "ru", "uk", From 1fedd1c73b576ab9a969a30cde94bf19b329999a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:15:31 +0000 Subject: [PATCH 063/243] =?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 a25c94b7f..5bd1b03ae 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Update translations for uk (update-outdated). PR [#14587](https://github.com/fastapi/fastapi/pull/14587) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for es (update-outdated). PR [#14686](https://github.com/fastapi/fastapi/pull/14686) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add LLM prompt file for Turkish, generated from the existing translations. PR [#14547](https://github.com/fastapi/fastapi/pull/14547) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add LLM prompt file for Traditional Chinese, generated from the existing translations. PR [#14550](https://github.com/fastapi/fastapi/pull/14550) by [@tiangolo](https://github.com/tiangolo). From 16e583413c678f36e3f2204675c384bc884ecbd1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:16:33 +0000 Subject: [PATCH 064/243] =?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 5bd1b03ae..19efeec13 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Update translations for ko (update-outdated). PR [#14589](https://github.com/fastapi/fastapi/pull/14589) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for uk (update-outdated). PR [#14587](https://github.com/fastapi/fastapi/pull/14587) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for es (update-outdated). PR [#14686](https://github.com/fastapi/fastapi/pull/14686) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add LLM prompt file for Turkish, generated from the existing translations. PR [#14547](https://github.com/fastapi/fastapi/pull/14547) by [@tiangolo](https://github.com/tiangolo). From 5ec2615b1ac7b6970b8d548a8556cd6b2026c30f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 10 Jan 2026 16:21:07 -0800 Subject: [PATCH 065/243] =?UTF-8?q?=F0=9F=91=B7=20Tweak=20CI=20input=20nam?= =?UTF-8?q?es=20(#14688)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/translate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index 734a3782f..83518614b 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -31,7 +31,7 @@ on: required: false default: "" commit_in_place: - description: Whether to commit changes directly instead of making a PR + description: Commit changes directly instead of making a PR type: boolean required: false default: false From 50fa3f7c882cea592e5488a50354bfc016ea65bb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:21:31 +0000 Subject: [PATCH 066/243] =?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 19efeec13..52166f4e8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ hide: ### Internal +* 👷 Tweak CI input names. PR [#14688](https://github.com/fastapi/fastapi/pull/14688) by [@tiangolo](https://github.com/tiangolo). * 🔨 Refactor translation script to allow committing in place. PR [#14687](https://github.com/fastapi/fastapi/pull/14687) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix translation script path. PR [#14685](https://github.com/fastapi/fastapi/pull/14685) by [@tiangolo](https://github.com/tiangolo). * ✅ Enable tests in CI for scripts. PR [#14684](https://github.com/fastapi/fastapi/pull/14684) by [@tiangolo](https://github.com/tiangolo). From 0383fb3ab912e1e8e1549597afd510f0e857b70f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:41:45 +0100 Subject: [PATCH 067/243] =?UTF-8?q?=E2=AC=86=20Bump=20actions/download-art?= =?UTF-8?q?ifact=20from=206=20to=207=20(#14526)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 79d7391c4..734fc244d 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -45,7 +45,7 @@ jobs: run: | rm -rf ./site mkdir ./site - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: path: ./site/ pattern: docs-site-* diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 1a45174dc..f23b962b7 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -28,7 +28,7 @@ jobs: pyproject.toml uv.lock - run: uv sync --locked --no-dev --group github-actions - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: name: coverage-html path: htmlcov diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d45c20eb..605d7c812 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -112,7 +112,7 @@ jobs: - name: Install Dependencies run: uv sync --locked --no-dev --group tests --extra all - name: Get coverage files - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: coverage-* path: coverage From 9a76f2fec9043cfa0834a70d8c879a03506e6860 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:42:08 +0000 Subject: [PATCH 068/243] =?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 52166f4e8..46488d326 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ hide: ### Internal +* ⬆ Bump actions/download-artifact from 6 to 7. PR [#14526](https://github.com/fastapi/fastapi/pull/14526) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Tweak CI input names. PR [#14688](https://github.com/fastapi/fastapi/pull/14688) by [@tiangolo](https://github.com/tiangolo). * 🔨 Refactor translation script to allow committing in place. PR [#14687](https://github.com/fastapi/fastapi/pull/14687) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix translation script path. PR [#14685](https://github.com/fastapi/fastapi/pull/14685) by [@tiangolo](https://github.com/tiangolo). From d05b18ec4012304355ec3df1d9cc4e7c78e5419a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:42:37 +0100 Subject: [PATCH 069/243] =?UTF-8?q?=E2=AC=86=20Bump=20actions/upload-artif?= =?UTF-8?q?act=20from=205=20to=206=20(#14525)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/test.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 2198fe668..d1b38930c 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -97,7 +97,7 @@ jobs: path: docs/${{ matrix.lang }}/.cache - name: Build Docs run: uv run ./scripts/docs.py build-lang ${{ matrix.lang }} - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: docs-site-${{ matrix.lang }} path: ./site/** diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 605d7c812..891f76717 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,7 +84,7 @@ jobs: # Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow - name: Store coverage files if: matrix.coverage == 'coverage' - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: coverage-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/coverage/.coverage.*') }} path: coverage @@ -121,7 +121,7 @@ jobs: - run: uv run coverage combine coverage - run: uv run coverage html --title "Coverage for ${{ github.sha }}" - name: Store coverage HTML - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: coverage-html path: htmlcov From 14f30687627bfde7056bea8066b3a006cd1abdad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:42:51 +0100 Subject: [PATCH 070/243] =?UTF-8?q?=E2=AC=86=20Bump=20actions/cache=20from?= =?UTF-8?q?=204=20to=205=20(#14511)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index d1b38930c..77bce055c 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -91,7 +91,7 @@ jobs: run: uv sync --locked --no-dev --group docs - name: Update Languages run: uv run ./scripts/docs.py update-languages - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: key: mkdocs-cards-${{ matrix.lang }}-${{ github.ref }} path: docs/${{ matrix.lang }}/.cache From 612a2d20bcbfa7041d2e87bbae7aa3db7bffee67 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:43:25 +0000 Subject: [PATCH 071/243] =?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 46488d326..491caaee8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ hide: ### Internal +* ⬆ Bump actions/upload-artifact from 5 to 6. PR [#14525](https://github.com/fastapi/fastapi/pull/14525) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/download-artifact from 6 to 7. PR [#14526](https://github.com/fastapi/fastapi/pull/14526) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Tweak CI input names. PR [#14688](https://github.com/fastapi/fastapi/pull/14688) by [@tiangolo](https://github.com/tiangolo). * 🔨 Refactor translation script to allow committing in place. PR [#14687](https://github.com/fastapi/fastapi/pull/14687) by [@tiangolo](https://github.com/tiangolo). From effe493ae0d398e8237de1796ec3ec25ef19e4f0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:43:35 +0000 Subject: [PATCH 072/243] =?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 491caaee8..7727fa699 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ hide: ### Internal +* ⬆ Bump actions/cache from 4 to 5. PR [#14511](https://github.com/fastapi/fastapi/pull/14511) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/upload-artifact from 5 to 6. PR [#14525](https://github.com/fastapi/fastapi/pull/14525) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/download-artifact from 6 to 7. PR [#14526](https://github.com/fastapi/fastapi/pull/14526) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Tweak CI input names. PR [#14688](https://github.com/fastapi/fastapi/pull/14688) by [@tiangolo](https://github.com/tiangolo). From 1054fbd2563c91ac7798ae25c486c426df9d7909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 11 Jan 2026 10:18:38 -0800 Subject: [PATCH 073/243] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20co?= =?UTF-8?q?ntributing=20with=20translations=20(#14701)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/contributing.md | 235 +++-------------------------------- scripts/docs.py | 11 +- 2 files changed, 17 insertions(+), 229 deletions(-) diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 18b7f60f7..be4706f74 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -177,252 +177,45 @@ as Uvicorn by default will use the port `8000`, the documentation on port `8008` ### Translations -/// warning | Attention - -**Update on Translations** - -We're updating the way we handle documentation translations. - -Until now, we invited community members to translate pages via pull requests, which were then reviewed by at least two native speakers. While this has helped bring FastAPI to many more users, we’ve also run into several challenges - some languages have only a few translated pages, others are outdated and hard to maintain over time. -To improve this, we’re working on automation tools 🤖 to manage translations more efficiently. Once ready, documentation will be machine-translated and still reviewed by at least two native speakers ✅ before publishing. This will allow us to keep translations up-to-date while reducing the review burden on maintainers. - -What’s changing now: - -* 🚫 We’re no longer accepting new community-submitted translation PRs. - -* ⏳ Existing open PRs will be reviewed and can still be merged if completed within the next 3 weeks (since July 11 2025). - -* 🌐 In the future, we will only support languages where at least three active native speakers are available to review and maintain translations. - -This transition will help us keep translations more consistent and timely while better supporting our contributors 🙌. Thank you to everyone who has contributed so far — your help has been invaluable! 💖 - -/// - - Help with translations is VERY MUCH appreciated! And it can't be done without the help from the community. 🌎 🚀 Here are the steps to help with translations. -#### Tips and guidelines +#### Review Translation PRs + +Translation pull requests are made by LLMs guided with prompts designed by the FastAPI team together with the community of native speakers for each supported language. + +These translations are normally still reviewed by native speakers, and here's where you can help! * Check the currently <a href="https://github.com/fastapi/fastapi/pulls" class="external-link" target="_blank">existing pull requests</a> for your language. You can filter the pull requests by the ones with the label for your language. For example, for Spanish, the label is <a href="https://github.com/fastapi/fastapi/pulls?q=is%3Aopen+sort%3Aupdated-desc+label%3Alang-es+label%3Aawaiting-review" class="external-link" target="_blank">`lang-es`</a>. -* Review those pull requests, requesting changes or approving them. For the languages I don't speak, I'll wait for several others to review the translation before merging. +* When reviewing a pull request, it's better not to suggest changes in the same pull request, because it is LLM generated, and it won't be possible to make sure that small individual changes are replicated in other similar sections, or that they are preserved when translating the same content again. + +* Instead of adding suggestions to the translation PR, make the suggestions to the LLM prompt file for that language, in a new PR. For example, for Spanish, the LLM prompt file is at: <a href="https://github.com/fastapi/fastapi/blob/master/docs/es/llm-prompt.md" class="external-link" target="_blank">`docs/es/llm-prompt.md`</a>. /// tip -You can <a href="https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request" class="external-link" target="_blank">add comments with change suggestions</a> to existing pull requests. - Check the docs about <a href="https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-reviews" class="external-link" target="_blank">adding a pull request review</a> to approve it or request changes. /// +#### Subscribe to Notifications for Your Language + * Check if there's a <a href="https://github.com/fastapi/fastapi/discussions/categories/translations" class="external-link" target="_blank">GitHub Discussion</a> to coordinate translations for your language. You can subscribe to it, and when there's a new pull request to review, an automatic comment will be added to the discussion. -* If you translate pages, add a single pull request per page translated. That will make it much easier for others to review it. - * To check the 2-letter code for the language you want to translate, you can use the table <a href="https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes" class="external-link" target="_blank">List of ISO 639-1 codes</a>. -#### Existing language - -Let's say you want to translate a page for a language that already has translations for some pages, like Spanish. - -In the case of Spanish, the 2-letter code is `es`. So, the directory for Spanish translations is located at `docs/es/`. - -/// tip - -The main ("official") language is English, located at `docs/en/`. - -/// - -Now run the live server for the docs in Spanish: - -<div class="termy"> - -```console -// Use the command "live" and pass the language code as a CLI argument -$ python ./scripts/docs.py live es - -<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008 -<span style="color: green;">[INFO]</span> Start watching changes -<span style="color: green;">[INFO]</span> Start detecting changes -``` - -</div> - -/// tip - -Alternatively, you can perform the same steps that scripts does manually. - -Go into the language directory, for the Spanish translations it's at `docs/es/`: - -```console -$ cd docs/es/ -``` - -Then run `mkdocs` in that directory: - -```console -$ mkdocs serve --dev-addr 127.0.0.1:8008 -``` - -/// - -Now you can go to <a href="http://127.0.0.1:8008" class="external-link" target="_blank">http://127.0.0.1:8008</a> and see your changes live. - -You will see that every language has all the pages. But some pages are not translated and have an info box at the top, about the missing translation. - -Now let's say that you want to add a translation for the section [Features](features.md){.internal-link target=_blank}. - -* Copy the file at: - -``` -docs/en/docs/features.md -``` - -* Paste it in exactly the same location but for the language you want to translate, e.g.: - -``` -docs/es/docs/features.md -``` - -/// tip - -Notice that the only change in the path and file name is the language code, from `en` to `es`. - -/// - -If you go to your browser you will see that now the docs show your new section (the info box at the top is gone). 🎉 - -Now you can translate it all and see how it looks as you save the file. - -#### Don't Translate these Pages - -🚨 Don't translate: - -* Files under `reference/` -* `release-notes.md` -* `fastapi-people.md` -* `external-links.md` -* `newsletter.md` -* `management-tasks.md` -* `management.md` -* `contributing.md` - -Some of these files are updated very frequently and a translation would always be behind, or they include the main content from English source files, etc. - #### Request a New Language Let's say that you want to request translations for a language that is not yet translated, not even some pages. For example, Latin. -If there is no discussion for that language, you can start by requesting the new language. For that, you can follow these steps: - +* The first step would be for you to find other 2 people that would be willing to be reviewing translation PRs for that language with you. +* Once there are at least 3 people that would be willing to commit to help maintain that language, you can continue the next steps. * Create a new discussion following the template. -* Get a few native speakers to comment on the discussion and commit to help review translations for that language. +* Tag the other 2 people that will help with the language, and ask them to confirm there they will help. Once there are several people in the discussion, the FastAPI team can evaluate it and can make it an official translation. -Then the docs will be automatically translated using AI, and the team of native speakers can review the translation, and help tweak the AI prompts. +Then the docs will be automatically translated using LLMs, and the team of native speakers can review the translation, and help tweak the LLM prompts. Once there's a new translation, for example if docs are updated or there's a new section, there will be a comment in the same discussion with the link to the new translation to review. - -#### New Language - -/// note - -These steps will be performed by the FastAPI team. - -/// - -Checking the link from above (List of ISO 639-1 codes), you can see that the 2-letter code for Latin is `la`. - -Now you can create a new directory for the new language, running the following script: - -<div class="termy"> - -```console -// Use the command new-lang, pass the language code as a CLI argument -$ python ./scripts/docs.py new-lang la - -Successfully initialized: docs/la -``` - -</div> - -Now you can check in your code editor the newly created directory `docs/la/`. - -That command created a file `docs/la/mkdocs.yml` with a simple config that inherits everything from the `en` version: - -```yaml -INHERIT: ../en/mkdocs.yml -``` - -/// tip - -You could also simply create that file with those contents manually. - -/// - -That command also created a dummy file `docs/la/index.md` for the main page, you can start by translating that one. - -You can continue with the previous instructions for an "Existing Language" for that process. - -You can make the first pull request with those two files, `docs/la/mkdocs.yml` and `docs/la/index.md`. 🎉 - -#### Preview the result - -As already mentioned above, you can use the `./scripts/docs.py` with the `live` command to preview the results (or `mkdocs serve`). - -Once you are done, you can also test it all as it would look online, including all the other languages. - -To do that, first build all the docs: - -<div class="termy"> - -```console -// Use the command "build-all", this will take a bit -$ python ./scripts/docs.py build-all - -Building docs for: en -Building docs for: es -Successfully built docs for: es -``` - -</div> - -This builds all those independent MkDocs sites for each language, combines them, and generates the final output at `./site/`. - -Then you can serve that with the command `serve`: - -<div class="termy"> - -```console -// Use the command "serve" after running "build-all" -$ python ./scripts/docs.py serve - -Warning: this is a very simple server. For development, use mkdocs serve instead. -This is here only to preview a site with translations already built. -Make sure you run the build-all command first. -Serving at: http://127.0.0.1:8008 -``` - -</div> - -#### Translation specific tips and guidelines - -* Translate only the Markdown documents (`.md`). Do not translate the code examples at `./docs_src`. - -* In code blocks within the Markdown document, translate comments (`# a comment`), but leave the rest unchanged. - -* Do not change anything enclosed in "``" (inline code). - -* In lines starting with `///` translate only the text part after `|`. Leave the rest unchanged. - -* You can translate info boxes like `/// warning` with for example `/// warning | Achtung`. But do not change the word immediately after the `///`, it determines the color of the info box. - -* Do not change the paths in links to images, code files, Markdown documents. - -* However, when a Markdown document is translated, the `#hash-parts` in links to its headings may change. Update these links if possible. - * Search for such links in the translated document using the regex `#[^# ]`. - * Search in all documents already translated into your language for `your-translated-document.md`. For example VS Code has an option "Edit" -> "Find in Files". - * When translating a document, do not "pre-translate" `#hash-parts` that link to headings in untranslated documents. diff --git a/scripts/docs.py b/scripts/docs.py index c350ee831..20bf1c168 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -130,14 +130,9 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): new_path.mkdir() new_config_path: Path = Path(new_path) / mkdocs_name new_config_path.write_text("INHERIT: ../en/mkdocs.yml\n", encoding="utf-8") - new_config_docs_path: Path = new_path / "docs" - new_config_docs_path.mkdir() - en_index_path: Path = en_docs_path / "docs" / "index.md" - new_index_path: Path = new_config_docs_path / "index.md" - en_index_content = en_index_path.read_text(encoding="utf-8") - new_index_content = f"{missing_translation_snippet}\n\n{en_index_content}" - new_index_path.write_text(new_index_content, encoding="utf-8") - typer.secho(f"Successfully initialized: {new_path}", color=typer.colors.GREEN) + new_llm_prompt_path: Path = new_path / "llm-prompt.md" + new_llm_prompt_path.write_text("", encoding="utf-8") + print(f"Successfully initialized: {new_path}") update_languages() From 97aa825422737b8fbafa9ad76390a86b67a88381 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 18:18:58 +0000 Subject: [PATCH 074/243] =?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 7727fa699..2a2b0d639 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Update docs for contributing with translations. PR [#14701](https://github.com/fastapi/fastapi/pull/14701) by [@tiangolo](https://github.com/tiangolo). * 📝 Specify language code for code block. PR [#14656](https://github.com/fastapi/fastapi/pull/14656) by [@YuriiMotov](https://github.com/YuriiMotov). ### Translations From 249a776b70dfc173d4c9b91a3b0c8d9319d0ee14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 11 Jan 2026 11:19:05 -0800 Subject: [PATCH 075/243] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20about=20?= =?UTF-8?q?managing=20translations=20(#14704)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/fastapi-people.md | 22 +----- docs/en/docs/management-tasks.md | 126 ++----------------------------- 2 files changed, 7 insertions(+), 141 deletions(-) diff --git a/docs/en/docs/fastapi-people.md b/docs/en/docs/fastapi-people.md index f2ca26013..2c07af764 100644 --- a/docs/en/docs/fastapi-people.md +++ b/docs/en/docs/fastapi-people.md @@ -196,31 +196,11 @@ They have contributed source code, documentation, etc. 📦 There are hundreds of other contributors, you can see them all in the <a href="https://github.com/fastapi/fastapi/graphs/contributors" class="external-link" target="_blank">FastAPI GitHub Contributors page</a>. 👷 -## Top Translators - -These are the **Top Translators**. 🌐 - -These users have created the most Pull Requests with [translations to other languages](contributing.md#translations){.internal-link target=_blank} that have been *merged*. - -<div class="user-list user-list-center"> - -{% for user in (translators.values() | list)[:50] %} - -{% if user.login not in skip_users %} - -<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Translations: {{ user.count }}</div></div> - -{% endif %} - -{% endfor %} - -</div> - ## Top Translation Reviewers These users are the **Top Translation Reviewers**. 🕵️ -I only speak a few languages (and not very well 😅). So, the reviewers are the ones that have the [**power to approve translations**](contributing.md#translations){.internal-link target=_blank} of the documentation. Without them, there wouldn't be documentation in several other languages. +Translation reviewers have the [**power to approve translations**](contributing.md#translations){.internal-link target=_blank} of the documentation. Without them, there wouldn't be documentation in several other languages. <div class="user-list user-list-center"> {% for user in (translation_reviewers.values() | list)[:50] %} diff --git a/docs/en/docs/management-tasks.md b/docs/en/docs/management-tasks.md index aac4d6fe4..68d10a42c 100644 --- a/docs/en/docs/management-tasks.md +++ b/docs/en/docs/management-tasks.md @@ -106,129 +106,15 @@ This way, we can notice when there are new translations ready, because they have ## Merge Translation PRs -For Spanish, as I'm a native speaker and it's a language close to me, I will give it a final review myself and in most cases tweak the PR a bit before merging it. +Translations are generated automatically with LLMs and scripts. -For the other languages, confirm that: +There's one GitHub Action that can be manually run to add or update translations for a language: <a href="https://github.com/fastapi/fastapi/actions/workflows/translate.yml" class="external-link" target="_blank">`translate.yml`</a>. -* The title is correct following the instructions above. +For these language translation PRs, confirm that: + +* The PR was automated (authored by @tiangolo), not made by another user. * It has the labels `lang-all` and `lang-{lang code}`. -* The PR changes only one Markdown file adding a translation. - * Or in some cases, at most two files, if they are small, for the same language, and people reviewed them. - * If it's the first translation for that language, it will have additional `mkdocs.yml` files, for those cases follow the instructions below. -* The PR doesn't add any additional or extraneous files. -* The translation seems to have a similar structure as the original English file. -* The translation doesn't seem to change the original content, for example with obvious additional documentation sections. -* The translation doesn't use different Markdown structures, for example adding HTML tags when the original didn't have them. -* The "admonition" sections, like `tip`, `info`, etc. are not changed or translated. For example: - -``` -/// tip - -This is a tip. - -/// - -``` - -looks like this: - -/// tip - -This is a tip. - -/// - -...it could be translated as: - -``` -/// tip - -Esto es un consejo. - -/// - -``` - -...but needs to keep the exact `tip` keyword. If it was translated to `consejo`, like: - -``` -/// consejo - -Esto es un consejo. - -/// - -``` - -it would change the style to the default one, it would look like: - -/// consejo - -Esto es un consejo. - -/// - -Those don't have to be translated, but if they are, they need to be written as: - -``` -/// tip | consejo - -Esto es un consejo. - -/// - -``` - -Which looks like: - -/// tip | consejo - -Esto es un consejo. - -/// - -## First Translation PR - -When there's a first translation for a language, it will have a `docs/{lang code}/docs/index.md` translated file and a `docs/{lang code}/mkdocs.yml`. - -For example, for Bosnian, it would be: - -* `docs/bs/docs/index.md` -* `docs/bs/mkdocs.yml` - -The `mkdocs.yml` file will have only the following content: - -```YAML -INHERIT: ../en/mkdocs.yml -``` - -The language code would normally be in the <a href="https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes" class="external-link" target="_blank">ISO 639-1 list of language codes</a>. - -In any case, the language code should be in the file <a href="https://github.com/fastapi/fastapi/blob/master/docs/language_names.yml" class="external-link" target="_blank">docs/language_names.yml</a>. - -There won't be yet a label for the language code, for example, if it was Bosnian, there wouldn't be a `lang-bs`. Before creating the label and adding it to the PR, create the GitHub Discussion: - -* Go to the <a href="https://github.com/fastapi/fastapi/discussions/categories/translations" class="external-link" target="_blank">Translations GitHub Discussions</a> -* Create a new discussion with the title `Bosnian Translations` (or the language name in English) -* A description of: - -```Markdown -## Bosnian translations - -This is the issue to track translations of the docs to Bosnian. 🚀 - -Here are the [PRs to review with the label `lang-bs`](https://github.com/fastapi/fastapi/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc+label%3Alang-bs+label%3A%22awaiting-review%22). 🤓 -``` - -Update "Bosnian" with the new language. - -And update the search link to point to the new language label that will be created, like `lang-bs`. - -Create and add the label to that new Discussion just created, like `lang-bs`. - -Then go back to the PR, and add the label, like `lang-bs`, and `lang-all` and `awaiting-review`. - -Now the GitHub action will automatically detect the label `lang-bs` and will post in that Discussion that this PR is waiting to be reviewed. +* If the PR is approved by at least one native speaker, you can merge it. ## Review PRs From e9e0419ed0deee736e2b6ef01fc6dea1d6ed987e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 19:19:29 +0000 Subject: [PATCH 076/243] =?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 2a2b0d639..1df9e6351 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Update docs about managing translations. PR [#14704](https://github.com/fastapi/fastapi/pull/14704) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for contributing with translations. PR [#14701](https://github.com/fastapi/fastapi/pull/14701) by [@tiangolo](https://github.com/tiangolo). * 📝 Specify language code for code block. PR [#14656](https://github.com/fastapi/fastapi/pull/14656) by [@YuriiMotov](https://github.com/YuriiMotov). From 7b864acf370f3587cc29eb58f1f897c5c21d139d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 11 Jan 2026 13:19:26 -0800 Subject: [PATCH 077/243] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20ma?= =?UTF-8?q?nagement=20tasks=20(#14705)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/management-tasks.md | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/docs/en/docs/management-tasks.md b/docs/en/docs/management-tasks.md index 68d10a42c..6f34afb73 100644 --- a/docs/en/docs/management-tasks.md +++ b/docs/en/docs/management-tasks.md @@ -74,7 +74,7 @@ Make sure you use a supported label from the <a href="https://github.com/tiangol * `refactor`: Refactors * This is normally for changes to the internal code that don't change the behavior. Normally it improves maintainability, or enables future features, etc. * `upgrade`: Upgrades - * This is for upgrades to direct dependencies from the project, or extra optional dependencies, normally in `pyproject.toml`. So, things that would affect final users, they would end up receiving the upgrade in their code base once they update. But this is not for upgrades to internal dependencies used for development, testing, docs, etc. Those internal dependencies, normally in `requirements.txt` files or GitHub Action versions should be marked as `internal`, not `upgrade`. + * This is for upgrades to direct dependencies from the project, or extra optional dependencies, normally in `pyproject.toml`. So, things that would affect final users, they would end up receiving the upgrade in their code base once they update. But this is not for upgrades to internal dependencies used for development, testing, docs, etc. Those internal dependencies or GitHub Action versions should be marked as `internal`, not `upgrade`. * `docs`: Docs * Changes in docs. This includes updating the docs, fixing typos. But it doesn't include changes to translations. * You can normally quickly detect it by going to the "Files changed" tab in the PR and checking if the updated file(s) starts with `docs/en/docs`. The original version of the docs is always in English, so in `docs/en/docs`. @@ -118,9 +118,11 @@ For these language translation PRs, confirm that: ## Review PRs -If a PR doesn't explain what it does or why, ask for more information. +* If a PR doesn't explain what it does or why, if it seems like it could be useful, ask for more information. Otherwise, feel free to close it. -A PR should have a specific use case that it is solving. +* If a PR seems to be spam, meaningless, only to change statistics (to appear as "contributor") or similar, you can simply mark it as `invalid`, and it will be automatically closed. + +* A PR should have a specific use case that it is solving. * If the PR is for a feature, it should have docs. * Unless it's a feature we want to discourage, like support for a corner case that we don't want users to use. @@ -140,27 +142,12 @@ Every month, a GitHub Action updates the FastAPI People data. Those PRs look lik If the tests are passing, you can merge it right away. -## External Links PRs - -When people add external links they edit this file <a href="https://github.com/fastapi/fastapi/blob/master/docs/en/data/external_links.yml" class="external-link" target="_blank">external_links.yml</a>. - -* Make sure the new link is in the correct category (e.g. "Podcasts") and language (e.g. "Japanese"). -* A new link should be at the top of its list. -* The link URL should work (it should not return a 404). -* The content of the link should be about FastAPI. -* The new addition should have these fields: - * `author`: The name of the author. - * `link`: The URL with the content. - * `title`: The title of the link (the title of the article, podcast, etc). - -After checking all these things and ensuring the PR has the right labels, you can merge it. - ## Dependabot PRs Dependabot will create PRs to update dependencies for several things, and those PRs all look similar, but some are way more delicate than others. -* If the PR is for a direct dependency, so, Dependabot is modifying `pyproject.toml`, **don't merge it**. 😱 Let me check it first. There's a good chance that some additional tweaks or updates are needed. -* If the PR updates one of the internal dependencies, for example it's modifying `requirements.txt` files, or GitHub Action versions, if the tests are passing, the release notes (shown in a summary in the PR) don't show any obvious potential breaking change, you can merge it. 😎 +* If the PR is for a direct dependency, so, Dependabot is modifying `pyproject.toml` in the main dependencies, **don't merge it**. 😱 Let me check it first. There's a good chance that some additional tweaks or updates are needed. +* If the PR updates one of the internal dependencies, for example the group `dev` in `pyproject.toml`, or GitHub Action versions, if the tests are passing, the release notes (shown in a summary in the PR) don't show any obvious potential breaking change, you can merge it. 😎 ## Mark GitHub Discussions Answers From e63f382b0f189bc1ab76a58eaaf92ce83b775f48 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 21:19:50 +0000 Subject: [PATCH 078/243] =?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 1df9e6351..628eff348 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Update docs for management tasks. PR [#14705](https://github.com/fastapi/fastapi/pull/14705) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs about managing translations. PR [#14704](https://github.com/fastapi/fastapi/pull/14704) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for contributing with translations. PR [#14701](https://github.com/fastapi/fastapi/pull/14701) by [@tiangolo](https://github.com/tiangolo). * 📝 Specify language code for code block. PR [#14656](https://github.com/fastapi/fastapi/pull/14656) by [@YuriiMotov](https://github.com/YuriiMotov). From 1be80f48851845d9e0a6ce8ee6a8b871cd9ff5ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 11 Jan 2026 14:22:58 -0800 Subject: [PATCH 079/243] =?UTF-8?q?=F0=9F=93=9D=20Add=20contribution=20ins?= =?UTF-8?q?tructions=20about=20LLM=20generated=20code=20and=20comments=20a?= =?UTF-8?q?nd=20automated=20tools=20for=20PRs=20(#14706)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-manager.yml | 6 ++++- docs/en/docs/contributing.md | 36 +++++++++++++++++++++++++++++ docs/en/docs/management-tasks.md | 2 ++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index f40ec4dc4..2ae588da1 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -41,11 +41,15 @@ jobs: "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR.", "reminder": { "before": "P3D", - "message": "Heads-up: this will be closed in 3 days unless there’s new activity." + "message": "Heads-up: this will be closed in 3 days unless there's new activity." } }, "invalid": { "delay": 0, "message": "This was marked as invalid and will be closed now. If this is an error, please provide additional details." + }, + "maybe-ai": { + "delay": 0, + "message": "This was marked as potentially AI generated and will be closed now. If this is an error, please provide additional details, make sure to read the docs about contributing and AI." } } diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index be4706f74..a4d896109 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -219,3 +219,39 @@ Once there are several people in the discussion, the FastAPI team can evaluate i Then the docs will be automatically translated using LLMs, and the team of native speakers can review the translation, and help tweak the LLM prompts. Once there's a new translation, for example if docs are updated or there's a new section, there will be a comment in the same discussion with the link to the new translation to review. + +## Automated Code and AI + +You are encouraged to use all the tools you want to do your work and contribute as efficiently as possible, this includes AI (LLM) tools, etc. Nevertheless, contributions should have meaningful human intervention, judgement, context, etc. + +If the **human effort** put in a PR, e.g. writing LLM prompts, is **less** than the **effort we would need to put** to **review it**, please **don't** submit the PR. + +Think of it this way: we can already write LLM prompts or run automated tools ourselves, and that would be faster than reviewing external PRs. + +### Closing Automated and AI PRs + +If we see PRs that seem AI generated or automated in similar ways, we'll flag them and close them. + +The same applies to comments and descriptions, please don't copy paste the content generated by an LLM. + +### Human Effort Denial of Service + +Using automated tools and AI to submit PRs or comments that we have to carefully review and handle would be the equivalent of a <a href="https://en.wikipedia.org/wiki/Denial-of-service_attack" class="external-link" target="_blank">Denial-of-service attack</a> on our human effort. + +It would be very little effort from the person submitting the PR (an LLM prompt) that generates a large amount of effort on our side (carefully reviewing code). + +Please don't do that. + +We'll need to block accounts that spam us with repeated automated PRs or comments. + +### Use Tools Wisely + +As Uncle Ben said: + +<blockquote> +With great <strike>power</strike> <strong>tools</strong> comes great responsibility. +</blockquote> + +Avoid inadvertently doing harm. + +You have amazing tools at hand, use them wisely to help effectively. diff --git a/docs/en/docs/management-tasks.md b/docs/en/docs/management-tasks.md index 6f34afb73..fc6e1eb28 100644 --- a/docs/en/docs/management-tasks.md +++ b/docs/en/docs/management-tasks.md @@ -122,6 +122,8 @@ For these language translation PRs, confirm that: * If a PR seems to be spam, meaningless, only to change statistics (to appear as "contributor") or similar, you can simply mark it as `invalid`, and it will be automatically closed. +* If a PR seems to be AI generated, and seems like reviewing it would take more time from you than the time it took to write the prompt, mark it as `maybe-ai`, and it will be automatically closed. + * A PR should have a specific use case that it is solving. * If the PR is for a feature, it should have docs. From a456e92a21d5b94a67d6d0fd070218df09444443 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:23:21 +0000 Subject: [PATCH 080/243] =?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 628eff348..45bc9873e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add contribution instructions about LLM generated code and comments and automated tools for PRs. PR [#14706](https://github.com/fastapi/fastapi/pull/14706) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for management tasks. PR [#14705](https://github.com/fastapi/fastapi/pull/14705) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs about managing translations. PR [#14704](https://github.com/fastapi/fastapi/pull/14704) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for contributing with translations. PR [#14701](https://github.com/fastapi/fastapi/pull/14701) by [@tiangolo](https://github.com/tiangolo). From a96dd013a4d2ce4f5f758fe51bea19f541743164 Mon Sep 17 00:00:00 2001 From: fcharrier <fcharrie@yahoo.fr> Date: Fri, 16 Jan 2026 11:53:45 +0100 Subject: [PATCH 081/243] =?UTF-8?q?=F0=9F=90=9B=20Fix=20copy=20button=20in?= =?UTF-8?q?=20custom.js=20(#14722)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/js/custom.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/js/custom.js b/docs/en/docs/js/custom.js index 48e95901d..be326d302 100644 --- a/docs/en/docs/js/custom.js +++ b/docs/en/docs/js/custom.js @@ -81,8 +81,14 @@ function setupTermynal() { } } saveBuffer(); + const inputCommands = useLines + .filter(line => line.type === "input") + .map(line => line.value) + .join("\n"); + node.textContent = inputCommands; const div = document.createElement("div"); - node.replaceWith(div); + node.style.display = "none"; + node.after(div); const termynal = new Termynal(div, { lineData: useLines, noInit: true, From c597d9cb53415cf81188750ee946d69304922fbb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:54:08 +0000 Subject: [PATCH 082/243] =?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 45bc9873e..233fdb743 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 🐛 Fix copy button in custom.js. PR [#14722](https://github.com/fastapi/fastapi/pull/14722) by [@fcharrier](https://github.com/fcharrier). * 📝 Add contribution instructions about LLM generated code and comments and automated tools for PRs. PR [#14706](https://github.com/fastapi/fastapi/pull/14706) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for management tasks. PR [#14705](https://github.com/fastapi/fastapi/pull/14705) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs about managing translations. PR [#14704](https://github.com/fastapi/fastapi/pull/14704) by [@tiangolo](https://github.com/tiangolo). From f317ede223131a2822723602fb3f576f1ca0f5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Fri, 16 Jan 2026 03:54:01 -0800 Subject: [PATCH 083/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20ko=20(add-missing)=20(#14699)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- docs/ko/docs/_llm-test.md | 503 +++++++++++++++++ docs/ko/docs/advanced/additional-responses.md | 247 +++++++++ docs/ko/docs/advanced/behind-a-proxy.md | 466 ++++++++++++++++ docs/ko/docs/advanced/dataclasses.md | 95 ++++ docs/ko/docs/advanced/generate-clients.md | 208 ++++++++ docs/ko/docs/advanced/middleware.md | 97 ++++ docs/ko/docs/advanced/openapi-callbacks.md | 186 +++++++ docs/ko/docs/advanced/openapi-webhooks.md | 55 ++ .../path-operation-advanced-configuration.md | 172 ++++++ .../docs/advanced/security/http-basic-auth.md | 107 ++++ docs/ko/docs/advanced/security/index.md | 19 + .../docs/advanced/security/oauth2-scopes.md | 274 ++++++++++ docs/ko/docs/advanced/settings.md | 302 +++++++++++ docs/ko/docs/alternatives.md | 485 +++++++++++++++++ docs/ko/docs/deployment/concepts.md | 321 +++++++++++ docs/ko/docs/deployment/fastapicloud.md | 65 +++ docs/ko/docs/deployment/https.md | 231 ++++++++ docs/ko/docs/deployment/manually.md | 157 ++++++ .../authentication-error-status-code.md | 17 + docs/ko/docs/how-to/custom-docs-ui-assets.md | 185 +++++++ .../docs/how-to/custom-request-and-route.md | 109 ++++ docs/ko/docs/how-to/extending-openapi.md | 80 +++ docs/ko/docs/how-to/general.md | 39 ++ docs/ko/docs/how-to/graphql.md | 60 +++ docs/ko/docs/how-to/index.md | 13 + ...migrate-from-pydantic-v1-to-pydantic-v2.md | 135 +++++ .../docs/how-to/separate-openapi-schemas.md | 102 ++++ docs/ko/docs/how-to/testing-database.md | 7 + docs/ko/docs/tutorial/bigger-applications.md | 504 ++++++++++++++++++ docs/ko/docs/tutorial/body-updates.md | 100 ++++ .../tutorial/dependencies/sub-dependencies.md | 105 ++++ docs/ko/docs/tutorial/handling-errors.md | 244 +++++++++ docs/ko/docs/tutorial/security/first-steps.md | 203 +++++++ docs/ko/docs/tutorial/security/index.md | 106 ++++ 34 files changed, 5999 insertions(+) create mode 100644 docs/ko/docs/_llm-test.md create mode 100644 docs/ko/docs/advanced/additional-responses.md create mode 100644 docs/ko/docs/advanced/behind-a-proxy.md create mode 100644 docs/ko/docs/advanced/dataclasses.md create mode 100644 docs/ko/docs/advanced/generate-clients.md create mode 100644 docs/ko/docs/advanced/middleware.md create mode 100644 docs/ko/docs/advanced/openapi-callbacks.md create mode 100644 docs/ko/docs/advanced/openapi-webhooks.md create mode 100644 docs/ko/docs/advanced/path-operation-advanced-configuration.md create mode 100644 docs/ko/docs/advanced/security/http-basic-auth.md create mode 100644 docs/ko/docs/advanced/security/index.md create mode 100644 docs/ko/docs/advanced/security/oauth2-scopes.md create mode 100644 docs/ko/docs/advanced/settings.md create mode 100644 docs/ko/docs/alternatives.md create mode 100644 docs/ko/docs/deployment/concepts.md create mode 100644 docs/ko/docs/deployment/fastapicloud.md create mode 100644 docs/ko/docs/deployment/https.md create mode 100644 docs/ko/docs/deployment/manually.md create mode 100644 docs/ko/docs/how-to/authentication-error-status-code.md create mode 100644 docs/ko/docs/how-to/custom-docs-ui-assets.md create mode 100644 docs/ko/docs/how-to/custom-request-and-route.md create mode 100644 docs/ko/docs/how-to/extending-openapi.md create mode 100644 docs/ko/docs/how-to/general.md create mode 100644 docs/ko/docs/how-to/graphql.md create mode 100644 docs/ko/docs/how-to/index.md create mode 100644 docs/ko/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md create mode 100644 docs/ko/docs/how-to/separate-openapi-schemas.md create mode 100644 docs/ko/docs/how-to/testing-database.md create mode 100644 docs/ko/docs/tutorial/bigger-applications.md create mode 100644 docs/ko/docs/tutorial/body-updates.md create mode 100644 docs/ko/docs/tutorial/dependencies/sub-dependencies.md create mode 100644 docs/ko/docs/tutorial/handling-errors.md create mode 100644 docs/ko/docs/tutorial/security/first-steps.md create mode 100644 docs/ko/docs/tutorial/security/index.md diff --git a/docs/ko/docs/_llm-test.md b/docs/ko/docs/_llm-test.md new file mode 100644 index 000000000..1b828c663 --- /dev/null +++ b/docs/ko/docs/_llm-test.md @@ -0,0 +1,503 @@ +# LLM 테스트 파일 { #llm-test-file } + +이 문서는 문서를 번역하는 <abbr title="Large Language Model - 대규모 언어 모델">LLM</abbr>이 `scripts/translate.py`의 `general_prompt`와 `docs/{language code}/llm-prompt.md`의 언어별 프롬프트를 이해하는지 테스트합니다. 언어별 프롬프트는 `general_prompt`에 추가됩니다. + +여기에 추가된 테스트는 언어별 프롬프트를 설계하는 모든 사람이 보게 됩니다. + +사용 방법은 다음과 같습니다: + +* 언어별 프롬프트 `docs/{language code}/llm-prompt.md`를 준비합니다. +* 이 문서를 원하는 대상 언어로 새로 번역합니다(예: `translate.py`의 `translate-page` 명령). 그러면 `docs/{language code}/docs/_llm-test.md` 아래에 번역이 생성됩니다. +* 번역에서 문제가 없는지 확인합니다. +* 필요하다면 언어별 프롬프트, 일반 프롬프트, 또는 영어 문서를 개선합니다. +* 그런 다음 번역에서 남아 있는 문제를 수동으로 수정해 좋은 번역이 되게 합니다. +* 좋은 번역을 둔 상태에서 다시 번역합니다. 이상적인 결과는 LLM이 더 이상 번역에 변경을 만들지 않는 것입니다. 이는 일반 프롬프트와 언어별 프롬프트가 가능한 한 최선이라는 뜻입니다(때때로 몇 가지 seemingly random 변경을 할 수 있는데, 그 이유는 <a href="https://doublespeak.chat/#/handbook#deterministic-output" class="external-link" target="_blank">LLM은 결정론적 알고리즘이 아니기 때문</a>입니다). + +테스트: + +## 코드 스니펫 { #code-snippets } + +//// tab | 테스트 + +다음은 코드 스니펫입니다: `foo`. 그리고 이것은 또 다른 코드 스니펫입니다: `bar`. 그리고 또 하나: `baz quux`. + +//// + +//// tab | 정보 + +코드 스니펫의 내용은 그대로 두어야 합니다. + +`scripts/translate.py`의 일반 프롬프트에서 `### Content of code snippets` 섹션을 참고하세요. + +//// + +## 따옴표 { #quotes } + +//// tab | 테스트 + +어제 제 친구가 이렇게 썼습니다: "If you spell incorrectly correctly, you have spelled it incorrectly". 이에 저는 이렇게 답했습니다: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'"". + +/// note | 참고 + +LLM은 아마 이것을 잘못 번역할 것입니다. 흥미로운 점은 재번역할 때 고정된 번역을 유지하는지 여부뿐입니다. + +/// + +//// + +//// tab | 정보 + +프롬프트 설계자는 중립 따옴표를 타이포그래피 따옴표로 변환할지 선택할 수 있습니다. 그대로 두어도 괜찮습니다. + +예를 들어 `docs/de/llm-prompt.md`의 `### Quotes` 섹션을 참고하세요. + +//// + +## 코드 스니펫의 따옴표 { #quotes-in-code-snippets } + +//// tab | 테스트 + +`pip install "foo[bar]"` + +코드 스니펫에서 문자열 리터럴의 예: `"this"`, `'that'`. + +코드 스니펫에서 문자열 리터럴의 어려운 예: `f"I like {'oranges' if orange else "apples"}"` + +하드코어: `Yesterday, my friend wrote: "If you spell incorrectly correctly, you have spelled it incorrectly". To which I answered: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'"` + +//// + +//// tab | 정보 + +... 하지만 코드 스니펫 안의 따옴표는 그대로 유지되어야 합니다. + +//// + +## 코드 블록 { #code-blocks } + +//// tab | 테스트 + +Bash 코드 예시... + +```bash +# 우주에 인사말 출력 +echo "Hello universe" +``` + +...그리고 콘솔 코드 예시... + +```console +$ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid">main.py</u> +<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting server + Searching for package file structure +``` + +...그리고 또 다른 콘솔 코드 예시... + +```console +// "Code" 디렉터리 생성 +$ mkdir code +// 해당 디렉터리로 이동 +$ cd code +``` + +...그리고 Python 코드 예시... + +```Python +wont_work() # 이건 동작하지 않습니다 😱 +works(foo="bar") # 이건 동작합니다 🎉 +``` + +...이상입니다. + +//// + +//// tab | 정보 + +코드 블록의 코드는(주석을 제외하고) 수정하면 안 됩니다. + +`scripts/translate.py`의 일반 프롬프트에서 `### Content of code blocks` 섹션을 참고하세요. + +//// + +## 탭과 색상 박스 { #tabs-and-colored-boxes } + +//// tab | 테스트 + +/// info | 정보 +일부 텍스트 +/// + +/// note | 참고 +일부 텍스트 +/// + +/// note Technical details | 기술 세부사항 +일부 텍스트 +/// + +/// check | 확인 +일부 텍스트 +/// + +/// tip | 팁 +일부 텍스트 +/// + +/// warning | 경고 +일부 텍스트 +/// + +/// danger | 위험 +일부 텍스트 +/// + +//// + +//// tab | 정보 + +탭과 `Info`/`Note`/`Warning`/등의 블록은 제목 번역을 수직 막대(`|`) 뒤에 추가해야 합니다. + +`scripts/translate.py`의 일반 프롬프트에서 `### Special blocks`와 `### Tab blocks` 섹션을 참고하세요. + +//// + +## 웹 및 내부 링크 { #web-and-internal-links } + +//// tab | 테스트 + +링크 텍스트는 번역되어야 하고, 링크 주소는 변경되지 않아야 합니다: + +* [위의 제목으로 가는 링크](#code-snippets) +* [내부 링크](index.md#installation){.internal-link target=_blank} +* <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">외부 링크</a> +* <a href="https://fastapi.tiangolo.com/css/styles.css" class="external-link" target="_blank">스타일로 가는 링크</a> +* <a href="https://fastapi.tiangolo.com/js/logic.js" class="external-link" target="_blank">스크립트로 가는 링크</a> +* <a href="https://fastapi.tiangolo.com/img/foo.jpg" class="external-link" target="_blank">이미지로 가는 링크</a> + +링크 텍스트는 번역되어야 하고, 링크 주소는 번역 페이지를 가리켜야 합니다: + +* <a href="https://fastapi.tiangolo.com/ko/" class="external-link" target="_blank">FastAPI 링크</a> + +//// + +//// tab | 정보 + +링크는 번역되어야 하지만, 주소는 변경되지 않아야 합니다. 예외는 FastAPI 문서 페이지로 향하는 절대 링크이며, 이 경우 번역 페이지로 연결되어야 합니다. + +`scripts/translate.py`의 일반 프롬프트에서 `### Links` 섹션을 참고하세요. + +//// + +## HTML "abbr" 요소 { #html-abbr-elements } + +//// tab | 테스트 + +여기 HTML "abbr" 요소로 감싼 몇 가지가 있습니다(일부는 임의로 만든 것입니다): + +### abbr가 전체 문구를 제공 { #the-abbr-gives-a-full-phrase } + +* <abbr title="Getting Things Done - 일을 끝내는 방법론">GTD</abbr> +* <abbr title="less than - 보다 작음"><code>lt</code></abbr> +* <abbr title="XML Web Token - XML 웹 토큰">XWT</abbr> +* <abbr title="Parallel Server Gateway Interface - 병렬 서버 게이트웨이 인터페이스">PSGI</abbr> + +### abbr가 설명을 제공 { #the-abbr-gives-an-explanation } + +* <abbr title="어떤 방식으로든 서로 연결되고 함께 작동하도록 구성된 머신들의 집합입니다.">cluster</abbr> +* <abbr title="입력과 출력 계층 사이에 수많은 은닉 계층을 둔 인공 신경망을 사용하는 머신 러닝 방법으로, 이를 통해 포괄적인 내부 구조를 형성합니다">Deep Learning</abbr> + +### abbr가 전체 문구와 설명을 제공 { #the-abbr-gives-a-full-phrase-and-an-explanation } + +* <abbr title="Mozilla Developer Network - 모질라 개발자 네트워크: Firefox를 만드는 사람들이 작성한 개발자용 문서">MDN</abbr> +* <abbr title="Input/Output - 입력/출력: 디스크 읽기 또는 쓰기, 네트워크 통신.">I/O</abbr>. + +//// + +//// tab | 정보 + +"abbr" 요소의 "title" 속성은 몇 가지 구체적인 지침에 따라 번역됩니다. + +번역에서는(영어 단어를 설명하기 위해) 자체 "abbr" 요소를 추가할 수 있으며, LLM은 이를 제거하면 안 됩니다. + +`scripts/translate.py`의 일반 프롬프트에서 `### HTML abbr elements` 섹션을 참고하세요. + +//// + +## 제목 { #headings } + +//// tab | 테스트 + +### 웹앱 개발하기 - 튜토리얼 { #develop-a-webapp-a-tutorial } + +안녕하세요. + +### 타입 힌트와 -애너테이션 { #type-hints-and-annotations } + +다시 안녕하세요. + +### super- 및 subclasses { #super-and-subclasses } + +다시 안녕하세요. + +//// + +//// tab | 정보 + +제목에 대한 유일한 강한 규칙은, LLM이 중괄호 안의 해시 부분을 변경하지 않아 링크가 깨지지 않게 하는 것입니다. + +`scripts/translate.py`의 일반 프롬프트에서 `### Headings` 섹션을 참고하세요. + +언어별 지침은 예를 들어 `docs/de/llm-prompt.md`의 `### Headings` 섹션을 참고하세요. + +//// + +## 문서에서 사용되는 용어 { #terms-used-in-the-docs } + +//// tab | 테스트 + +* 당신 +* 당신의 + +* 예: (e.g.) +* 등 (etc.) + +* `int`로서의 `foo` +* `str`로서의 `bar` +* `list`로서의 `baz` + +* 튜토리얼 - 사용자 가이드 +* 고급 사용자 가이드 +* SQLModel 문서 +* API 문서 +* 자동 문서 + +* Data Science +* Deep Learning +* Machine Learning +* Dependency Injection +* HTTP Basic authentication +* HTTP Digest +* ISO format +* JSON Schema 표준 +* JSON schema +* schema definition +* Password Flow +* Mobile + +* deprecated +* designed +* invalid +* on the fly +* standard +* default +* case-sensitive +* case-insensitive + +* 애플리케이션을 서빙하다 +* 페이지를 서빙하다 + +* 앱 +* 애플리케이션 + +* 요청 +* 응답 +* 오류 응답 + +* 경로 처리 +* 경로 처리 데코레이터 +* 경로 처리 함수 + +* body +* 요청 body +* 응답 body +* JSON body +* form body +* file body +* 함수 body + +* parameter +* body parameter +* path parameter +* query parameter +* cookie parameter +* header parameter +* form parameter +* function parameter + +* event +* startup event +* 서버 startup +* shutdown event +* lifespan event + +* handler +* event handler +* exception handler +* 처리하다 + +* model +* Pydantic model +* data model +* database model +* form model +* model object + +* class +* base class +* parent class +* subclass +* child class +* sibling class +* class method + +* header +* headers +* authorization header +* `Authorization` header +* forwarded header + +* dependency injection system +* dependency +* dependable +* dependant + +* I/O bound +* CPU bound +* concurrency +* parallelism +* multiprocessing + +* env var +* environment variable +* `PATH` +* `PATH` variable + +* authentication +* authentication provider +* authorization +* authorization form +* authorization provider +* 사용자가 인증한다 +* 시스템이 사용자를 인증한다 + +* CLI +* command line interface + +* server +* client + +* cloud provider +* cloud service + +* development +* development stages + +* dict +* dictionary +* enumeration +* enum +* enum member + +* encoder +* decoder +* encode하다 +* decode하다 + +* exception +* raise하다 + +* expression +* statement + +* frontend +* backend + +* GitHub discussion +* GitHub issue + +* performance +* performance optimization + +* return type +* return value + +* security +* security scheme + +* task +* background task +* task function + +* template +* template engine + +* type annotation +* type hint + +* server worker +* Uvicorn worker +* Gunicorn Worker +* worker process +* worker class +* workload + +* deployment +* deploy하다 + +* SDK +* software development kit + +* `APIRouter` +* `requirements.txt` +* Bearer Token +* breaking change +* bug +* button +* callable +* code +* commit +* context manager +* coroutine +* database session +* disk +* domain +* engine +* fake X +* HTTP GET method +* item +* library +* lifespan +* lock +* middleware +* mobile application +* module +* mounting +* network +* origin +* override +* payload +* processor +* property +* proxy +* pull request +* query +* RAM +* remote machine +* status code +* string +* tag +* web framework +* wildcard +* return하다 +* validate하다 + +//// + +//// tab | 정보 + +이것은 문서에서 보이는 (대부분) 기술 용어의 불완전하고 비규범적인 목록입니다. 프롬프트 설계자가 어떤 용어에 대해 LLM에 추가적인 도움이 필요한지 파악하는 데 유용할 수 있습니다. 예를 들어, 좋은 번역을 계속 덜 좋은 번역으로 되돌릴 때, 또는 언어에서 용어의 활용/변화를 처리하는 데 문제가 있을 때 도움이 됩니다. + +예를 들어 `docs/de/llm-prompt.md`의 `### List of English terms and their preferred German translations` 섹션을 참고하세요. + +//// diff --git a/docs/ko/docs/advanced/additional-responses.md b/docs/ko/docs/advanced/additional-responses.md new file mode 100644 index 000000000..a6f51f5b9 --- /dev/null +++ b/docs/ko/docs/advanced/additional-responses.md @@ -0,0 +1,247 @@ +# OpenAPI에서 추가 응답 { #additional-responses-in-openapi } + +/// warning | 경고 + +이는 꽤 고급 주제입니다. + +**FastAPI**를 막 시작했다면, 이 내용이 필요 없을 수도 있습니다. + +/// + +추가 상태 코드, 미디어 타입, 설명 등을 포함한 추가 응답을 선언할 수 있습니다. + +이러한 추가 응답은 OpenAPI 스키마에 포함되므로 API 문서에도 표시됩니다. + +하지만 이러한 추가 응답의 경우, 상태 코드와 콘텐츠를 포함하여 `JSONResponse` 같은 `Response`를 직접 반환하도록 반드시 처리해야 합니다. + +## `model`을 사용한 추가 응답 { #additional-response-with-model } + +*경로 처리 데코레이터*에 `responses` 파라미터를 전달할 수 있습니다. + +이는 `dict`를 받습니다. 키는 각 응답의 상태 코드(예: `200`)이고, 값은 각 응답에 대한 정보를 담은 다른 `dict`입니다. + +각 응답 `dict`에는 `response_model`처럼 Pydantic 모델을 담는 `model` 키가 있을 수 있습니다. + +**FastAPI**는 그 모델을 사용해 JSON Schema를 생성하고, OpenAPI의 올바른 위치에 포함합니다. + +예를 들어, 상태 코드 `404`와 Pydantic 모델 `Message`를 사용하는 다른 응답을 선언하려면 다음과 같이 작성할 수 있습니다: + +{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *} + +/// note | 참고 + +`JSONResponse`를 직접 반환해야 한다는 점을 기억하세요. + +/// + +/// info | 정보 + +`model` 키는 OpenAPI의 일부가 아닙니다. + +**FastAPI**는 여기에서 Pydantic 모델을 가져와 JSON Schema를 생성하고 올바른 위치에 넣습니다. + +올바른 위치는 다음과 같습니다: + +* 값으로 또 다른 JSON 객체(`dict`)를 가지는 `content` 키 안에: + * 미디어 타입(예: `application/json`)을 키로 가지며, 값으로 또 다른 JSON 객체를 포함하고: + * `schema` 키가 있고, 그 값이 모델에서 생성된 JSON Schema입니다. 이것이 올바른 위치입니다. + * **FastAPI**는 이를 직접 포함하는 대신, OpenAPI의 다른 위치에 있는 전역 JSON Schemas를 참조하도록 여기에서 reference를 추가합니다. 이렇게 하면 다른 애플리케이션과 클라이언트가 그 JSON Schema를 직접 사용할 수 있고, 더 나은 코드 생성 도구 등을 제공할 수 있습니다. + +/// + +이 *경로 처리*에 대해 OpenAPI에 생성되는 응답은 다음과 같습니다: + +```JSON hl_lines="3-12" +{ + "responses": { + "404": { + "description": "Additional Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } +} +``` + +스키마는 OpenAPI 스키마 내부의 다른 위치를 참조합니다: + +```JSON hl_lines="4-16" +{ + "components": { + "schemas": { + "Message": { + "title": "Message", + "required": [ + "message" + ], + "type": "object", + "properties": { + "message": { + "title": "Message", + "type": "string" + } + } + }, + "Item": { + "title": "Item", + "required": [ + "id", + "value" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + } + }, + "ValidationError": { + "title": "ValidationError", + "required": [ + "loc", + "msg", + "type" + ], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "type": "string" + } + }, + "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" + } + } + } + } + } + } +} +``` + +## 주요 응답에 대한 추가 미디어 타입 { #additional-media-types-for-the-main-response } + +같은 `responses` 파라미터를 사용해 동일한 주요 응답에 대해 다른 미디어 타입을 추가할 수도 있습니다. + +예를 들어, *경로 처리*가 JSON 객체(미디어 타입 `application/json`) 또는 PNG 이미지(미디어 타입 `image/png`)를 반환할 수 있다고 선언하기 위해 `image/png`라는 추가 미디어 타입을 추가할 수 있습니다: + +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} + +/// note | 참고 + +이미지는 `FileResponse`를 사용해 직접 반환해야 한다는 점에 유의하세요. + +/// + +/// info | 정보 + +`responses` 파라미터에서 다른 미디어 타입을 명시적으로 지정하지 않는 한, FastAPI는 응답이 주요 응답 클래스와 동일한 미디어 타입(기본값 `application/json`)을 가진다고 가정합니다. + +하지만 커스텀 응답 클래스를 지정하면서 미디어 타입을 `None`으로 설정했다면, FastAPI는 연결된 모델이 있는 모든 추가 응답에 대해 `application/json`을 사용합니다. + +/// + +## 정보 결합하기 { #combining-information } + +`response_model`, `status_code`, `responses` 파라미터를 포함해 여러 위치의 응답 정보를 결합할 수도 있습니다. + +기본 상태 코드 `200`(또는 필요하다면 커스텀 코드)을 사용하여 `response_model`을 선언하고, 그와 동일한 응답에 대한 추가 정보를 `responses`에서 OpenAPI 스키마에 직접 선언할 수 있습니다. + +**FastAPI**는 `responses`의 추가 정보를 유지하고, 모델의 JSON Schema와 결합합니다. + +예를 들어, Pydantic 모델을 사용하고 커스텀 `description`을 가진 상태 코드 `404` 응답을 선언할 수 있습니다. + +또한 `response_model`을 사용하는 상태 코드 `200` 응답을 선언하되, 커스텀 `example`을 포함할 수도 있습니다: + +{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *} + +이 모든 내용은 OpenAPI에 결합되어 포함되고, API 문서에 표시됩니다: + +<img src="/img/tutorial/additional-responses/image01.png"> + +## 미리 정의된 응답과 커스텀 응답 결합하기 { #combine-predefined-responses-and-custom-ones } + +여러 *경로 처리*에 적용되는 미리 정의된 응답이 필요할 수도 있지만, 각 *경로 처리*마다 필요한 커스텀 응답과 결합하고 싶을 수도 있습니다. + +그런 경우 Python의 `dict` “unpacking” 기법인 `**dict_to_unpack`을 사용할 수 있습니다: + +```Python +old_dict = { + "old key": "old value", + "second old key": "second old value", +} +new_dict = {**old_dict, "new key": "new value"} +``` + +여기서 `new_dict`는 `old_dict`의 모든 키-값 쌍에 더해 새 키-값 쌍까지 포함합니다: + +```Python +{ + "old key": "old value", + "second old key": "second old value", + "new key": "new value", +} +``` + +이 기법을 사용해 *경로 처리*에서 일부 미리 정의된 응답을 재사용하고, 추가 커스텀 응답과 결합할 수 있습니다. + +예를 들어: + +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} + +## OpenAPI 응답에 대한 추가 정보 { #more-information-about-openapi-responses } + +응답에 정확히 무엇을 포함할 수 있는지 보려면, OpenAPI 사양의 다음 섹션을 확인하세요: + +* <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#responses-object" class="external-link" target="_blank">OpenAPI Responses Object</a>: `Response Object`를 포함합니다. +* <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#response-object" class="external-link" target="_blank">OpenAPI Response Object</a>: `responses` 파라미터 안의 각 응답에 이것의 어떤 항목이든 직접 포함할 수 있습니다. `description`, `headers`, `content`(여기에서 서로 다른 미디어 타입과 JSON Schema를 선언합니다), `links` 등을 포함할 수 있습니다. diff --git a/docs/ko/docs/advanced/behind-a-proxy.md b/docs/ko/docs/advanced/behind-a-proxy.md new file mode 100644 index 000000000..92bddac51 --- /dev/null +++ b/docs/ko/docs/advanced/behind-a-proxy.md @@ -0,0 +1,466 @@ +# 프록시 뒤에서 실행하기 { #behind-a-proxy } + +많은 경우 FastAPI 앱 앞단에 Traefik이나 Nginx 같은 **프록시(proxy)**를 두고 사용합니다. + +이런 프록시는 HTTPS 인증서 처리 등 여러 작업을 담당할 수 있습니다. + +## 프록시 전달 헤더 { #proxy-forwarded-headers } + +애플리케이션 앞단의 **프록시**는 보통 **서버**로 요청을 보내기 전에, 해당 요청이 프록시에 의해 **전달(forwarded)**되었다는 것을 서버가 알 수 있도록 몇몇 헤더를 동적으로 설정합니다. 이를 통해 서버는 도메인을 포함한 원래의 (공개) URL, HTTPS 사용 여부 등 정보를 알 수 있습니다. + +**서버** 프로그램(예: **FastAPI CLI**를 통해 실행되는 **Uvicorn**)은 이런 헤더를 해석할 수 있고, 그 정보를 애플리케이션으로 전달할 수 있습니다. + +하지만 보안상, 서버는 자신이 신뢰할 수 있는 프록시 뒤에 있다는 것을 모르면 해당 헤더를 해석하지 않습니다. + +/// note | 기술 세부사항 + +프록시 헤더는 다음과 같습니다: + +* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For" class="external-link" target="_blank">X-Forwarded-For</a> +* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto" class="external-link" target="_blank">X-Forwarded-Proto</a> +* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host" class="external-link" target="_blank">X-Forwarded-Host</a> + +/// + +### 프록시 전달 헤더 활성화하기 { #enable-proxy-forwarded-headers } + +FastAPI CLI를 *CLI 옵션* `--forwarded-allow-ips`로 실행하고, 전달 헤더를 읽을 수 있도록 신뢰할 IP 주소들을 넘길 수 있습니다. + +`--forwarded-allow-ips="*"`로 설정하면 들어오는 모든 IP를 신뢰합니다. + +**서버**가 신뢰할 수 있는 **프록시** 뒤에 있고 프록시만 서버에 접근한다면, 이는 해당 **프록시**의 IP가 무엇이든 간에 받아들이게 됩니다. + +<div class="termy"> + +```console +$ fastapi run --forwarded-allow-ips="*" + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +### HTTPS에서 리디렉션 { #redirects-with-https } + +예를 들어, *경로 처리* `/items/`를 정의했다고 해봅시다: + +{* ../../docs_src/behind_a_proxy/tutorial001_01_py39.py hl[6] *} + +클라이언트가 `/items`로 접근하면, 기본적으로 `/items/`로 리디렉션됩니다. + +하지만 *CLI 옵션* `--forwarded-allow-ips`를 설정하기 전에는 `http://localhost:8000/items/`로 리디렉션될 수 있습니다. + +그런데 애플리케이션이 `https://mysuperapp.com`에 호스팅되어 있고, 리디렉션도 `https://mysuperapp.com/items/`로 되어야 할 수 있습니다. + +이때 `--proxy-headers`를 설정하면 FastAPI가 올바른 위치로 리디렉션할 수 있습니다. 😎 + +``` +https://mysuperapp.com/items/ +``` + +/// tip | 팁 + +HTTPS에 대해 더 알아보려면 가이드 [HTTPS에 대하여](../deployment/https.md){.internal-link target=_blank}를 확인하세요. + +/// + +### 프록시 전달 헤더가 동작하는 방식 { #how-proxy-forwarded-headers-work } + +다음은 **프록시**가 클라이언트와 **애플리케이션 서버** 사이에서 전달 헤더를 추가하는 과정을 시각적으로 나타낸 것입니다: + +```mermaid +sequenceDiagram + participant Client + participant Proxy as Proxy/Load Balancer + participant Server as FastAPI Server + + Client->>Proxy: HTTPS Request<br/>Host: mysuperapp.com<br/>Path: /items + + Note over Proxy: Proxy adds forwarded headers + + Proxy->>Server: HTTP Request<br/>X-Forwarded-For: [client IP]<br/>X-Forwarded-Proto: https<br/>X-Forwarded-Host: mysuperapp.com<br/>Path: /items + + Note over Server: Server interprets headers<br/>(if --forwarded-allow-ips is set) + + Server->>Proxy: HTTP Response<br/>with correct HTTPS URLs + + Proxy->>Client: HTTPS Response +``` + +**프록시**는 원래의 클라이언트 요청을 가로채고, **애플리케이션 서버**로 요청을 전달하기 전에 특수한 *forwarded* 헤더(`X-Forwarded-*`)를 추가합니다. + +이 헤더들은 그렇지 않으면 사라질 수 있는 원래 요청의 정보를 보존합니다: + +* **X-Forwarded-For**: 원래 클라이언트의 IP 주소 +* **X-Forwarded-Proto**: 원래 프로토콜(`https`) +* **X-Forwarded-Host**: 원래 호스트(`mysuperapp.com`) + +**FastAPI CLI**를 `--forwarded-allow-ips`로 설정하면, 이 헤더를 신뢰하고 사용합니다. 예를 들어 리디렉션에서 올바른 URL을 생성하는 데 사용됩니다. + +## 제거된 경로 접두사를 가진 프록시 { #proxy-with-a-stripped-path-prefix } + +애플리케이션에 경로 접두사(prefix)를 추가하는 프록시를 둘 수도 있습니다. + +이런 경우 `root_path`를 사용해 애플리케이션을 구성할 수 있습니다. + +`root_path`는 (FastAPI가 Starlette를 통해 기반으로 하는) ASGI 사양에서 제공하는 메커니즘입니다. + +`root_path`는 이러한 특정 사례를 처리하는 데 사용됩니다. + +또한 서브 애플리케이션을 마운트할 때 내부적으로도 사용됩니다. + +경로 접두사가 제거(stripped)되는 프록시가 있다는 것은, 코드에서는 `/app`에 경로를 선언하지만, 위에 한 겹(프록시)을 추가해 **FastAPI** 애플리케이션을 `/api/v1` 같은 경로 아래에 두는 것을 의미합니다. + +이 경우 원래 경로 `/app`은 실제로 `/api/v1/app`에서 서비스됩니다. + +코드는 모두 `/app`만 있다고 가정하고 작성되어 있는데도 말입니다. + +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[6] *} + +그리고 프록시는 요청을 앱 서버(아마 FastAPI CLI를 통해 실행되는 Uvicorn)로 전달하기 전에, 동적으로 **경로 접두사**를 **"제거"**합니다. 그래서 애플리케이션은 여전히 `/app`에서 서비스된다고 믿게 되고, 코드 전체를 `/api/v1` 접두사를 포함하도록 수정할 필요가 없어집니다. + +여기까지는 보통 정상적으로 동작합니다. + +하지만 통합 문서 UI(프론트엔드)를 열면, OpenAPI 스키마를 `/api/v1/openapi.json`이 아니라 `/openapi.json`에서 가져오려고 합니다. + +그래서 브라우저에서 실행되는 프론트엔드는 `/openapi.json`에 접근하려고 시도하지만 OpenAPI 스키마를 얻지 못합니다. + +앱에 대해 `/api/v1` 경로 접두사를 가진 프록시가 있으므로, 프론트엔드는 `/api/v1/openapi.json`에서 OpenAPI 스키마를 가져와야 합니다. + +```mermaid +graph LR + +browser("Browser") +proxy["Proxy on http://0.0.0.0:9999/api/v1/app"] +server["Server on http://127.0.0.1:8000/app"] + +browser --> proxy +proxy --> server +``` + +/// tip | 팁 + +IP `0.0.0.0`은 보통 해당 머신/서버에서 사용 가능한 모든 IP에서 프로그램이 리슨한다는 의미로 사용됩니다. + +/// + +문서 UI는 또한 OpenAPI 스키마에서 이 API `server`가 `/api/v1`(프록시 뒤) 위치에 있다고 선언해야 합니다. 예: + +```JSON hl_lines="4-8" +{ + "openapi": "3.1.0", + // More stuff here + "servers": [ + { + "url": "/api/v1" + } + ], + "paths": { + // More stuff here + } +} +``` + +이 예시에서 "Proxy"는 **Traefik** 같은 것이고, 서버는 **Uvicorn**으로 실행되는 FastAPI CLI처럼, FastAPI 애플리케이션을 실행하는 구성일 수 있습니다. + +### `root_path` 제공하기 { #providing-the-root-path } + +이를 달성하려면 다음처럼 커맨드 라인 옵션 `--root-path`를 사용할 수 있습니다: + +<div class="termy"> + +```console +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +Hypercorn을 사용한다면, Hypercorn에도 `--root-path` 옵션이 있습니다. + +/// note | 기술 세부사항 + +ASGI 사양은 이 사용 사례를 위해 `root_path`를 정의합니다. + +그리고 커맨드 라인 옵션 `--root-path`가 그 `root_path`를 제공합니다. + +/// + +### 현재 `root_path` 확인하기 { #checking-the-current-root-path } + +요청마다 애플리케이션에서 사용 중인 현재 `root_path`를 얻을 수 있는데, 이는 `scope` 딕셔너리(ASGI 사양의 일부)에 포함되어 있습니다. + +여기서는 데모 목적을 위해 메시지에 포함하고 있습니다. + +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[8] *} + +그 다음 Uvicorn을 다음과 같이 시작하면: + +<div class="termy"> + +```console +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +응답은 다음과 비슷할 것입니다: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +### FastAPI 앱에서 `root_path` 설정하기 { #setting-the-root-path-in-the-fastapi-app } + +또는 `--root-path` 같은 커맨드 라인 옵션(또는 동등한 방법)을 제공할 수 없는 경우, FastAPI 앱을 생성할 때 `root_path` 파라미터를 설정할 수 있습니다: + +{* ../../docs_src/behind_a_proxy/tutorial002_py39.py hl[3] *} + +`FastAPI`에 `root_path`를 전달하는 것은 Uvicorn이나 Hypercorn에 커맨드 라인 옵션 `--root-path`를 전달하는 것과 동일합니다. + +### `root_path`에 대하여 { #about-root-path } + +서버(Uvicorn)는 그 `root_path`를 앱에 전달하는 것 외에는 다른 용도로 사용하지 않는다는 점을 기억하세요. + +하지만 브라우저로 <a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app</a>에 접속하면 정상 응답을 볼 수 있습니다: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +따라서 `http://127.0.0.1:8000/api/v1/app`로 접근될 것이라고 기대하지는 않습니다. + +Uvicorn은 프록시가 `http://127.0.0.1:8000/app`에서 Uvicorn에 접근할 것을 기대하고, 그 위에 `/api/v1` 접두사를 추가하는 것은 프록시의 책임입니다. + +## 제거된 경로 접두사를 가진 프록시에 대하여 { #about-proxies-with-a-stripped-path-prefix } + +경로 접두사가 제거되는 프록시는 구성 방법 중 하나일 뿐이라는 점을 기억하세요. + +많은 경우 기본값은 프록시가 경로 접두사를 제거하지 않는 방식일 것입니다. + +그런 경우(경로 접두사를 제거하지 않는 경우) 프록시는 `https://myawesomeapp.com` 같은 곳에서 리슨하고, 브라우저가 `https://myawesomeapp.com/api/v1/app`로 접근하면, 서버(예: Uvicorn)가 `http://127.0.0.1:8000`에서 리슨하고 있을 때 프록시(경로 접두사를 제거하지 않는)는 동일한 경로로 Uvicorn에 접근합니다: `http://127.0.0.1:8000/api/v1/app`. + +## Traefik으로 로컬 테스트하기 { #testing-locally-with-traefik } + +<a href="https://docs.traefik.io/" class="external-link" target="_blank">Traefik</a>을 사용하면, 경로 접두사가 제거되는 구성을 로컬에서 쉽게 실험할 수 있습니다. + +<a href="https://github.com/containous/traefik/releases" class="external-link" target="_blank">Traefik 다운로드</a>는 단일 바이너리이며, 압축 파일을 풀고 터미널에서 바로 실행할 수 있습니다. + +그 다음 다음 내용을 가진 `traefik.toml` 파일을 생성하세요: + +```TOML hl_lines="3" +[entryPoints] + [entryPoints.http] + address = ":9999" + +[providers] + [providers.file] + filename = "routes.toml" +``` + +이는 Traefik이 9999 포트에서 리슨하고, 다른 파일 `routes.toml`을 사용하도록 지시합니다. + +/// tip | 팁 + +표준 HTTP 포트 80 대신 9999 포트를 사용해서, 관리자(`sudo`) 권한으로 실행하지 않아도 되게 했습니다. + +/// + +이제 다른 파일 `routes.toml`을 생성하세요: + +```TOML hl_lines="5 12 20" +[http] + [http.middlewares] + + [http.middlewares.api-stripprefix.stripPrefix] + prefixes = ["/api/v1"] + + [http.routers] + + [http.routers.app-http] + entryPoints = ["http"] + service = "app" + rule = "PathPrefix(`/api/v1`)" + middlewares = ["api-stripprefix"] + + [http.services] + + [http.services.app] + [http.services.app.loadBalancer] + [[http.services.app.loadBalancer.servers]] + url = "http://127.0.0.1:8000" +``` + +이 파일은 Traefik이 경로 접두사 `/api/v1`을 사용하도록 설정합니다. + +그리고 Traefik은 요청을 `http://127.0.0.1:8000`에서 실행 중인 Uvicorn으로 전달합니다. + +이제 Traefik을 시작하세요: + +<div class="termy"> + +```console +$ ./traefik --configFile=traefik.toml + +INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml +``` + +</div> + +그리고 `--root-path` 옵션을 사용해 앱을 시작하세요: + +<div class="termy"> + +```console +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +### 응답 확인하기 { #check-the-responses } + +이제 Uvicorn의 포트로 된 URL인 <a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app</a>로 접속하면 정상 응답을 볼 수 있습니다: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +/// tip | 팁 + +`http://127.0.0.1:8000/app`로 접근했는데도 `/api/v1`의 `root_path`가 표시되는 것에 주의하세요. 이는 옵션 `--root-path`에서 가져온 값입니다. + +/// + +이제 Traefik의 포트가 포함되고 경로 접두사가 포함된 URL <a href="http://127.0.0.1:9999/api/v1/app" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/app</a>을 여세요. + +동일한 응답을 얻습니다: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +하지만 이번에는 프록시가 제공한 접두사 경로 `/api/v1`이 포함된 URL에서의 응답입니다. + +물론 여기서의 아이디어는 모두가 프록시를 통해 앱에 접근한다는 것이므로, `/api/v1` 경로 접두사가 있는 버전이 "올바른" 접근입니다. + +그리고 경로 접두사가 없는 버전(`http://127.0.0.1:8000/app`)은 Uvicorn이 직접 제공하는 것이며, 오직 _프록시_(Traefik)가 접근하기 위한 용도입니다. + +이는 프록시(Traefik)가 경로 접두사를 어떻게 사용하는지, 그리고 서버(Uvicorn)가 옵션 `--root-path`로부터의 `root_path`를 어떻게 사용하는지를 보여줍니다. + +### 문서 UI 확인하기 { #check-the-docs-ui } + +하지만 재미있는 부분은 여기입니다. ✨ + +앱에 접근하는 "공식" 방법은 우리가 정의한 경로 접두사를 가진 프록시를 통해서입니다. 따라서 기대하는 대로, URL에 경로 접두사가 없는 상태에서 Uvicorn이 직접 제공하는 docs UI를 시도하면, 프록시를 통해 접근된다고 가정하고 있기 때문에 동작하지 않습니다. + +<a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>에서 확인할 수 있습니다: + +<img src="/img/tutorial/behind-a-proxy/image01.png"> + +하지만 프록시(포트 `9999`)를 사용해 "공식" URL인 `/api/v1/docs`에서 docs UI에 접근하면, 올바르게 동작합니다! 🎉 + +<a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a>에서 확인할 수 있습니다: + +<img src="/img/tutorial/behind-a-proxy/image02.png"> + +원하던 그대로입니다. ✔️ + +이는 FastAPI가 이 `root_path`를 사용해, OpenAPI에서 기본 `server`를 `root_path`가 제공한 URL로 생성하기 때문입니다. + +## 추가 서버 { #additional-servers } + +/// warning | 경고 + +이는 더 고급 사용 사례입니다. 건너뛰어도 괜찮습니다. + +/// + +기본적으로 **FastAPI**는 OpenAPI 스키마에서 `root_path`의 URL로 `server`를 생성합니다. + +하지만 예를 들어 동일한 docs UI가 스테이징과 프로덕션 환경 모두와 상호작용하도록 하려면, 다른 대안 `servers`를 제공할 수도 있습니다. + +사용자 정의 `servers` 리스트를 전달했고 `root_path`(API가 프록시 뒤에 있기 때문)가 있다면, **FastAPI**는 리스트의 맨 앞에 이 `root_path`를 가진 "server"를 삽입합니다. + +예: + +{* ../../docs_src/behind_a_proxy/tutorial003_py39.py hl[4:7] *} + +다음과 같은 OpenAPI 스키마를 생성합니다: + +```JSON hl_lines="5-7" +{ + "openapi": "3.1.0", + // More stuff here + "servers": [ + { + "url": "/api/v1" + }, + { + "url": "https://stag.example.com", + "description": "Staging environment" + }, + { + "url": "https://prod.example.com", + "description": "Production environment" + } + ], + "paths": { + // More stuff here + } +} +``` + +/// tip | 팁 + +`root_path`에서 가져온 값인 `/api/v1`의 `url` 값을 가진, 자동 생성된 server에 주목하세요. + +/// + +<a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a>의 docs UI에서는 다음처럼 보입니다: + +<img src="/img/tutorial/behind-a-proxy/image03.png"> + +/// tip | 팁 + +docs UI는 선택한 server와 상호작용합니다. + +/// + +/// note | 기술 세부사항 + +OpenAPI 사양에서 `servers` 속성은 선택 사항입니다. + +`servers` 파라미터를 지정하지 않고 `root_path`가 `/`와 같다면, 생성된 OpenAPI 스키마의 `servers` 속성은 기본적으로 완전히 생략되며, 이는 `url` 값이 `/`인 단일 server와 동등합니다. + +/// + +### `root_path`에서 자동 server 비활성화하기 { #disable-automatic-server-from-root-path } + +**FastAPI**가 `root_path`를 사용한 자동 server를 포함하지 않게 하려면, `root_path_in_servers=False` 파라미터를 사용할 수 있습니다: + +{* ../../docs_src/behind_a_proxy/tutorial004_py39.py hl[9] *} + +그러면 OpenAPI 스키마에 포함되지 않습니다. + +## 서브 애플리케이션 마운트하기 { #mounting-a-sub-application } + +프록시에서 `root_path`를 사용하면서도, [서브 애플리케이션 - 마운트](sub-applications.md){.internal-link target=_blank}에 설명된 것처럼 서브 애플리케이션을 마운트해야 한다면, 기대하는 대로 일반적으로 수행할 수 있습니다. + +FastAPI가 내부적으로 `root_path`를 똑똑하게 사용하므로, 그냥 동작합니다. ✨ diff --git a/docs/ko/docs/advanced/dataclasses.md b/docs/ko/docs/advanced/dataclasses.md new file mode 100644 index 000000000..92ad5545b --- /dev/null +++ b/docs/ko/docs/advanced/dataclasses.md @@ -0,0 +1,95 @@ +# Dataclasses 사용하기 { #using-dataclasses } + +FastAPI는 **Pydantic** 위에 구축되어 있으며, 지금까지는 Pydantic 모델을 사용해 요청과 응답을 선언하는 방법을 보여드렸습니다. + +하지만 FastAPI는 <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a>도 같은 방식으로 사용하는 것을 지원합니다: + +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} + +이는 **Pydantic** 덕분에 여전히 지원되는데, Pydantic이 <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">`dataclasses`에 대한 내부 지원</a>을 제공하기 때문입니다. + +따라서 위 코드처럼 Pydantic을 명시적으로 사용하지 않더라도, FastAPI는 Pydantic을 사용해 표준 dataclasses를 Pydantic의 dataclasses 변형으로 변환합니다. + +그리고 물론 다음과 같은 기능도 동일하게 지원합니다: + +* 데이터 검증 +* 데이터 직렬화 +* 데이터 문서화 등 + +이는 Pydantic 모델을 사용할 때와 같은 방식으로 동작합니다. 그리고 실제로도 내부적으로는 Pydantic을 사용해 같은 방식으로 구현됩니다. + +/// info | 정보 + +dataclasses는 Pydantic 모델이 할 수 있는 모든 것을 할 수는 없다는 점을 기억하세요. + +그래서 여전히 Pydantic 모델을 사용해야 할 수도 있습니다. + +하지만 이미 여러 dataclasses를 가지고 있다면, 이것은 FastAPI로 웹 API를 구동하는 데 그것들을 활용할 수 있는 좋은 방법입니다. 🤓 + +/// + +## `response_model`에서 Dataclasses 사용하기 { #dataclasses-in-response-model } + +`response_model` 매개변수에서도 `dataclasses`를 사용할 수 있습니다: + +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} + +dataclass는 자동으로 Pydantic dataclass로 변환됩니다. + +이렇게 하면 해당 스키마가 API docs 사용자 인터페이스에 표시됩니다: + +<img src="/img/tutorial/dataclasses/image01.png"> + +## 중첩 데이터 구조에서 Dataclasses 사용하기 { #dataclasses-in-nested-data-structures } + +`dataclasses`를 다른 타입 애너테이션과 조합해 중첩 데이터 구조를 만들 수도 있습니다. + +일부 경우에는 Pydantic 버전의 `dataclasses`를 사용해야 할 수도 있습니다. 예를 들어 자동 생성된 API 문서에서 오류가 발생하는 경우입니다. + +그런 경우 표준 `dataclasses`를 드롭인 대체재인 `pydantic.dataclasses`로 간단히 바꾸면 됩니다: + +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} + +1. 표준 `dataclasses`에서 `field`를 계속 임포트합니다. + +2. `pydantic.dataclasses`는 `dataclasses`의 드롭인 대체재입니다. + +3. `Author` dataclass에는 `Item` dataclasses의 리스트가 포함됩니다. + +4. `Author` dataclass가 `response_model` 매개변수로 사용됩니다. + +5. 요청 본문으로 dataclasses와 함께 다른 표준 타입 애너테이션을 사용할 수 있습니다. + + 이 경우에는 `Item` dataclasses의 리스트입니다. + +6. 여기서는 dataclasses 리스트인 `items`를 포함하는 딕셔너리를 반환합니다. + + FastAPI는 여전히 데이터를 JSON으로 <abbr title="converting the data to a format that can be transmitted - 데이터를 전송 가능한 형식으로 변환하는 것">serializing</abbr>할 수 있습니다. + +7. 여기서 `response_model`은 `Author` dataclasses 리스트에 대한 타입 애너테이션을 사용합니다. + + 다시 말해, `dataclasses`를 표준 타입 애너테이션과 조합할 수 있습니다. + +8. 이 *경로 처리 함수*는 `async def` 대신 일반 `def`를 사용하고 있다는 점에 주목하세요. + + 언제나처럼 FastAPI에서는 필요에 따라 `def`와 `async def`를 조합해 사용할 수 있습니다. + + 어떤 것을 언제 사용해야 하는지 다시 확인하고 싶다면, [`async`와 `await`](../async.md#in-a-hurry){.internal-link target=_blank} 문서의 _"급하신가요?"_ 섹션을 확인하세요. + +9. 이 *경로 처리 함수*는 dataclasses를(물론 반환할 수도 있지만) 반환하지 않고, 내부 데이터를 담은 딕셔너리들의 리스트를 반환합니다. + + FastAPI는 `response_model` 매개변수(dataclasses 포함)를 사용해 응답을 변환합니다. + +`dataclasses`는 다른 타입 애너테이션과 매우 다양한 조합으로 결합해 복잡한 데이터 구조를 구성할 수 있습니다. + +더 구체적인 내용은 위 코드 내 애너테이션 팁을 확인하세요. + +## 더 알아보기 { #learn-more } + +`dataclasses`를 다른 Pydantic 모델과 조합하거나, 이를 상속하거나, 여러분의 모델에 포함하는 등의 작업도 할 수 있습니다. + +자세한 내용은 <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/" class="external-link" target="_blank">dataclasses에 관한 Pydantic 문서</a>를 참고하세요. + +## 버전 { #version } + +이 기능은 FastAPI `0.67.0` 버전부터 사용할 수 있습니다. 🔖 diff --git a/docs/ko/docs/advanced/generate-clients.md b/docs/ko/docs/advanced/generate-clients.md new file mode 100644 index 000000000..1def3efe1 --- /dev/null +++ b/docs/ko/docs/advanced/generate-clients.md @@ -0,0 +1,208 @@ +# SDK 생성하기 { #generating-sdks } + +**FastAPI**는 **OpenAPI** 사양을 기반으로 하므로, FastAPI의 API는 많은 도구가 이해할 수 있는 표준 형식으로 설명할 수 있습니다. + +덕분에 여러 언어용 클라이언트 라이브러리(<abbr title="Software Development Kits - 소프트웨어 개발 키트">**SDKs**</abbr>), 최신 **문서**, 그리고 코드와 동기화된 **테스트** 또는 **자동화 워크플로**를 쉽게 생성할 수 있습니다. + +이 가이드에서는 FastAPI 백엔드용 **TypeScript SDK**를 생성하는 방법을 배웁니다. + +## 오픈 소스 SDK 생성기 { #open-source-sdk-generators } + +다양하게 활용할 수 있는 옵션으로 <a href="https://openapi-generator.tech/" class="external-link" target="_blank">OpenAPI Generator</a>가 있으며, **다양한 프로그래밍 언어**를 지원하고 OpenAPI 사양으로부터 SDK를 생성할 수 있습니다. + +**TypeScript 클라이언트**의 경우 <a href="https://heyapi.dev/" class="external-link" target="_blank">Hey API</a>는 TypeScript 생태계에 최적화된 경험을 제공하는 목적에 맞게 설계된 솔루션입니다. + +더 많은 SDK 생성기는 <a href="https://openapi.tools/#sdk" class="external-link" target="_blank">OpenAPI.Tools</a>에서 확인할 수 있습니다. + +/// tip | 팁 + +FastAPI는 **OpenAPI 3.1** 사양을 자동으로 생성하므로, 사용하는 도구는 이 버전을 지원해야 합니다. + +/// + +## FastAPI 스폰서의 SDK 생성기 { #sdk-generators-from-fastapi-sponsors } + +이 섹션에서는 FastAPI를 후원하는 회사들이 제공하는 **벤처 투자 기반** 및 **기업 지원** 솔루션을 소개합니다. 이 제품들은 고품질로 생성된 SDK에 더해 **추가 기능**과 **통합**을 제공합니다. + +✨ [**FastAPI 후원하기**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨를 통해, 이 회사들은 프레임워크와 그 **생태계**가 건강하고 **지속 가능**하게 유지되도록 돕습니다. + +또한 이들의 후원은 FastAPI **커뮤니티**(여러분)에 대한 강한 헌신을 보여주며, **좋은 서비스**를 제공하는 것뿐 아니라, 견고하고 활발한 프레임워크인 FastAPI를 지원하는 데에도 관심이 있음을 나타냅니다. 🙇 + +예를 들어 다음을 사용해 볼 수 있습니다: + +* <a href="https://speakeasy.com/editor?utm_source=fastapi+repo&utm_medium=github+sponsorship" class="external-link" target="_blank">Speakeasy</a> +* <a href="https://www.stainless.com/?utm_source=fastapi&utm_medium=referral" class="external-link" target="_blank">Stainless</a> +* <a href="https://developers.liblab.com/tutorials/sdk-for-fastapi?utm_source=fastapi" class="external-link" target="_blank">liblab</a> + +이 중 일부는 오픈 소스이거나 무료 티어를 제공하므로, 비용 부담 없이 사용해 볼 수 있습니다. 다른 상용 SDK 생성기도 있으며 온라인에서 찾을 수 있습니다. 🤓 + +## TypeScript SDK 만들기 { #create-a-typescript-sdk } + +간단한 FastAPI 애플리케이션으로 시작해 보겠습니다: + +{* ../../docs_src/generate_clients/tutorial001_py39.py hl[7:9,12:13,16:17,21] *} + +*path operation*에서 요청 페이로드와 응답 페이로드에 사용하는 모델을 `Item`, `ResponseMessage` 모델로 정의하고 있다는 점에 주목하세요. + +### API 문서 { #api-docs } + +`/docs`로 이동하면, 요청으로 보낼 데이터와 응답으로 받을 데이터에 대한 **스키마(schemas)**가 있는 것을 볼 수 있습니다: + +<img src="/img/tutorial/generate-clients/image01.png"> + +이 스키마는 앱에서 모델로 선언되었기 때문에 볼 수 있습니다. + +그 정보는 앱의 **OpenAPI 스키마**에서 사용할 수 있고, 이후 API 문서에 표시됩니다. + +OpenAPI에 포함된 모델의 동일한 정보가 **클라이언트 코드 생성**에 사용될 수 있습니다. + +### Hey API { #hey-api } + +모델이 포함된 FastAPI 앱이 준비되면, Hey API를 사용해 TypeScript 클라이언트를 생성할 수 있습니다. 가장 빠른 방법은 npx를 사용하는 것입니다. + +```sh +npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client +``` + +이 명령은 `./src/client`에 TypeScript SDK를 생성합니다. + +<a href="https://heyapi.dev/openapi-ts/get-started" class="external-link" target="_blank">`@hey-api/openapi-ts` 설치 방법</a>과 <a href="https://heyapi.dev/openapi-ts/output" class="external-link" target="_blank">생성된 결과물</a>은 해당 웹사이트에서 확인할 수 있습니다. + +### SDK 사용하기 { #using-the-sdk } + +이제 클라이언트 코드를 import해서 사용할 수 있습니다. 아래처럼 사용할 수 있으며, 메서드에 대한 자동 완성이 제공되는 것을 확인할 수 있습니다: + +<img src="/img/tutorial/generate-clients/image02.png"> + +보낼 페이로드에 대해서도 자동 완성이 제공됩니다: + +<img src="/img/tutorial/generate-clients/image03.png"> + +/// tip | 팁 + +`name`과 `price`에 대한 자동 완성은 FastAPI 애플리케이션에서 `Item` 모델에 정의된 내용입니다. + +/// + +전송하는 데이터에 대해 인라인 오류도 표시됩니다: + +<img src="/img/tutorial/generate-clients/image04.png"> + +응답 객체도 자동 완성을 제공합니다: + +<img src="/img/tutorial/generate-clients/image05.png"> + +## 태그가 있는 FastAPI 앱 { #fastapi-app-with-tags } + +대부분의 경우 FastAPI 앱은 더 커지고, 서로 다른 *path operations* 그룹을 분리하기 위해 태그를 사용하게 될 가능성이 큽니다. + +예를 들어 **items** 섹션과 **users** 섹션이 있고, 이를 태그로 분리할 수 있습니다: + +{* ../../docs_src/generate_clients/tutorial002_py39.py hl[21,26,34] *} + +### 태그로 TypeScript 클라이언트 생성하기 { #generate-a-typescript-client-with-tags } + +태그를 사용하는 FastAPI 앱에 대해 클라이언트를 생성하면, 일반적으로 생성된 클라이언트 코드도 태그를 기준으로 분리됩니다. + +이렇게 하면 클라이언트 코드에서 항목들이 올바르게 정렬되고 그룹화됩니다: + +<img src="/img/tutorial/generate-clients/image06.png"> + +이 경우 다음이 있습니다: + +* `ItemsService` +* `UsersService` + +### 클라이언트 메서드 이름 { #client-method-names } + +현재 `createItemItemsPost` 같은 생성된 메서드 이름은 그다지 깔끔하지 않습니다: + +```TypeScript +ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) +``` + +...이는 클라이언트 생성기가 각 *path operation*에 대해 OpenAPI 내부의 **operation ID**를 사용하기 때문입니다. + +OpenAPI는 모든 *path operations* 전체에서 operation ID가 각각 유일해야 한다고 요구합니다. 그래서 FastAPI는 operation ID가 유일하도록 **함수 이름**, **경로**, **HTTP method/operation**을 조합해 operation ID를 생성합니다. + +하지만 다음에서 이를 개선하는 방법을 보여드리겠습니다. 🤓 + +## 커스텀 Operation ID와 더 나은 메서드 이름 { #custom-operation-ids-and-better-method-names } + +클라이언트에서 **더 단순한 메서드 이름**을 갖도록, operation ID가 **생성되는 방식**을 **수정**할 수 있습니다. + +이 경우 operation ID가 다른 방식으로도 **유일**하도록 보장해야 합니다. + +예를 들어 각 *path operation*이 태그를 갖도록 한 다음, **태그**와 *path operation* **이름**(함수 이름)을 기반으로 operation ID를 생성할 수 있습니다. + +### 유일 ID 생성 함수 커스터마이징 { #custom-generate-unique-id-function } + +FastAPI는 각 *path operation*에 대해 **유일 ID**를 사용하며, 이는 **operation ID** 및 요청/응답에 필요한 커스텀 모델 이름에도 사용됩니다. + +이 함수를 커스터마이징할 수 있습니다. 이 함수는 `APIRoute`를 받아 문자열을 반환합니다. + +예를 들어 아래에서는 첫 번째 태그(대부분 태그는 하나만 있을 것입니다)와 *path operation* 이름(함수 이름)을 사용합니다. + +그 다음 이 커스텀 함수를 `generate_unique_id_function` 매개변수로 **FastAPI**에 전달할 수 있습니다: + +{* ../../docs_src/generate_clients/tutorial003_py39.py hl[6:7,10] *} + +### 커스텀 Operation ID로 TypeScript 클라이언트 생성하기 { #generate-a-typescript-client-with-custom-operation-ids } + +이제 클라이언트를 다시 생성하면, 개선된 메서드 이름을 확인할 수 있습니다: + +<img src="/img/tutorial/generate-clients/image07.png"> + +보시다시피, 이제 메서드 이름은 태그 다음에 함수 이름이 오며, URL 경로와 HTTP operation의 정보는 포함하지 않습니다. + +### 클라이언트 생성기를 위한 OpenAPI 사양 전처리 { #preprocess-the-openapi-specification-for-the-client-generator } + +생성된 코드에는 여전히 일부 **중복 정보**가 있습니다. + +`ItemsService`(태그에서 가져옴)에 이미 **items**가 포함되어 있어 이 메서드가 items와 관련되어 있음을 알 수 있지만, 메서드 이름에도 태그 이름이 접두사로 붙어 있습니다. 😕 + +OpenAPI 전반에서는 operation ID가 **유일**하다는 것을 보장하기 위해 이 방식을 유지하고 싶을 수 있습니다. + +하지만 생성된 클라이언트에서는, 클라이언트를 생성하기 직전에 OpenAPI operation ID를 **수정**해서 메서드 이름을 더 보기 좋고 **깔끔하게** 만들 수 있습니다. + +OpenAPI JSON을 `openapi.json` 파일로 다운로드한 뒤, 아래와 같은 스크립트로 **접두사 태그를 제거**할 수 있습니다: + +{* ../../docs_src/generate_clients/tutorial004_py39.py *} + +//// tab | Node.js + +```Javascript +{!> ../../docs_src/generate_clients/tutorial004.js!} +``` + +//// + +이렇게 하면 operation ID가 `items-get_items` 같은 형태에서 `get_items`로 변경되어, 클라이언트 생성기가 더 단순한 메서드 이름을 생성할 수 있습니다. + +### 전처리된 OpenAPI로 TypeScript 클라이언트 생성하기 { #generate-a-typescript-client-with-the-preprocessed-openapi } + +이제 최종 결과가 `openapi.json` 파일에 있으므로, 입력 위치를 업데이트해야 합니다: + +```sh +npx @hey-api/openapi-ts -i ./openapi.json -o src/client +``` + +새 클라이언트를 생성한 후에는 **깔끔한 메서드 이름**을 가지면서도, **자동 완성**, **인라인 오류** 등은 그대로 제공됩니다: + +<img src="/img/tutorial/generate-clients/image08.png"> + +## 장점 { #benefits } + +자동으로 생성된 클라이언트를 사용하면 다음에 대해 **자동 완성**을 받을 수 있습니다: + +* 메서드 +* 본문(body)의 요청 페이로드, 쿼리 파라미터 등 +* 응답 페이로드 + +또한 모든 것에 대해 **인라인 오류**도 확인할 수 있습니다. + +그리고 백엔드 코드를 업데이트한 뒤 프론트엔드를 **재생성(regenerate)**하면, 새 *path operations*가 메서드로 추가되고 기존 것은 제거되며, 그 밖의 변경 사항도 생성된 코드에 반영됩니다. 🤓 + +이는 무언가 변경되면 그 변경이 클라이언트 코드에도 자동으로 **반영**된다는 뜻입니다. 또한 클라이언트를 **빌드(build)**하면 사용된 데이터가 **불일치(mismatch)**할 경우 오류가 발생합니다. + +따라서 운영 환경에서 최종 사용자에게 오류가 노출된 뒤 문제를 추적하는 대신, 개발 사이클 초기에 **많은 오류를 매우 빨리 감지**할 수 있습니다. ✨ diff --git a/docs/ko/docs/advanced/middleware.md b/docs/ko/docs/advanced/middleware.md new file mode 100644 index 000000000..be2c972a6 --- /dev/null +++ b/docs/ko/docs/advanced/middleware.md @@ -0,0 +1,97 @@ +# 고급 Middleware { #advanced-middleware } + +메인 튜토리얼에서 애플리케이션에 [커스텀 Middleware](../tutorial/middleware.md){.internal-link target=_blank}를 추가하는 방법을 읽었습니다. + +그리고 [`CORSMiddleware`로 CORS 처리하기](../tutorial/cors.md){.internal-link target=_blank}도 읽었습니다. + +이 섹션에서는 다른 middleware들을 사용하는 방법을 살펴보겠습니다. + +## ASGI middleware 추가하기 { #adding-asgi-middlewares } + +**FastAPI**는 Starlette를 기반으로 하고 <abbr title="Asynchronous Server Gateway Interface">ASGI</abbr> 사양을 구현하므로, 어떤 ASGI middleware든 사용할 수 있습니다. + +ASGI 사양을 따르기만 하면, FastAPI나 Starlette를 위해 만들어진 middleware가 아니어도 동작합니다. + +일반적으로 ASGI middleware는 첫 번째 인자로 ASGI 앱을 받도록 기대하는 클래스입니다. + +그래서 서드파티 ASGI middleware 문서에서는 아마 다음과 같이 하라고 안내할 것입니다: + +```Python +from unicorn import UnicornMiddleware + +app = SomeASGIApp() + +new_app = UnicornMiddleware(app, some_config="rainbow") +``` + +하지만 FastAPI(정확히는 Starlette)는 더 간단한 방법을 제공하며, 이를 통해 내부 middleware가 서버 오류를 처리하고 커스텀 예외 핸들러가 올바르게 동작하도록 보장합니다. + +이를 위해(그리고 CORS 예제에서처럼) `app.add_middleware()`를 사용합니다. + +```Python +from fastapi import FastAPI +from unicorn import UnicornMiddleware + +app = FastAPI() + +app.add_middleware(UnicornMiddleware, some_config="rainbow") +``` + +`app.add_middleware()`는 첫 번째 인자로 middleware 클래스를 받고, 그 뒤에는 middleware에 전달할 추가 인자들을 받습니다. + +## 통합 middleware { #integrated-middlewares } + +**FastAPI**에는 일반적인 사용 사례를 위한 여러 middleware가 포함되어 있습니다. 다음에서 이를 사용하는 방법을 살펴보겠습니다. + +/// note | 기술 세부사항 + +다음 예제에서는 `from starlette.middleware.something import SomethingMiddleware`를 사용해도 됩니다. + +**FastAPI**는 개발자 편의를 위해 `fastapi.middleware`에 여러 middleware를 제공하지만, 사용 가능한 대부분의 middleware는 Starlette에서 직접 제공됩니다. + +/// + +## `HTTPSRedirectMiddleware` { #httpsredirectmiddleware } + +들어오는 모든 요청이 `https` 또는 `wss`여야 하도록 강제합니다. + +`http` 또는 `ws`로 들어오는 모든 요청은 대신 보안 스킴으로 리디렉션됩니다. + +{* ../../docs_src/advanced_middleware/tutorial001_py39.py hl[2,6] *} + +## `TrustedHostMiddleware` { #trustedhostmiddleware } + +HTTP Host Header 공격을 방어하기 위해, 들어오는 모든 요청에 올바르게 설정된 `Host` 헤더가 있어야 하도록 강제합니다. + +{* ../../docs_src/advanced_middleware/tutorial002_py39.py hl[2,6:8] *} + +다음 인자들을 지원합니다: + +* `allowed_hosts` - 호스트명으로 허용할 도메인 이름 목록입니다. `*.example.com` 같은 와일드카드 도메인으로 서브도메인을 매칭하는 것도 지원합니다. 어떤 호스트명이든 허용하려면 `allowed_hosts=["*"]`를 사용하거나 middleware를 생략하세요. +* `www_redirect` - True로 설정하면, 허용된 호스트의 non-www 버전으로 들어오는 요청을 www 버전으로 리디렉션합니다. 기본값은 `True`입니다. + +들어오는 요청이 올바르게 검증되지 않으면 `400` 응답이 전송됩니다. + +## `GZipMiddleware` { #gzipmiddleware } + +`Accept-Encoding` 헤더에 `"gzip"`이 포함된 어떤 요청이든 GZip 응답을 처리합니다. + +이 middleware는 일반 응답과 스트리밍 응답을 모두 처리합니다. + +{* ../../docs_src/advanced_middleware/tutorial003_py39.py hl[2,6] *} + +다음 인자들을 지원합니다: + +* `minimum_size` - 바이트 단위로 지정한 최소 크기보다 작은 응답은 GZip으로 압축하지 않습니다. 기본값은 `500`입니다. +* `compresslevel` - GZip 압축 중에 사용됩니다. 1부터 9까지의 정수입니다. 기본값은 `9`입니다. 값이 낮을수록 압축은 더 빠르지만 파일 크기는 더 커지고, 값이 높을수록 압축은 더 느리지만 파일 크기는 더 작아집니다. + +## 다른 middleware { #other-middlewares } + +다른 ASGI middleware도 많이 있습니다. + +예를 들어: + +* <a href="https://github.com/encode/uvicorn/blob/master/uvicorn/middleware/proxy_headers.py" class="external-link" target="_blank">Uvicorn의 `ProxyHeadersMiddleware`</a> +* <a href="https://github.com/florimondmanca/msgpack-asgi" class="external-link" target="_blank">MessagePack</a> + +사용 가능한 다른 middleware를 보려면 <a href="https://www.starlette.dev/middleware/" class="external-link" target="_blank">Starlette의 Middleware 문서</a>와 <a href="https://github.com/florimondmanca/awesome-asgi" class="external-link" target="_blank">ASGI Awesome List</a>를 확인하세요. diff --git a/docs/ko/docs/advanced/openapi-callbacks.md b/docs/ko/docs/advanced/openapi-callbacks.md new file mode 100644 index 000000000..e4bdea9d6 --- /dev/null +++ b/docs/ko/docs/advanced/openapi-callbacks.md @@ -0,0 +1,186 @@ +# OpenAPI 콜백 { #openapi-callbacks } + +다른 사람이 만든 *external API*(아마도 당신의 API를 *사용*할 동일한 개발자)가 요청을 트리거하도록 만드는 *경로 처리*를 가진 API를 만들 수 있습니다. + +당신의 API 앱이 *external API*를 호출할 때 일어나는 과정을 "callback"이라고 합니다. 외부 개발자가 작성한 소프트웨어가 당신의 API로 요청을 보낸 다음, 당신의 API가 다시 *external API*로 요청을 보내 *되돌려 호출*하기 때문입니다(아마도 같은 개발자가 만든 API일 것입니다). + +이 경우, 그 *external API*가 어떤 형태여야 하는지 문서화하고 싶을 수 있습니다. 어떤 *경로 처리*를 가져야 하는지, 어떤 body를 기대하는지, 어떤 응답을 반환해야 하는지 등입니다. + +## 콜백이 있는 앱 { #an-app-with-callbacks } + +예시로 확인해 보겠습니다. + +청구서를 생성할 수 있는 앱을 개발한다고 가정해 보세요. + +이 청구서는 `id`, `title`(선택 사항), `customer`, `total`을 갖습니다. + +당신의 API 사용자(외부 개발자)는 POST 요청으로 당신의 API에서 청구서를 생성합니다. + +그 다음 당신의 API는(가정해 보면): + +* 청구서를 외부 개발자의 고객에게 전송합니다. +* 돈을 수금합니다. +* API 사용자(외부 개발자)의 API로 다시 알림을 보냅니다. + * 이는 (당신의 API에서) 그 외부 개발자가 제공하는 어떤 *external API*로 POST 요청을 보내는 방식으로 수행됩니다(이것이 "callback"입니다). + +## 일반적인 **FastAPI** 앱 { #the-normal-fastapi-app } + +먼저 콜백을 추가하기 전, 일반적인 API 앱이 어떻게 생겼는지 보겠습니다. + +`Invoice` body를 받는 *경로 처리*와, 콜백을 위한 URL을 담는 쿼리 파라미터 `callback_url`이 있을 것입니다. + +이 부분은 꽤 일반적이며, 대부분의 코드는 이미 익숙할 것입니다: + +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} + +/// tip | 팁 + +`callback_url` 쿼리 파라미터는 Pydantic의 <a href="https://docs.pydantic.dev/latest/api/networks/" class="external-link" target="_blank">Url</a> 타입을 사용합니다. + +/// + +유일하게 새로운 것은 *경로 처리 데코레이터*의 인자로 `callbacks=invoices_callback_router.routes`가 들어간다는 점입니다. 이것이 무엇인지 다음에서 보겠습니다. + +## 콜백 문서화하기 { #documenting-the-callback } + +실제 콜백 코드는 당신의 API 앱에 크게 의존합니다. + +그리고 앱마다 많이 달라질 수 있습니다. + +다음처럼 한두 줄의 코드일 수도 있습니다: + +```Python +callback_url = "https://example.com/api/v1/invoices/events/" +httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) +``` + +하지만 콜백에서 가장 중요한 부분은, 당신의 API 사용자(외부 개발자)가 콜백 요청 body로 *당신의 API*가 보낼 데이터 등에 맞춰 *external API*를 올바르게 구현하도록 보장하는 것입니다. + +그래서 다음으로 할 일은, *당신의 API*에서 보내는 콜백을 받기 위해 그 *external API*가 어떤 형태여야 하는지 문서화하는 코드를 추가하는 것입니다. + +그 문서는 당신의 API에서 `/docs`의 Swagger UI에 표시되며, 외부 개발자들이 *external API*를 어떻게 만들어야 하는지 알 수 있게 해줍니다. + +이 예시는 콜백 자체(한 줄 코드로도 될 수 있음)를 구현하지 않고, 문서화 부분만 구현합니다. + +/// tip | 팁 + +실제 콜백은 단지 HTTP 요청입니다. + +콜백을 직접 구현할 때는 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>나 <a href="https://requests.readthedocs.io/" class="external-link" target="_blank">Requests</a> 같은 것을 사용할 수 있습니다. + +/// + +## 콜백 문서화 코드 작성하기 { #write-the-callback-documentation-code } + +이 코드는 앱에서 실행되지 않습니다. 그 *external API*가 어떤 형태여야 하는지 *문서화*하는 데만 필요합니다. + +하지만 **FastAPI**로 API의 자동 문서를 쉽게 생성하는 방법은 이미 알고 있습니다. + +따라서 그와 같은 지식을 사용해 *external API*가 어떻게 생겨야 하는지 문서화할 것입니다... 즉 외부 API가 구현해야 하는 *경로 처리(들)*(당신의 API가 호출할 것들)을 만들어서 말입니다. + +/// tip | 팁 + +콜백을 문서화하는 코드를 작성할 때는, 자신이 그 *외부 개발자*라고 상상하는 것이 유용할 수 있습니다. 그리고 지금은 *당신의 API*가 아니라 *external API*를 구현하고 있다고 생각해 보세요. + +이 관점(외부 개발자의 관점)을 잠시 채택하면, 그 *external API*를 위해 파라미터, body용 Pydantic 모델, 응답 등을 어디에 두어야 하는지가 더 명확하게 느껴질 수 있습니다. + +/// + +### 콜백 `APIRouter` 생성하기 { #create-a-callback-apirouter } + +먼저 하나 이상의 콜백을 담을 새 `APIRouter`를 만듭니다. + +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *} + +### 콜백 *경로 처리* 생성하기 { #create-the-callback-path-operation } + +콜백 *경로 처리*를 만들려면 위에서 만든 동일한 `APIRouter`를 사용합니다. + +일반적인 FastAPI *경로 처리*처럼 보일 것입니다: + +* 아마도 받아야 할 body 선언이 있을 것입니다(예: `body: InvoiceEvent`). +* 그리고 반환해야 할 응답 선언도 있을 수 있습니다(예: `response_model=InvoiceEventReceived`). + +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *} + +일반적인 *경로 처리*와의 주요 차이점은 2가지입니다: + +* 실제 코드를 가질 필요가 없습니다. 당신의 앱은 이 코드를 절대 호출하지 않기 때문입니다. 이는 *external API*를 문서화하는 데만 사용됩니다. 따라서 함수는 그냥 `pass`만 있어도 됩니다. +* *path*에는 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#key-expression" class="external-link" target="_blank">OpenAPI 3 expression</a>(자세한 내용은 아래 참고)이 포함될 수 있으며, 이를 통해 *당신의 API*로 보내진 원래 요청의 파라미터와 일부 값을 변수로 사용할 수 있습니다. + +### 콜백 경로 표현식 { #the-callback-path-expression } + +콜백 *path*는 *당신의 API*로 보내진 원래 요청의 일부를 포함할 수 있는 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#key-expression" class="external-link" target="_blank">OpenAPI 3 expression</a>을 가질 수 있습니다. + +이 경우, 다음 `str`입니다: + +```Python +"{$callback_url}/invoices/{$request.body.id}" +``` + +따라서 당신의 API 사용자(외부 개발자)가 *당신의 API*로 다음 요청을 보내고: + +``` +https://yourapi.com/invoices/?callback_url=https://www.external.org/events +``` + +JSON body가 다음과 같다면: + +```JSON +{ + "id": "2expen51ve", + "customer": "Mr. Richie Rich", + "total": "9999" +} +``` + +그러면 *당신의 API*는 청구서를 처리하고, 나중에 어느 시점에서 `callback_url`(즉 *external API*)로 콜백 요청을 보냅니다: + +``` +https://www.external.org/events/invoices/2expen51ve +``` + +그리고 다음과 같은 JSON body를 포함할 것입니다: + +```JSON +{ + "description": "Payment celebration", + "paid": true +} +``` + +또한 그 *external API*로부터 다음과 같은 JSON body 응답을 기대합니다: + +```JSON +{ + "ok": true +} +``` + +/// tip | 팁 + +콜백 URL에는 `callback_url` 쿼리 파라미터로 받은 URL(`https://www.external.org/events`)뿐 아니라, JSON body 안의 청구서 `id`(`2expen51ve`)도 함께 사용된다는 점에 주목하세요. + +/// + +### 콜백 라우터 추가하기 { #add-the-callback-router } + +이 시점에서, 위에서 만든 콜백 라우터 안에 *콜백 경로 처리(들)*(즉 *external developer*가 *external API*에 구현해야 하는 것들)을 준비했습니다. + +이제 *당신의 API 경로 처리 데코레이터*에서 `callbacks` 파라미터를 사용해, 그 콜백 라우터의 `.routes` 속성(실제로는 routes/*경로 처리*의 `list`)을 전달합니다: + +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} + +/// tip | 팁 + +`callback=`에 라우터 자체(`invoices_callback_router`)를 넘기는 것이 아니라, `invoices_callback_router.routes`처럼 `.routes` 속성을 넘긴다는 점에 주목하세요. + +/// + +### 문서 확인하기 { #check-the-docs } + +이제 앱을 실행하고 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>로 이동하세요. + +*경로 처리*에 대해 "Callbacks" 섹션을 포함한 문서가 표시되며, *external API*가 어떤 형태여야 하는지 확인할 수 있습니다: + +<img src="/img/tutorial/openapi-callbacks/image01.png"> diff --git a/docs/ko/docs/advanced/openapi-webhooks.md b/docs/ko/docs/advanced/openapi-webhooks.md new file mode 100644 index 000000000..89cacf7b7 --- /dev/null +++ b/docs/ko/docs/advanced/openapi-webhooks.md @@ -0,0 +1,55 @@ +# OpenAPI Webhooks { #openapi-webhooks } + +앱이 어떤 데이터와 함께 (요청을 보내서) *사용자의* 앱을 호출할 수 있고, 보통 어떤 **이벤트**를 **알리기** 위해 그렇게 할 수 있다는 것을 API **사용자**에게 알려야 하는 경우가 있습니다. + +이는 사용자가 여러분의 API로 요청을 보내는 일반적인 과정 대신, **여러분의 API**(또는 앱)가 **사용자의 시스템**(사용자의 API, 사용자의 앱)으로 **요청을 보낼 수 있다**는 의미입니다. + +이를 보통 **webhook**이라고 합니다. + +## Webhooks 단계 { #webhooks-steps } + +일반적인 과정은, 여러분이 코드에서 보낼 메시지, 즉 **요청 본문(body)**이 무엇인지 **정의**하는 것입니다. + +또한 여러분의 앱이 어떤 **시점**에 그 요청(또는 이벤트)을 보낼지도 어떤 방식으로든 정의합니다. + +그리고 **사용자**는 (예: 어딘가의 웹 대시보드에서) 여러분의 앱이 그 요청을 보내야 할 **URL**을 어떤 방식으로든 정의합니다. + +webhook의 URL을 등록하는 방법과 실제로 그 요청을 보내는 코드에 대한 모든 **로직**은 여러분에게 달려 있습니다. **여러분의 코드**에서 원하는 방식으로 작성하면 됩니다. + +## **FastAPI**와 OpenAPI로 webhooks 문서화하기 { #documenting-webhooks-with-fastapi-and-openapi } + +**FastAPI**에서는 OpenAPI를 사용해, 이러한 webhook의 이름, 여러분의 앱이 보낼 수 있는 HTTP 작업 타입(예: `POST`, `PUT` 등), 그리고 여러분의 앱이 보낼 요청 **본문(body)**을 정의할 수 있습니다. + +이렇게 하면 사용자가 여러분의 **webhook** 요청을 받기 위해 **자신들의 API를 구현**하기가 훨씬 쉬워지고, 경우에 따라서는 자신의 API 코드 일부를 자동 생성할 수도 있습니다. + +/// info | 정보 + +Webhooks는 OpenAPI 3.1.0 이상에서 사용할 수 있으며, FastAPI `0.99.0` 이상에서 지원됩니다. + +/// + +## webhooks가 있는 앱 { #an-app-with-webhooks } + +**FastAPI** 애플리케이션을 만들면, *경로 처리*를 정의하는 것과 같은 방식으로(예: `@app.webhooks.post()`), *webhooks*를 정의하는 데 사용할 수 있는 `webhooks` 속성이 있습니다. + +{* ../../docs_src/openapi_webhooks/tutorial001_py39.py hl[9:13,36:53] *} + +여러분이 정의한 webhook은 **OpenAPI** 스키마와 자동 **docs UI**에 포함됩니다. + +/// info | 정보 + +`app.webhooks` 객체는 실제로 `APIRouter`일 뿐이며, 여러 파일로 앱을 구조화할 때 사용하는 것과 동일한 타입입니다. + +/// + +webhook에서는 실제로(`/items/` 같은) *경로(path)*를 선언하지 않는다는 점에 유의하세요. 그곳에 전달하는 텍스트는 webhook의 **식별자**(이벤트 이름)일 뿐입니다. 예를 들어 `@app.webhooks.post("new-subscription")`에서 webhook 이름은 `new-subscription`입니다. + +이는 **사용자**가 webhook 요청을 받고 싶은 실제 **URL 경로**를 다른 방식(예: 웹 대시보드)으로 정의할 것이라고 기대하기 때문입니다. + +### 문서 확인하기 { #check-the-docs } + +이제 앱을 실행하고 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>로 이동하세요. + +문서에 일반적인 *경로 처리*가 보이고, 이제는 일부 **webhooks**도 함께 보일 것입니다: + +<img src="/img/tutorial/openapi-webhooks/image01.png"> diff --git a/docs/ko/docs/advanced/path-operation-advanced-configuration.md b/docs/ko/docs/advanced/path-operation-advanced-configuration.md new file mode 100644 index 000000000..f20fa6d26 --- /dev/null +++ b/docs/ko/docs/advanced/path-operation-advanced-configuration.md @@ -0,0 +1,172 @@ +# 경로 처리 고급 구성 { #path-operation-advanced-configuration } + +## OpenAPI operationId { #openapi-operationid } + +/// warning | 경고 + +OpenAPI “전문가”가 아니라면, 아마 이 내용은 필요하지 않을 것입니다. + +/// + +매개변수 `operation_id`를 사용해 *경로 처리*에 사용할 OpenAPI `operationId`를 설정할 수 있습니다. + +각 작업마다 고유하도록 보장해야 합니다. + +{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} + +### *경로 처리 함수* 이름을 operationId로 사용하기 { #using-the-path-operation-function-name-as-the-operationid } + +API의 함수 이름을 `operationId`로 사용하고 싶다면, 모든 API를 순회하면서 `APIRoute.name`을 사용해 각 *경로 처리*의 `operation_id`를 덮어쓸 수 있습니다. + +모든 *경로 처리*를 추가한 뒤에 수행해야 합니다. + +{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *} + +/// tip | 팁 + +`app.openapi()`를 수동으로 호출한다면, 그 전에 `operationId`들을 업데이트해야 합니다. + +/// + +/// warning | 경고 + +이렇게 할 경우, 각 *경로 처리 함수*의 이름이 고유하도록 보장해야 합니다. + +서로 다른 모듈(파이썬 파일)에 있어도 마찬가지입니다. + +/// + +## OpenAPI에서 제외하기 { #exclude-from-openapi } + +생성된 OpenAPI 스키마(따라서 자동 문서화 시스템)에서 특정 *경로 처리*를 제외하려면, `include_in_schema` 매개변수를 `False`로 설정하세요: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *} + +## docstring에서 고급 설명 가져오기 { #advanced-description-from-docstring } + +OpenAPI에 사용할 *경로 처리 함수*의 docstring 줄 수를 제한할 수 있습니다. + +`\f`(이스케이프된 "form feed" 문자)를 추가하면 **FastAPI**는 이 지점에서 OpenAPI에 사용할 출력 내용을 잘라냅니다. + +문서에는 표시되지 않지만, Sphinx 같은 다른 도구는 나머지 부분을 사용할 수 있습니다. + +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} + +## 추가 응답 { #additional-responses } + +*경로 처리*에 대해 `response_model`과 `status_code`를 선언하는 방법을 이미 보셨을 것입니다. + +이는 *경로 처리*의 기본 응답에 대한 메타데이터를 정의합니다. + +모델, 상태 코드 등과 함께 추가 응답도 선언할 수 있습니다. + +이에 대한 문서의 전체 장이 있으니, [OpenAPI의 추가 응답](additional-responses.md){.internal-link target=_blank}에서 읽어보세요. + +## OpenAPI Extra { #openapi-extra } + +애플리케이션에서 *경로 처리*를 선언하면, **FastAPI**는 OpenAPI 스키마에 포함될 해당 *경로 처리*의 관련 메타데이터를 자동으로 생성합니다. + +/// note | 기술 세부사항 + +OpenAPI 명세에서는 이를 <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object" class="external-link" target="_blank">Operation Object</a>라고 부릅니다. + +/// + +여기에는 *경로 처리*에 대한 모든 정보가 있으며, 자동 문서를 생성하는 데 사용됩니다. + +`tags`, `parameters`, `requestBody`, `responses` 등이 포함됩니다. + +이 *경로 처리* 전용 OpenAPI 스키마는 보통 **FastAPI**가 자동으로 생성하지만, 확장할 수도 있습니다. + +/// tip | 팁 + +이는 저수준 확장 지점입니다. + +추가 응답만 선언하면 된다면, 더 편리한 방법은 [OpenAPI의 추가 응답](additional-responses.md){.internal-link target=_blank}을 사용하는 것입니다. + +/// + +`openapi_extra` 매개변수를 사용해 *경로 처리*의 OpenAPI 스키마를 확장할 수 있습니다. + +### OpenAPI 확장 { #openapi-extensions } + +예를 들어 `openapi_extra`는 [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions)를 선언하는 데 도움이 될 수 있습니다: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *} + +자동 API 문서를 열면, 해당 특정 *경로 처리*의 하단에 확장이 표시됩니다. + +<img src="/img/tutorial/path-operation-advanced-configuration/image01.png"> + +또한 API의 `/openapi.json`에서 결과 OpenAPI를 보면, 특정 *경로 처리*의 일부로 확장이 포함된 것도 확인할 수 있습니다: + +```JSON hl_lines="22" +{ + "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": {} + } + } + } + }, + "x-aperture-labs-portal": "blue" + } + } + } +} +``` + +### 사용자 정의 OpenAPI *경로 처리* 스키마 { #custom-openapi-path-operation-schema } + +`openapi_extra`의 딕셔너리는 *경로 처리*에 대해 자동으로 생성된 OpenAPI 스키마와 깊게 병합됩니다. + +따라서 자동 생성된 스키마에 추가 데이터를 더할 수 있습니다. + +예를 들어 Pydantic과 함께 FastAPI의 자동 기능을 사용하지 않고, 자체 코드로 요청을 읽고 검증하기로 결정할 수도 있지만, OpenAPI 스키마에는 여전히 그 요청을 정의하고 싶을 수 있습니다. + +그럴 때 `openapi_extra`를 사용할 수 있습니다: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} + +이 예시에서는 어떤 Pydantic 모델도 선언하지 않았습니다. 사실 요청 바디는 JSON으로 <abbr title="converted from some plain format, like bytes, into Python objects - bytes 같은 일반 형식에서 Python 객체로 변환">parsed</abbr>되지도 않고, `bytes`로 직접 읽습니다. 그리고 함수 `magic_data_reader()`가 어떤 방식으로든 이를 파싱하는 역할을 담당합니다. + +그럼에도 불구하고, 요청 바디에 대해 기대하는 스키마를 선언할 수 있습니다. + +### 사용자 정의 OpenAPI 콘텐츠 타입 { #custom-openapi-content-type } + +같은 트릭을 사용하면, Pydantic 모델을 이용해 JSON Schema를 정의하고 이를 *경로 처리*의 사용자 정의 OpenAPI 스키마 섹션에 포함시킬 수 있습니다. + +요청의 데이터 타입이 JSON이 아니더라도 이렇게 할 수 있습니다. + +예를 들어 이 애플리케이션에서는 Pydantic 모델에서 JSON Schema를 추출하는 FastAPI의 통합 기능도, JSON에 대한 자동 검증도 사용하지 않습니다. 실제로 요청 콘텐츠 타입을 JSON이 아니라 YAML로 선언합니다: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} + +그럼에도 기본 통합 기능을 사용하지 않더라도, YAML로 받고자 하는 데이터에 대한 JSON Schema를 수동으로 생성하기 위해 Pydantic 모델을 여전히 사용합니다. + +그 다음 요청을 직접 사용하고, 바디를 `bytes`로 추출합니다. 이는 FastAPI가 요청 페이로드를 JSON으로 파싱하려고 시도조차 하지 않는다는 뜻입니다. + +그리고 코드에서 YAML 콘텐츠를 직접 파싱한 뒤, 다시 같은 Pydantic 모델을 사용해 YAML 콘텐츠를 검증합니다: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} + +/// tip | 팁 + +여기서는 같은 Pydantic 모델을 재사용합니다. + +하지만 마찬가지로, 다른 방식으로 검증할 수도 있습니다. + +/// diff --git a/docs/ko/docs/advanced/security/http-basic-auth.md b/docs/ko/docs/advanced/security/http-basic-auth.md new file mode 100644 index 000000000..611aad795 --- /dev/null +++ b/docs/ko/docs/advanced/security/http-basic-auth.md @@ -0,0 +1,107 @@ +# HTTP Basic Auth { #http-basic-auth } + +가장 단순한 경우에는 HTTP Basic Auth를 사용할 수 있습니다. + +HTTP Basic Auth에서는 애플리케이션이 사용자명과 비밀번호가 들어 있는 헤더를 기대합니다. + +이를 받지 못하면 HTTP 401 "Unauthorized" 오류를 반환합니다. + +그리고 값이 `Basic`이고 선택적으로 `realm` 파라미터를 포함하는 `WWW-Authenticate` 헤더를 반환합니다. + +이는 브라우저가 사용자명과 비밀번호를 입력하는 통합 프롬프트를 표시하도록 알려줍니다. + +그다음 사용자명과 비밀번호를 입력하면, 브라우저가 자동으로 해당 값을 헤더에 담아 전송합니다. + +## 간단한 HTTP Basic Auth { #simple-http-basic-auth } + +* `HTTPBasic`과 `HTTPBasicCredentials`를 임포트합니다. +* `HTTPBasic`을 사용해 "`security` scheme"을 생성합니다. +* *경로 처리*에서 dependency로 해당 `security`를 사용합니다. +* `HTTPBasicCredentials` 타입의 객체를 반환합니다: + * 전송된 `username`과 `password`를 포함합니다. + +{* ../../docs_src/security/tutorial006_an_py39.py hl[4,8,12] *} + +처음으로 URL을 열어보면(또는 문서에서 "Execute" 버튼을 클릭하면) 브라우저가 사용자명과 비밀번호를 물어봅니다: + +<img src="/img/tutorial/security/image12.png"> + +## 사용자명 확인하기 { #check-the-username } + +더 완전한 예시입니다. + +dependency를 사용해 사용자명과 비밀번호가 올바른지 확인하세요. + +이를 위해 Python 표준 모듈 <a href="https://docs.python.org/3/library/secrets.html" class="external-link" target="_blank">`secrets`</a>를 사용해 사용자명과 비밀번호를 확인합니다. + +`secrets.compare_digest()`는 `bytes` 또는 ASCII 문자(영어에서 사용하는 문자)만 포함한 `str`을 받아야 합니다. 즉, `Sebastián`의 `á` 같은 문자가 있으면 동작하지 않습니다. + +이를 처리하기 위해 먼저 `username`과 `password`를 UTF-8로 인코딩해서 `bytes`로 변환합니다. + +그런 다음 `secrets.compare_digest()`를 사용해 `credentials.username`이 `"stanleyjobson"`이고 `credentials.password`가 `"swordfish"`인지 확실히 확인할 수 있습니다. + +{* ../../docs_src/security/tutorial007_an_py39.py hl[1,12:24] *} + +이는 다음과 비슷합니다: + +```Python +if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): + # Return some error + ... +``` + +하지만 `secrets.compare_digest()`를 사용하면 "timing attacks"라고 불리는 한 유형의 공격에 대해 안전해집니다. + +### Timing Attacks { #timing-attacks } + +그렇다면 "timing attack"이란 무엇일까요? + +공격자들이 사용자명과 비밀번호를 추측하려고 한다고 가정해봅시다. + +그리고 사용자명 `johndoe`, 비밀번호 `love123`으로 요청을 보냅니다. + +그러면 애플리케이션의 Python 코드는 대략 다음과 같을 것입니다: + +```Python +if "johndoe" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +하지만 Python이 `johndoe`의 첫 글자 `j`를 `stanleyjobson`의 첫 글자 `s`와 비교하는 순간, 두 문자열이 같지 않다는 것을 이미 알게 되어 `False`를 반환합니다. 이는 “나머지 글자들을 비교하느라 계산을 더 낭비할 필요가 없다”고 판단하기 때문입니다. 그리고 애플리케이션은 "Incorrect username or password"라고 말합니다. + +그런데 공격자들이 사용자명을 `stanleyjobsox`, 비밀번호를 `love123`으로 다시 시도합니다. + +그러면 애플리케이션 코드는 다음과 비슷하게 동작합니다: + +```Python +if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +Python은 두 문자열이 같지 않다는 것을 알아차리기 전까지 `stanleyjobsox`와 `stanleyjobson` 양쪽의 `stanleyjobso` 전체를 비교해야 합니다. 그래서 "Incorrect username or password"라고 응답하기까지 추가로 몇 마이크로초가 더 걸릴 것입니다. + +#### 응답 시간은 공격자에게 도움이 됩니다 { #the-time-to-answer-helps-the-attackers } + +이 시점에서 서버가 "Incorrect username or password" 응답을 보내는 데 몇 마이크로초 더 걸렸다는 것을 알아채면, 공격자들은 _무언가_ 맞았다는 것(초기 몇 글자가 맞았다는 것)을 알게 됩니다. + +그리고 `johndoe`보다는 `stanleyjobsox`에 더 가까운 값을 시도해야 한다는 것을 알고 다시 시도할 수 있습니다. + +#### "전문적인" 공격 { #a-professional-attack } + +물론 공격자들은 이런 작업을 손으로 하지 않습니다. 보통 초당 수천~수백만 번 테스트할 수 있는 프로그램을 작성할 것이고, 한 번에 정답 글자 하나씩 추가로 얻어낼 수 있습니다. + +그렇게 하면 몇 분 또는 몇 시간 만에, 응답에 걸린 시간만을 이용해(우리 애플리케이션의 “도움”을 받아) 올바른 사용자명과 비밀번호를 추측할 수 있게 됩니다. + +#### `secrets.compare_digest()`로 해결하기 { #fix-it-with-secrets-compare-digest } + +하지만 우리 코드는 실제로 `secrets.compare_digest()`를 사용하고 있습니다. + +요약하면, `stanleyjobsox`와 `stanleyjobson`을 비교하는 데 걸리는 시간은 `johndoe`와 `stanleyjobson`을 비교하는 데 걸리는 시간과 같아집니다. 비밀번호도 마찬가지입니다. + +이렇게 애플리케이션 코드에서 `secrets.compare_digest()`를 사용하면, 이러한 범위의 보안 공격 전반에 대해 안전해집니다. + +### 오류 반환하기 { #return-the-error } + +자격 증명이 올바르지 않다고 판단되면, 상태 코드 401(자격 증명이 제공되지 않았을 때와 동일)을 사용하는 `HTTPException`을 반환하고 브라우저가 로그인 프롬프트를 다시 표시하도록 `WWW-Authenticate` 헤더를 추가하세요: + +{* ../../docs_src/security/tutorial007_an_py39.py hl[26:30] *} diff --git a/docs/ko/docs/advanced/security/index.md b/docs/ko/docs/advanced/security/index.md new file mode 100644 index 000000000..4c7abfacc --- /dev/null +++ b/docs/ko/docs/advanced/security/index.md @@ -0,0 +1,19 @@ +# 고급 보안 { #advanced-security } + +## 추가 기능 { #additional-features } + +[튜토리얼 - 사용자 가이드: 보안](../../tutorial/security/index.md){.internal-link target=_blank}에서 다룬 내용 외에도, 보안을 처리하기 위한 몇 가지 추가 기능이 있습니다. + +/// tip | 팁 + +다음 섹션들은 **반드시 "고급"이라고 할 수는 없습니다**. + +그리고 사용 사례에 따라, 그중 하나에 해결책이 있을 수도 있습니다. + +/// + +## 먼저 튜토리얼 읽기 { #read-the-tutorial-first } + +다음 섹션은 주요 [튜토리얼 - 사용자 가이드: 보안](../../tutorial/security/index.md){.internal-link target=_blank}을 이미 읽었다고 가정합니다. + +모두 동일한 개념을 기반으로 하지만, 몇 가지 추가 기능을 사용할 수 있게 해줍니다. diff --git a/docs/ko/docs/advanced/security/oauth2-scopes.md b/docs/ko/docs/advanced/security/oauth2-scopes.md new file mode 100644 index 000000000..0f90f92ae --- /dev/null +++ b/docs/ko/docs/advanced/security/oauth2-scopes.md @@ -0,0 +1,274 @@ +# OAuth2 스코프 { #oauth2-scopes } + +**FastAPI**에서 OAuth2 스코프를 직접 사용할 수 있으며, 자연스럽게 동작하도록 통합되어 있습니다. + +이를 통해 OAuth2 표준을 따르는 더 세밀한 권한 시스템을 OpenAPI 애플리케이션(및 API 문서)에 통합할 수 있습니다. + +스코프를 사용하는 OAuth2는 Facebook, Google, GitHub, Microsoft, X(Twitter) 등 많은 대형 인증 제공자가 사용하는 메커니즘입니다. 이들은 이를 통해 사용자와 애플리케이션에 특정 권한을 제공합니다. + +Facebook, Google, GitHub, Microsoft, X(Twitter)로 “로그인”할 때마다, 해당 애플리케이션은 스코프가 있는 OAuth2를 사용하고 있습니다. + +이 섹션에서는 **FastAPI** 애플리케이션에서 동일한 “스코프가 있는 OAuth2”로 인증(Authentication)과 인가(Authorization)를 관리하는 방법을 확인합니다. + +/// warning | 경고 + +이 섹션은 다소 고급 내용입니다. 이제 막 시작했다면 건너뛰어도 됩니다. + +OAuth2 스코프가 반드시 필요한 것은 아니며, 인증과 인가는 원하는 방식으로 처리할 수 있습니다. + +하지만 스코프가 있는 OAuth2는 (OpenAPI와 함께) API 및 API 문서에 깔끔하게 통합될 수 있습니다. + +그럼에도 불구하고, 해당 스코프(또는 그 밖의 어떤 보안/인가 요구사항이든)는 코드에서 필요에 맞게 직접 강제해야 합니다. + +많은 경우 스코프가 있는 OAuth2는 과한 선택일 수 있습니다. + +하지만 필요하다고 알고 있거나 궁금하다면 계속 읽어보세요. + +/// + +## OAuth2 스코프와 OpenAPI { #oauth2-scopes-and-openapi } + +OAuth2 사양은 “스코프(scopes)”를 공백으로 구분된 문자열 목록으로 정의합니다. + +각 문자열의 내용은 어떤 형식이든 될 수 있지만, 공백을 포함하면 안 됩니다. + +이 스코프들은 “권한”을 나타냅니다. + +OpenAPI(예: API 문서)에서는 “security schemes”를 정의할 수 있습니다. + +이 security scheme 중 하나가 OAuth2를 사용한다면, 스코프도 선언하고 사용할 수 있습니다. + +각 “스코프”는 (공백 없는) 문자열일 뿐입니다. + +보통 다음과 같이 특정 보안 권한을 선언하는 데 사용합니다: + +* `users:read` 또는 `users:write` 는 흔한 예시입니다. +* `instagram_basic` 는 Facebook/Instagram에서 사용합니다. +* `https://www.googleapis.com/auth/drive` 는 Google에서 사용합니다. + +/// info | 정보 + +OAuth2에서 “스코프”는 필요한 특정 권한을 선언하는 문자열일 뿐입니다. + +`:` 같은 다른 문자가 있거나 URL이어도 상관없습니다. + +그런 세부사항은 구현에 따라 달라집니다. + +OAuth2 입장에서는 그저 문자열입니다. + +/// + +## 전체 개요 { #global-view } + +먼저, 메인 **튜토리얼 - 사용자 가이드**의 [비밀번호(및 해싱)를 사용하는 OAuth2, JWT 토큰을 사용하는 Bearer](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank} 예제에서 어떤 부분이 바뀌는지 빠르게 살펴보겠습니다. 이제 OAuth2 스코프를 사용합니다: + +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:126,130:136,141,157] *} + +이제 변경 사항을 단계별로 살펴보겠습니다. + +## OAuth2 보안 스킴 { #oauth2-security-scheme } + +첫 번째 변경 사항은 이제 사용 가능한 스코프 2개(`me`, `items`)로 OAuth2 보안 스킴을 선언한다는 점입니다. + +`scopes` 매개변수는 각 스코프를 키로 하고, 설명을 값으로 하는 `dict`를 받습니다: + +{* ../../docs_src/security/tutorial005_an_py310.py hl[63:66] *} + +이제 스코프를 선언했기 때문에, 로그인/인가할 때 API 문서에 스코프가 표시됩니다. + +그리고 접근을 허용할 스코프를 선택할 수 있게 됩니다: `me`와 `items`. + +이는 Facebook, Google, GitHub 등으로 로그인하면서 권한을 부여할 때 사용되는 것과 동일한 메커니즘입니다: + +<img src="/img/tutorial/security/image11.png"> + +## 스코프를 포함한 JWT 토큰 { #jwt-token-with-scopes } + +이제 토큰 *경로 처리*를 수정해, 요청된 스코프를 반환하도록 합니다. + +여전히 동일한 `OAuth2PasswordRequestForm`을 사용합니다. 여기에는 요청에서 받은 각 스코프를 담는 `scopes` 속성이 있으며, 타입은 `str`의 `list`입니다. + +그리고 JWT 토큰의 일부로 스코프를 반환합니다. + +/// danger | 위험 + +단순화를 위해, 여기서는 요청으로 받은 스코프를 그대로 토큰에 추가하고 있습니다. + +하지만 실제 애플리케이션에서는 보안을 위해, 사용자가 실제로 가질 수 있는 스코프만(또는 미리 정의한 것만) 추가하도록 반드시 확인해야 합니다. + +/// + +{* ../../docs_src/security/tutorial005_an_py310.py hl[157] *} + +## *경로 처리*와 의존성에서 스코프 선언하기 { #declare-scopes-in-path-operations-and-dependencies } + +이제 `/users/me/items/`에 대한 *경로 처리*가 스코프 `items`를 요구한다고 선언합니다. + +이를 위해 `fastapi`에서 `Security`를 import하여 사용합니다. + +`Security`는 (`Depends`처럼) 의존성을 선언하는 데 사용할 수 있지만, `Security`는 스코프(문자열) 목록을 받는 `scopes` 매개변수도 받습니다. + +이 경우, 의존성 함수 `get_current_active_user`를 `Security`에 전달합니다(`Depends`로 할 때와 같은 방식). + +하지만 스코프 `list`도 함께 전달합니다. 여기서는 스코프 하나만: `items`(더 많을 수도 있습니다). + +또한 의존성 함수 `get_current_active_user`는 `Depends`뿐 아니라 `Security`로도 하위 의존성을 선언할 수 있습니다. 자체 하위 의존성 함수(`get_current_user`)와 추가 스코프 요구사항을 선언합니다. + +이 경우에는 스코프 `me`를 요구합니다(여러 스코프를 요구할 수도 있습니다). + +/// note | 참고 + +반드시 서로 다른 곳에 서로 다른 스코프를 추가해야 하는 것은 아닙니다. + +여기서는 **FastAPI**가 서로 다른 레벨에서 선언된 스코프를 어떻게 처리하는지 보여주기 위해 이렇게 합니다. + +/// + +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,172] *} + +/// info | 기술 세부사항 + +`Security`는 실제로 `Depends`의 서브클래스이며, 나중에 보게 될 추가 매개변수 하나만 더 있습니다. + +하지만 `Depends` 대신 `Security`를 사용하면, **FastAPI**는 보안 스코프를 선언할 수 있음을 알고 내부적으로 이를 사용하며, OpenAPI로 API를 문서화할 수 있습니다. + +하지만 `fastapi`에서 `Query`, `Path`, `Depends`, `Security` 등을 import할 때, 이것들은 실제로 특수한 클래스를 반환하는 함수입니다. + +/// + +## `SecurityScopes` 사용하기 { #use-securityscopes } + +이제 의존성 `get_current_user`를 업데이트합니다. + +이는 위의 의존성들이 사용하는 것입니다. + +여기에서 앞서 만든 동일한 OAuth2 스킴을 의존성으로 선언하여 사용합니다: `oauth2_scheme`. + +이 의존성 함수 자체에는 스코프 요구사항이 없기 때문에, `oauth2_scheme`와 함께 `Depends`를 사용할 수 있습니다. 보안 스코프를 지정할 필요가 없을 때는 `Security`를 쓸 필요가 없습니다. + +또한 `fastapi.security`에서 import한 `SecurityScopes` 타입의 특별한 매개변수를 선언합니다. + +이 `SecurityScopes` 클래스는 `Request`와 비슷합니다(`Request`는 요청 객체를 직접 얻기 위해 사용했습니다). + +{* ../../docs_src/security/tutorial005_an_py310.py hl[9,106] *} + +## `scopes` 사용하기 { #use-the-scopes } + +매개변수 `security_scopes`의 타입은 `SecurityScopes`입니다. + +여기에는 `scopes` 속성이 있으며, 자기 자신과 이 함수를 하위 의존성으로 사용하는 모든 의존성이 요구하는 스코프 전체를 담은 `list`를 가집니다. 즉, 모든 “dependants”... 다소 헷갈릴 수 있는데, 아래에서 다시 설명합니다. + +`security_scopes` 객체(`SecurityScopes` 클래스)에는 또한 `scope_str` 속성이 있는데, 공백으로 구분된 단일 문자열로 스코프들을 담고 있습니다(이를 사용할 것입니다). + +나중에 여러 지점에서 재사용(`raise`)할 수 있는 `HTTPException`을 생성합니다. + +이 예외에는 필요한 스코프(있다면)를 공백으로 구분된 문자열(`scope_str`)로 포함합니다. 그리고 그 스코프 문자열을 `WWW-Authenticate` 헤더에 넣습니다(이는 사양의 일부입니다). + +{* ../../docs_src/security/tutorial005_an_py310.py hl[106,108:116] *} + +## `username`과 데이터 형태 검증하기 { #verify-the-username-and-data-shape } + +`username`을 얻었는지 확인하고, 스코프를 추출합니다. + +그런 다음 Pydantic 모델로 데이터를 검증합니다(`ValidationError` 예외를 잡습니다). JWT 토큰을 읽거나 Pydantic으로 데이터를 검증하는 과정에서 오류가 나면, 앞에서 만든 `HTTPException`을 raise합니다. + +이를 위해 Pydantic 모델 `TokenData`에 새 속성 `scopes`를 추가합니다. + +Pydantic으로 데이터를 검증하면, 예를 들어 스코프가 정확히 `str`의 `list`이고 `username`이 `str`인지 등을 보장할 수 있습니다. + +예를 들어 `dict`나 다른 형태라면, 나중에 애플리케이션이 어느 시점에 깨지면서 보안 위험이 될 수 있습니다. + +또한 해당 username을 가진 사용자가 있는지 확인하고, 없다면 앞에서 만든 동일한 예외를 raise합니다. + +{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:129] *} + +## `scopes` 검증하기 { #verify-the-scopes } + +이제 이 의존성과 모든 dependant( *경로 처리* 포함)가 요구하는 모든 스코프가, 받은 토큰의 스코프에 포함되어 있는지 확인합니다. 그렇지 않으면 `HTTPException`을 raise합니다. + +이를 위해, 모든 스코프를 `str`로 담고 있는 `security_scopes.scopes`를 사용합니다. + +{* ../../docs_src/security/tutorial005_an_py310.py hl[130:136] *} + +## 의존성 트리와 스코프 { #dependency-tree-and-scopes } + +이 의존성 트리와 스코프를 다시 살펴보겠습니다. + +`get_current_active_user` 의존성은 `get_current_user`를 하위 의존성으로 가지므로, `get_current_active_user`에서 선언된 스코프 `"me"`는 `get_current_user`에 전달되는 `security_scopes.scopes`의 요구 스코프 목록에 포함됩니다. + +*경로 처리* 자체도 스코프 `"items"`를 선언하므로, 이것 또한 `get_current_user`에 전달되는 `security_scopes.scopes` 목록에 포함됩니다. + +의존성과 스코프의 계층 구조는 다음과 같습니다: + +* *경로 처리* `read_own_items`는: + * 의존성과 함께 요구 스코프 `["items"]`를 가집니다: + * `get_current_active_user`: + * 의존성 함수 `get_current_active_user`는: + * 의존성과 함께 요구 스코프 `["me"]`를 가집니다: + * `get_current_user`: + * 의존성 함수 `get_current_user`는: + * 자체적으로는 요구 스코프가 없습니다. + * `oauth2_scheme`를 사용하는 의존성이 있습니다. + * `SecurityScopes` 타입의 `security_scopes` 매개변수가 있습니다: + * 이 `security_scopes` 매개변수는 위에서 선언된 모든 스코프를 담은 `list`인 `scopes` 속성을 가지므로: + * *경로 처리* `read_own_items`의 경우 `security_scopes.scopes`에는 `["me", "items"]`가 들어갑니다. + * *경로 처리* `read_users_me`의 경우 `security_scopes.scopes`에는 `["me"]`가 들어갑니다. 이는 의존성 `get_current_active_user`에서 선언되기 때문입니다. + * *경로 처리* `read_system_status`의 경우 `security_scopes.scopes`에는 `[]`(없음)가 들어갑니다. `scopes`가 있는 `Security`를 선언하지 않았고, 그 의존성인 `get_current_user`도 `scopes`를 선언하지 않았기 때문입니다. + +/// tip | 팁 + +여기서 중요한 “마법 같은” 점은 `get_current_user`가 각 *경로 처리*마다 검사해야 하는 `scopes` 목록이 달라진다는 것입니다. + +이는 특정 *경로 처리*에 대한 의존성 트리에서, 각 *경로 처리*와 각 의존성에 선언된 `scopes`에 따라 달라집니다. + +/// + +## `SecurityScopes`에 대한 추가 설명 { #more-details-about-securityscopes } + +`SecurityScopes`는 어느 지점에서든, 그리고 여러 곳에서 사용할 수 있으며, “루트” 의존성에만 있어야 하는 것은 아닙니다. + +`SecurityScopes`는 **해당 특정** *경로 처리*와 **해당 특정** 의존성 트리에 대해, 현재 `Security` 의존성과 모든 dependant에 선언된 보안 스코프를 항상 갖고 있습니다. + +`SecurityScopes`에는 dependant가 선언한 모든 스코프가 포함되므로, 중앙의 의존성 함수에서 토큰이 필요한 스코프를 가지고 있는지 검증한 다음, 서로 다른 *경로 처리*에서 서로 다른 스코프 요구사항을 선언할 수 있습니다. + +이들은 각 *경로 처리*마다 독립적으로 검사됩니다. + +## 확인하기 { #check-it } + +API 문서를 열면, 인증하고 인가할 스코프를 지정할 수 있습니다. + +<img src="/img/tutorial/security/image11.png"> + +어떤 스코프도 선택하지 않으면 “인증”은 되지만, `/users/me/` 또는 `/users/me/items/`에 접근하려고 하면 권한이 충분하지 않다는 오류가 발생합니다. `/status/`에는 여전히 접근할 수 있습니다. + +그리고 스코프 `me`는 선택했지만 스코프 `items`는 선택하지 않았다면, `/users/me/`에는 접근할 수 있지만 `/users/me/items/`에는 접근할 수 없습니다. + +이는 사용자가 애플리케이션에 얼마나 많은 권한을 부여했는지에 따라, 제3자 애플리케이션이 사용자로부터 제공받은 토큰으로 이 *경로 처리*들 중 하나에 접근하려고 할 때 발생하는 상황과 같습니다. + +## 제3자 통합에 대해 { #about-third-party-integrations } + +이 예제에서는 OAuth2 “password” 플로우를 사용하고 있습니다. + +이는 보통 자체 프론트엔드가 있는 우리 애플리케이션에 로그인할 때 적합합니다. + +우리가 이를 통제하므로 `username`과 `password`를 받는 것을 신뢰할 수 있기 때문입니다. + +하지만 다른 사람들이 연결할 OAuth2 애플리케이션(즉, Facebook, Google, GitHub 등과 동등한 인증 제공자를 만들고 있다면)을 구축한다면, 다른 플로우 중 하나를 사용해야 합니다. + +가장 흔한 것은 implicit 플로우입니다. + +가장 안전한 것은 code 플로우이지만, 더 많은 단계가 필요해 구현이 더 복잡합니다. 복잡하기 때문에 많은 제공자는 implicit 플로우를 권장하게 됩니다. + +/// note | 참고 + +인증 제공자마다 자신들의 브랜드의 일부로 만들기 위해, 각 플로우를 서로 다른 방식으로 이름 붙이는 경우가 흔합니다. + +하지만 결국, 동일한 OAuth2 표준을 구현하고 있는 것입니다. + +/// + +**FastAPI**는 `fastapi.security.oauth2`에 이러한 모든 OAuth2 인증 플로우를 위한 유틸리티를 포함합니다. + +## 데코레이터 `dependencies`에서의 `Security` { #security-in-decorator-dependencies } + +[경로 처리 데코레이터의 의존성](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}에서 설명한 것처럼 데코레이터의 `dependencies` 매개변수에 `Depends`의 `list`를 정의할 수 있는 것과 같은 방식으로, 거기에서 `scopes`와 함께 `Security`를 사용할 수도 있습니다. diff --git a/docs/ko/docs/advanced/settings.md b/docs/ko/docs/advanced/settings.md new file mode 100644 index 000000000..6fa7c644c --- /dev/null +++ b/docs/ko/docs/advanced/settings.md @@ -0,0 +1,302 @@ +# 설정과 환경 변수 { #settings-and-environment-variables } + +많은 경우 애플리케이션에는 외부 설정이나 구성(예: secret key, 데이터베이스 자격 증명, 이메일 서비스 자격 증명 등)이 필요할 수 있습니다. + +이러한 설정 대부분은 데이터베이스 URL처럼 변동 가능(변경될 수 있음)합니다. 그리고 많은 설정은 secret처럼 민감할 수 있습니다. + +이 때문에 보통 애플리케이션이 읽어들이는 환경 변수로 이를 제공하는 것이 일반적입니다. + +/// tip | 팁 + +환경 변수를 이해하려면 [환경 변수](../environment-variables.md){.internal-link target=_blank}를 읽어보세요. + +/// + +## 타입과 검증 { #types-and-validation } + +이 환경 변수들은 Python 외부에 있으며 다른 프로그램 및 시스템의 나머지 부분(그리고 Linux, Windows, macOS 같은 서로 다른 운영체제와도)과 호환되어야 하므로, 텍스트 문자열만 다룰 수 있습니다. + +즉, Python에서 환경 변수로부터 읽어온 어떤 값이든 `str`이 되며, 다른 타입으로의 변환이나 검증은 코드에서 수행해야 합니다. + +## Pydantic `Settings` { #pydantic-settings } + +다행히 Pydantic은 <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/" class="external-link" target="_blank">Pydantic: Settings management</a>를 통해 환경 변수에서 오는 이러한 설정을 처리할 수 있는 훌륭한 유틸리티를 제공합니다. + +### `pydantic-settings` 설치하기 { #install-pydantic-settings } + +먼저 [가상 환경](../virtual-environments.md){.internal-link target=_blank}을 만들고 활성화한 다음, `pydantic-settings` 패키지를 설치하세요: + +<div class="termy"> + +```console +$ pip install pydantic-settings +---> 100% +``` + +</div> + +또는 다음처럼 `all` extras를 설치하면 함께 포함됩니다: + +<div class="termy"> + +```console +$ pip install "fastapi[all]" +---> 100% +``` + +</div> + +### `Settings` 객체 만들기 { #create-the-settings-object } + +Pydantic에서 `BaseSettings`를 import하고, Pydantic 모델과 매우 비슷하게 서브클래스를 만드세요. + +Pydantic 모델과 같은 방식으로, 타입 어노테이션(그리고 필요하다면 기본값)과 함께 클래스 속성을 선언합니다. + +다양한 데이터 타입, `Field()`로 추가 검증 등 Pydantic 모델에서 사용하는 동일한 검증 기능과 도구를 모두 사용할 수 있습니다. + +{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} + +/// tip | 팁 + +빠르게 복사/붙여넣기할 예시가 필요하다면, 이 예시는 사용하지 말고 아래의 마지막 예시를 사용하세요. + +/// + +그 다음, 해당 `Settings` 클래스의 인스턴스(여기서는 `settings` 객체)를 생성하면 Pydantic이 대소문자를 구분하지 않고 환경 변수를 읽습니다. 따라서 대문자 변수 `APP_NAME`도 `app_name` 속성에 대해 읽힙니다. + +이후 데이터를 변환하고 검증합니다. 그래서 그 `settings` 객체를 사용할 때는 선언한 타입의 데이터를 갖게 됩니다(예: `items_per_user`는 `int`가 됩니다). + +### `settings` 사용하기 { #use-the-settings } + +이제 애플리케이션에서 새 `settings` 객체를 사용할 수 있습니다: + +{* ../../docs_src/settings/tutorial001_py39.py hl[18:20] *} + +### 서버 실행하기 { #run-the-server } + +다음으로 환경 변수를 통해 구성을 전달하면서 서버를 실행합니다. 예를 들어 다음처럼 `ADMIN_EMAIL`과 `APP_NAME`을 설정할 수 있습니다: + +<div class="termy"> + +```console +$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.py + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +/// tip | 팁 + +하나의 명령에 여러 env var를 설정하려면 공백으로 구분하고, 모두 명령 앞에 두세요. + +/// + +그러면 `admin_email` 설정은 `"deadpool@example.com"`으로 설정됩니다. + +`app_name`은 `"ChimichangApp"`이 됩니다. + +그리고 `items_per_user`는 기본값 `50`을 유지합니다. + +## 다른 모듈의 설정 { #settings-in-another-module } + +[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}에서 본 것처럼, 설정을 다른 모듈 파일에 넣을 수도 있습니다. + +예를 들어 `config.py` 파일을 다음처럼 만들 수 있습니다: + +{* ../../docs_src/settings/app01_py39/config.py *} + +그리고 `main.py` 파일에서 이를 사용합니다: + +{* ../../docs_src/settings/app01_py39/main.py hl[3,11:13] *} + +/// tip | 팁 + +[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}에서 본 것처럼 `__init__.py` 파일도 필요합니다. + +/// + +## 의존성에서 설정 사용하기 { #settings-in-a-dependency } + +어떤 경우에는 어디서나 사용되는 전역 `settings` 객체를 두는 대신, 의존성에서 설정을 제공하는 것이 유용할 수 있습니다. + +이는 특히 테스트 중에 유용할 수 있는데, 사용자 정의 설정으로 의존성을 override하기가 매우 쉽기 때문입니다. + +### config 파일 { #the-config-file } + +이전 예시에서 이어서, `config.py` 파일은 다음과 같을 수 있습니다: + +{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} + +이제는 기본 인스턴스 `settings = Settings()`를 생성하지 않는다는 점에 유의하세요. + +### 메인 앱 파일 { #the-main-app-file } + +이제 새로운 `config.Settings()`를 반환하는 의존성을 생성합니다. + +{* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *} + +/// tip | 팁 + +`@lru_cache`는 조금 뒤에 다룹니다. + +지금은 `get_settings()`가 일반 함수라고 가정해도 됩니다. + +/// + +그 다음 *경로 처리 함수*에서 이를 의존성으로 요구하고, 필요한 어디서든 사용할 수 있습니다. + +{* ../../docs_src/settings/app02_an_py39/main.py hl[17,19:21] *} + +### 설정과 테스트 { #settings-and-testing } + +그 다음, `get_settings`에 대한 의존성 override를 만들어 테스트 중에 다른 설정 객체를 제공하기가 매우 쉬워집니다: + +{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *} + +의존성 override에서는 새 `Settings` 객체를 생성할 때 `admin_email`의 새 값을 설정하고, 그 새 객체를 반환합니다. + +그 다음 그것이 사용되는지 테스트할 수 있습니다. + +## `.env` 파일 읽기 { #reading-a-env-file } + +많이 바뀔 수 있는 설정이 많고, 서로 다른 환경에서 사용한다면, 이를 파일에 넣어 환경 변수인 것처럼 읽는 것이 유용할 수 있습니다. + +이 관행은 충분히 흔해서 이름도 있는데, 이러한 환경 변수들은 보통 `.env` 파일에 두며, 그 파일을 "dotenv"라고 부릅니다. + +/// tip | 팁 + +점(`.`)으로 시작하는 파일은 Linux, macOS 같은 Unix 계열 시스템에서 숨김 파일입니다. + +하지만 dotenv 파일이 꼭 그 정확한 파일명을 가져야 하는 것은 아닙니다. + +/// + +Pydantic은 외부 라이브러리를 사용해 이런 유형의 파일에서 읽는 기능을 지원합니다. 자세한 내용은 <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic Settings: Dotenv (.env) support</a>를 참고하세요. + +/// tip | 팁 + +이를 사용하려면 `pip install python-dotenv`가 필요합니다. + +/// + +### `.env` 파일 { #the-env-file } + +다음과 같은 `.env` 파일을 둘 수 있습니다: + +```bash +ADMIN_EMAIL="deadpool@example.com" +APP_NAME="ChimichangApp" +``` + +### `.env`에서 설정 읽기 { #read-settings-from-env } + +그리고 `config.py`를 다음처럼 업데이트합니다: + +{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} + +/// tip | 팁 + +`model_config` 속성은 Pydantic 설정을 위한 것입니다. 자세한 내용은 <a href="https://docs.pydantic.dev/latest/concepts/config/" class="external-link" target="_blank">Pydantic: Concepts: Configuration</a>을 참고하세요. + +/// + +여기서는 Pydantic `Settings` 클래스 안에 config `env_file`을 정의하고, 사용하려는 dotenv 파일의 파일명을 값으로 설정합니다. + +### `lru_cache`로 `Settings`를 한 번만 생성하기 { #creating-the-settings-only-once-with-lru-cache } + +디스크에서 파일을 읽는 것은 보통 비용이 큰(느린) 작업이므로, 각 요청마다 읽기보다는 한 번만 수행하고 동일한 settings 객체를 재사용하는 것이 좋습니다. + +하지만 매번 다음을 수행하면: + +```Python +Settings() +``` + +새 `Settings` 객체가 생성되고, 생성 시점에 `.env` 파일을 다시 읽게 됩니다. + +의존성 함수가 단순히 다음과 같다면: + +```Python +def get_settings(): + return Settings() +``` + +요청마다 객체를 생성하게 되고, 요청마다 `.env` 파일을 읽게 됩니다. ⚠️ + +하지만 위에 `@lru_cache` 데코레이터를 사용하고 있으므로, `Settings` 객체는 최초 호출 시 딱 한 번만 생성됩니다. ✔️ + +{* ../../docs_src/settings/app03_an_py39/main.py hl[1,11] *} + +그 다음 요청들에서 의존성으로 `get_settings()`가 다시 호출될 때마다, `get_settings()`의 내부 코드를 실행해서 새 `Settings` 객체를 만드는 대신, 첫 호출에서 반환된 동일한 객체를 계속 반환합니다. + +#### `lru_cache` Technical Details { #lru-cache-technical-details } + +`@lru_cache`는 데코레이션한 함수가 매번 다시 계산하는 대신, 첫 번째에 반환된 동일한 값을 반환하도록 함수를 수정합니다(즉, 매번 함수 코드를 실행하지 않습니다). + +따라서 아래의 함수는 인자 조합마다 한 번씩 실행됩니다. 그리고 각각의 인자 조합에 대해 반환된 값은, 함수가 정확히 같은 인자 조합으로 호출될 때마다 반복해서 사용됩니다. + +예를 들어 다음 함수가 있다면: + +```Python +@lru_cache +def say_hi(name: str, salutation: str = "Ms."): + return f"Hello {salutation} {name}" +``` + +프로그램은 다음과 같이 실행될 수 있습니다: + +```mermaid +sequenceDiagram + +participant code as Code +participant function as say_hi() +participant execute as Execute function + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Camila") + function ->> execute: execute function code + execute ->> code: return the result + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Camila") + function ->> code: return stored result + end + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Rick") + function ->> execute: execute function code + execute ->> code: return the result + end + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Rick", salutation="Mr.") + function ->> execute: execute function code + execute ->> code: return the result + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Rick") + function ->> code: return stored result + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Camila") + function ->> code: return stored result + end +``` + +우리의 의존성 `get_settings()`의 경우, 함수가 어떤 인자도 받지 않으므로 항상 같은 값을 반환합니다. + +이렇게 하면 거의 전역 변수처럼 동작합니다. 하지만 의존성 함수를 사용하므로 테스트를 위해 쉽게 override할 수 있습니다. + +`@lru_cache`는 Python 표준 라이브러리의 `functools`에 포함되어 있으며, 자세한 내용은 <a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">`@lru_cache`에 대한 Python 문서</a>에서 확인할 수 있습니다. + +## 정리 { #recap } + +Pydantic Settings를 사용하면 Pydantic 모델의 모든 강력한 기능을 활용해 애플리케이션의 설정 또는 구성을 처리할 수 있습니다. + +* 의존성을 사용하면 테스트를 단순화할 수 있습니다. +* `.env` 파일을 사용할 수 있습니다. +* `@lru_cache`를 사용하면 각 요청마다 dotenv 파일을 반복해서 읽는 것을 피하면서도, 테스트 중에는 이를 override할 수 있습니다. diff --git a/docs/ko/docs/alternatives.md b/docs/ko/docs/alternatives.md new file mode 100644 index 000000000..d8c2df2d8 --- /dev/null +++ b/docs/ko/docs/alternatives.md @@ -0,0 +1,485 @@ +# 대안, 영감, 비교 { #alternatives-inspiration-and-comparisons } + +**FastAPI**에 영감을 준 것들, 대안과의 비교, 그리고 그로부터 무엇을 배웠는지에 대한 내용입니다. + +## 소개 { #intro } + +다른 사람들의 이전 작업이 없었다면 **FastAPI**는 존재하지 않았을 것입니다. + +그 전에 만들어진 많은 도구들이 **FastAPI**의 탄생에 영감을 주었습니다. + +저는 여러 해 동안 새로운 framework를 만드는 것을 피하고 있었습니다. 먼저 **FastAPI**가 다루는 모든 기능을 여러 서로 다른 framework, plug-in, 도구를 사용해 해결해 보려고 했습니다. + +하지만 어느 시점에는, 이전 도구들의 가장 좋은 아이디어를 가져와 가능한 최선의 방식으로 조합하고, 이전에는 존재하지 않았던 언어 기능(Python 3.6+ type hints)을 활용해 이 모든 기능을 제공하는 무언가를 만드는 것 외에는 다른 선택지가 없었습니다. + +## 이전 도구들 { #previous-tools } + +### <a href="https://www.djangoproject.com/" class="external-link" target="_blank">Django</a> { #django } + +가장 인기 있는 Python framework이며 널리 신뢰받고 있습니다. Instagram 같은 시스템을 만드는 데 사용됩니다. + +상대적으로 관계형 데이터베이스(예: MySQL 또는 PostgreSQL)와 강하게 결합되어 있어서, NoSQL 데이터베이스(예: Couchbase, MongoDB, Cassandra 등)를 주 저장 엔진으로 사용하는 것은 그리 쉽지 않습니다. + +백엔드에서 HTML을 생성하기 위해 만들어졌지, 현대적인 프런트엔드(예: React, Vue.js, Angular)나 다른 시스템(예: <abbr title="Internet of Things - 사물 인터넷">IoT</abbr> 기기)에서 사용되는 API를 만들기 위해 설계된 것은 아닙니다. + +### <a href="https://www.django-rest-framework.org/" class="external-link" target="_blank">Django REST Framework</a> { #django-rest-framework } + +Django REST framework는 Django를 기반으로 Web API를 구축하기 위한 유연한 toolkit으로 만들어졌고, Django의 API 기능을 개선하기 위한 목적이었습니다. + +Mozilla, Red Hat, Eventbrite를 포함해 많은 회사에서 사용합니다. + +**자동 API 문서화**의 초기 사례 중 하나였고, 이것이 특히 **FastAPI**를 "찾게 된" 첫 아이디어 중 하나였습니다. + +/// note | 참고 + +Django REST Framework는 Tom Christie가 만들었습니다. **FastAPI**의 기반이 되는 Starlette와 Uvicorn을 만든 사람과 동일합니다. + +/// + +/// check | **FastAPI**에 영감을 준 것 + +자동 API 문서화 웹 사용자 인터페이스를 제공하기. + +/// + +### <a href="https://flask.palletsprojects.com" class="external-link" target="_blank">Flask</a> { #flask } + +Flask는 "microframework"로, Django에 기본으로 포함된 데이터베이스 통합이나 여러 기능들을 포함하지 않습니다. + +이 단순함과 유연성 덕분에 NoSQL 데이터베이스를 주 데이터 저장 시스템으로 사용하는 같은 작업이 가능합니다. + +매우 단순하기 때문에 비교적 직관적으로 배울 수 있지만, 문서가 어떤 지점에서는 다소 기술적으로 깊어지기도 합니다. + +또한 데이터베이스, 사용자 관리, 혹은 Django에 미리 구축되어 있는 다양한 기능들이 꼭 필요하지 않은 다른 애플리케이션에도 흔히 사용됩니다. 물론 이런 기능들 중 다수는 plug-in으로 추가할 수 있습니다. + +이런 구성요소의 분리와, 필요한 것만 정확히 덧붙여 확장할 수 있는 "microframework"라는 점은 제가 유지하고 싶었던 핵심 특성이었습니다. + +Flask의 단순함을 고려하면 API를 구축하는 데 잘 맞는 것처럼 보였습니다. 다음으로 찾고자 했던 것은 Flask용 "Django REST Framework"였습니다. + +/// check | **FastAPI**에 영감을 준 것 + +micro-framework가 되기. 필요한 도구와 구성요소를 쉽게 조합할 수 있도록 하기. + +단순하고 사용하기 쉬운 routing 시스템을 갖기. + +/// + +### <a href="https://requests.readthedocs.io" class="external-link" target="_blank">Requests</a> { #requests } + +**FastAPI**는 실제로 **Requests**의 대안이 아닙니다. 둘의 범위는 매우 다릅니다. + +실제로 FastAPI 애플리케이션 *내부에서* Requests를 사용하는 경우도 흔합니다. + +그럼에도 FastAPI는 Requests로부터 꽤 많은 영감을 얻었습니다. + +**Requests**는 (클라이언트로서) API와 *상호작용*하기 위한 라이브러리이고, **FastAPI**는 (서버로서) API를 *구축*하기 위한 라이브러리입니다. + +대략 말하면 서로 반대편에 있으며, 서로를 보완합니다. + +Requests는 매우 단순하고 직관적인 설계를 가졌고, 합리적인 기본값을 바탕으로 사용하기가 아주 쉽습니다. 동시에 매우 강력하고 커스터마이징도 가능합니다. + +그래서 공식 웹사이트에서 말하듯이: + +> Requests is one of the most downloaded Python packages of all time + +사용 방법은 매우 간단합니다. 예를 들어 `GET` 요청을 하려면 다음처럼 작성합니다: + +```Python +response = requests.get("http://example.com/some/url") +``` + +이에 대응하는 FastAPI의 API *경로 처리*는 다음과 같이 보일 수 있습니다: + +```Python hl_lines="1" +@app.get("/some/url") +def read_url(): + return {"message": "Hello World"} +``` + +`requests.get(...)`와 `@app.get(...)`의 유사성을 확인해 보세요. + +/// check | **FastAPI**에 영감을 준 것 + +* 단순하고 직관적인 API를 갖기. +* HTTP method 이름(operations)을 직접, 직관적이고 명확한 방식으로 사용하기. +* 합리적인 기본값을 제공하되, 강력한 커스터마이징을 가능하게 하기. + +/// + +### <a href="https://swagger.io/" class="external-link" target="_blank">Swagger</a> / <a href="https://github.com/OAI/OpenAPI-Specification/" class="external-link" target="_blank">OpenAPI</a> { #swagger-openapi } + +제가 Django REST Framework에서 가장 원했던 주요 기능은 자동 API 문서화였습니다. + +그 후 JSON(또는 JSON의 확장인 YAML)을 사용해 API를 문서화하는 표준인 Swagger가 있다는 것을 알게 되었습니다. + +그리고 Swagger API를 위한 웹 사용자 인터페이스도 이미 만들어져 있었습니다. 그래서 API에 대한 Swagger 문서를 생성할 수 있다면, 이 웹 사용자 인터페이스를 자동으로 사용할 수 있게 됩니다. + +어느 시점에 Swagger는 Linux Foundation으로 넘어가 OpenAPI로 이름이 바뀌었습니다. + +그래서 2.0 버전을 이야기할 때는 "Swagger"라고 말하는 것이 일반적이고, 3+ 버전은 "OpenAPI"라고 말하는 것이 일반적입니다. + +/// check | **FastAPI**에 영감을 준 것 + +커스텀 schema 대신, API 사양을 위한 열린 표준을 채택하고 사용하기. + +또한 표준 기반의 사용자 인터페이스 도구를 통합하기: + +* <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> +* <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> + +이 두 가지는 꽤 대중적이고 안정적이기 때문에 선택되었습니다. 하지만 간단히 검색해보면 OpenAPI를 위한 대안 UI가 수십 가지나 있다는 것을 알 수 있습니다(**FastAPI**와 함께 사용할 수 있습니다). + +/// + +### Flask REST framework들 { #flask-rest-frameworks } + +Flask REST framework는 여러 개가 있지만, 시간을 들여 조사해 본 결과, 상당수가 중단되었거나 방치되어 있었고, 해결되지 않은 여러 이슈 때문에 적합하지 않은 경우가 많았습니다. + +### <a href="https://marshmallow.readthedocs.io/en/stable/" class="external-link" target="_blank">Marshmallow</a> { #marshmallow } + +API 시스템에 필요한 주요 기능 중 하나는 데이터 "<abbr title="also called marshalling, conversion - 마샬링, 변환이라고도 합니다">serialization</abbr>"입니다. 이는 코드(Python)에서 데이터를 가져와 네트워크로 전송할 수 있는 형태로 변환하는 것을 의미합니다. 예를 들어 데이터베이스의 데이터를 담은 객체를 JSON 객체로 변환하거나, `datetime` 객체를 문자열로 변환하는 등의 작업입니다. + +API에 또 하나 크게 필요한 기능은 데이터 검증입니다. 특정 파라미터를 기준으로 데이터가 유효한지 확인하는 것입니다. 예를 들어 어떤 필드가 `int`인지, 임의의 문자열이 아닌지 확인하는 식입니다. 이는 특히 들어오는 데이터에 유용합니다. + +데이터 검증 시스템이 없다면, 모든 검사를 코드에서 수동으로 해야 합니다. + +이런 기능들을 제공하기 위해 Marshmallow가 만들어졌습니다. 훌륭한 라이브러리이며, 저도 이전에 많이 사용했습니다. + +하지만 Python type hints가 존재하기 전에 만들어졌습니다. 그래서 각 <abbr title="the definition of how data should be formed - 데이터가 어떻게 구성되어야 하는지에 대한 정의">schema</abbr>를 정의하려면 Marshmallow가 제공하는 특정 유틸리티와 클래스를 사용해야 합니다. + +/// check | **FastAPI**에 영감을 준 것 + +데이터 타입과 검증을 제공하는 "schema"를 코드로 정의하고, 이를 자동으로 활용하기. + +/// + +### <a href="https://webargs.readthedocs.io/en/latest/" class="external-link" target="_blank">Webargs</a> { #webargs } + +API에 필요한 또 다른 큰 기능은 들어오는 요청에서 데이터를 <abbr title="reading and converting to Python data - 읽어서 Python 데이터로 변환하기">parsing</abbr>하는 것입니다. + +Webargs는 Flask를 포함한 여러 framework 위에서 이를 제공하기 위해 만들어진 도구입니다. + +내부적으로 Marshmallow를 사용해 데이터 검증을 수행합니다. 그리고 같은 개발자들이 만들었습니다. + +아주 훌륭한 도구이며, 저도 **FastAPI**를 만들기 전에 많이 사용했습니다. + +/// info | 정보 + +Webargs는 Marshmallow와 같은 개발자들이 만들었습니다. + +/// + +/// check | **FastAPI**에 영감을 준 것 + +들어오는 요청 데이터의 자동 검증을 갖기. + +/// + +### <a href="https://apispec.readthedocs.io/en/stable/" class="external-link" target="_blank">APISpec</a> { #apispec } + +Marshmallow와 Webargs는 plug-in 형태로 검증, parsing, serialization을 제공합니다. + +하지만 문서화는 여전히 부족했습니다. 그래서 APISpec이 만들어졌습니다. + +이는 여러 framework를 위한 plug-in이며(Starlette용 plug-in도 있습니다). + +작동 방식은, 각 route를 처리하는 함수의 docstring 안에 YAML 형식으로 schema 정의를 작성하고, + +그로부터 OpenAPI schema를 생성합니다. + +Flask, Starlette, Responder 등에서 이런 방식으로 동작합니다. + +하지만 다시, Python 문자열 내부(큰 YAML)에서 micro-syntax를 다루어야 한다는 문제가 있습니다. + +에디터가 이를 크게 도와주지 못합니다. 또한 파라미터나 Marshmallow schema를 수정해놓고 YAML docstring도 같이 수정하는 것을 잊어버리면, 생성된 schema는 오래된 상태가 됩니다. + +/// info | 정보 + +APISpec은 Marshmallow와 같은 개발자들이 만들었습니다. + +/// + +/// check | **FastAPI**에 영감을 준 것 + +API를 위한 열린 표준인 OpenAPI를 지원하기. + +/// + +### <a href="https://flask-apispec.readthedocs.io/en/latest/" class="external-link" target="_blank">Flask-apispec</a> { #flask-apispec } + +Flask plug-in으로, Webargs, Marshmallow, APISpec을 묶어줍니다. + +Webargs와 Marshmallow의 정보를 사용해 APISpec으로 OpenAPI schema를 자동 생성합니다. + +훌륭한 도구인데도 과소평가되어 있습니다. 다른 많은 Flask plug-in보다 훨씬 더 유명해져야 합니다. 문서가 너무 간결하고 추상적이라서 그럴 수도 있습니다. + +이 도구는 Python docstring 내부에 YAML(또 다른 문법)을 작성해야 하는 문제를 해결했습니다. + +Flask + Flask-apispec + Marshmallow + Webargs 조합은 **FastAPI**를 만들기 전까지 제가 가장 좋아하던 백엔드 stack이었습니다. + +이를 사용하면서 여러 Flask full-stack generator가 만들어졌습니다. 이것들이 지금까지 저(그리고 여러 외부 팀)가 사용해 온 주요 stack입니다: + +* <a href="https://github.com/tiangolo/full-stack" class="external-link" target="_blank">https://github.com/tiangolo/full-stack</a> +* <a href="https://github.com/tiangolo/full-stack-flask-couchbase" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-flask-couchbase</a> +* <a href="https://github.com/tiangolo/full-stack-flask-couchdb" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-flask-couchdb</a> + +그리고 이 동일한 full-stack generator들이 [**FastAPI** Project Generators](project-generation.md){.internal-link target=_blank}의 기반이 되었습니다. + +/// info | 정보 + +Flask-apispec은 Marshmallow와 같은 개발자들이 만들었습니다. + +/// + +/// check | **FastAPI**에 영감을 준 것 + +serialization과 validation을 정의하는 동일한 코드로부터 OpenAPI schema를 자동 생성하기. + +/// + +### <a href="https://nestjs.com/" class="external-link" target="_blank">NestJS</a> (그리고 <a href="https://angular.io/" class="external-link" target="_blank">Angular</a>) { #nestjs-and-angular } + +이건 Python도 아닙니다. NestJS는 Angular에서 영감을 받은 JavaScript(TypeScript) NodeJS framework입니다. + +Flask-apispec으로 할 수 있는 것과 어느 정도 비슷한 것을 달성합니다. + +Angular 2에서 영감을 받은 의존성 주입 시스템이 통합되어 있습니다. 제가 아는 다른 의존성 주입 시스템들처럼 "injectable"을 사전에 등록해야 하므로, 장황함과 코드 반복이 늘어납니다. + +파라미터가 TypeScript 타입(Python type hints와 유사함)으로 설명되기 때문에 에디터 지원은 꽤 좋습니다. + +하지만 TypeScript 데이터는 JavaScript로 컴파일된 뒤에는 보존되지 않기 때문에, 타입에 의존해 검증, serialization, 문서화를 동시에 정의할 수 없습니다. 이 점과 일부 설계 결정 때문에, 검증/serialization/자동 schema 생성을 하려면 여러 곳에 decorator를 추가해야 하며, 결과적으로 매우 장황해집니다. + +중첩 모델을 잘 처리하지 못합니다. 즉, 요청의 JSON body가 내부 필드를 가진 JSON 객체이고 그 내부 필드들이 다시 중첩된 JSON 객체인 경우, 제대로 문서화하고 검증할 수 없습니다. + +/// check | **FastAPI**에 영감을 준 것 + +Python 타입을 사용해 뛰어난 에디터 지원을 제공하기. + +강력한 의존성 주입 시스템을 갖추기. 코드 반복을 최소화하는 방법을 찾기. + +/// + +### <a href="https://sanic.readthedocs.io/en/latest/" class="external-link" target="_blank">Sanic</a> { #sanic } + +`asyncio` 기반의 매우 빠른 Python framework 중 초기 사례였습니다. Flask와 매우 유사하게 만들어졌습니다. + +/// note | 기술 세부사항 + +기본 Python `asyncio` 루프 대신 <a href="https://github.com/MagicStack/uvloop" class="external-link" target="_blank">`uvloop`</a>를 사용했습니다. 이것이 매우 빠르게 만든 요인입니다. + +이는 Uvicorn과 Starlette에 명확히 영감을 주었고, 현재 공개 benchmark에서는 이 둘이 Sanic보다 더 빠릅니다. + +/// + +/// check | **FastAPI**에 영감을 준 것 + +미친 성능을 낼 수 있는 방법을 찾기. + +그래서 **FastAPI**는 Starlette를 기반으로 합니다. Starlette는 사용 가능한 framework 중 가장 빠르기 때문입니다(서드파티 benchmark로 테스트됨). + +/// + +### <a href="https://falconframework.org/" class="external-link" target="_blank">Falcon</a> { #falcon } + +Falcon은 또 다른 고성능 Python framework로, 최소한으로 설계되었고 Hug 같은 다른 framework의 기반으로 동작하도록 만들어졌습니다. + +함수가 두 개의 파라미터(하나는 "request", 하나는 "response")를 받도록 설계되어 있습니다. 그런 다음 request에서 일부를 "읽고", response에 일부를 "작성"합니다. 이 설계 때문에, 표준 Python type hints를 함수 파라미터로 사용해 요청 파라미터와 body를 선언하는 것이 불가능합니다. + +따라서 데이터 검증, serialization, 문서화는 자동으로 되지 않고 코드로 해야 합니다. 또는 Hug처럼 Falcon 위에 framework를 얹어 구현해야 합니다. request 객체 하나와 response 객체 하나를 파라미터로 받는 Falcon의 설계에서 영감을 받은 다른 framework에서도 같은 구분이 나타납니다. + +/// check | **FastAPI**에 영감을 준 것 + +훌륭한 성능을 얻는 방법을 찾기. + +Hug(= Falcon 기반)과 함께, 함수에서 `response` 파라미터를 선언하도록 **FastAPI**에 영감을 주었습니다. + +다만 FastAPI에서는 선택 사항이며, 주로 헤더, 쿠키, 그리고 대체 status code를 설정하는 데 사용됩니다. + +/// + +### <a href="https://moltenframework.com/" class="external-link" target="_blank">Molten</a> { #molten } + +**FastAPI**를 만들기 시작한 초기 단계에서 Molten을 알게 되었고, 꽤 비슷한 아이디어를 갖고 있었습니다: + +* Python type hints 기반 +* 이 타입으로부터 검증과 문서화 생성 +* 의존성 주입 시스템 + +Pydantic 같은 서드파티 라이브러리를 사용해 데이터 검증/serialization/문서화를 하지 않고 자체 구현을 사용합니다. 그래서 이런 데이터 타입 정의를 쉽게 재사용하기는 어렵습니다. + +조금 더 장황한 설정이 필요합니다. 또한 WSGI(ASGI가 아니라) 기반이므로, Uvicorn, Starlette, Sanic 같은 도구가 제공하는 고성능을 활용하도록 설계되지 않았습니다. + +의존성 주입 시스템은 의존성을 사전에 등록해야 하고, 선언된 타입을 기반으로 의존성을 해결합니다. 따라서 특정 타입을 제공하는 "component"를 두 개 이상 선언할 수 없습니다. + +Route는 한 곳에서 선언하고, 다른 곳에 선언된 함수를 사용합니다(엔드포인트를 처리하는 함수 바로 위에 둘 수 있는 decorator를 사용하는 대신). 이는 Flask(및 Starlette)보다는 Django 방식에 가깝습니다. 코드에서 상대적으로 강하게 결합된 것들을 분리해 놓습니다. + +/// check | **FastAPI**에 영감을 준 것 + +모델 속성의 "default" 값으로 데이터 타입에 대한 추가 검증을 정의하기. 이는 에디터 지원을 개선하며, 이전에는 Pydantic에 없었습니다. + +이것은 실제로 Pydantic의 일부를 업데이트하여 같은 검증 선언 스타일을 지원하도록 하는 데 영감을 주었습니다(이 기능은 이제 Pydantic에 이미 포함되어 있습니다). + +/// + +### <a href="https://github.com/hugapi/hug" class="external-link" target="_blank">Hug</a> { #hug } + +Hug는 Python type hints를 사용해 API 파라미터 타입을 선언하는 기능을 구현한 초기 framework 중 하나였습니다. 이는 다른 도구들도 같은 방식을 하도록 영감을 준 훌륭한 아이디어였습니다. + +표준 Python 타입 대신 커스텀 타입을 선언에 사용했지만, 여전히 큰 진전이었습니다. + +또한 전체 API를 JSON으로 선언하는 커스텀 schema를 생성한 초기 framework 중 하나였습니다. + +OpenAPI나 JSON Schema 같은 표준을 기반으로 하지 않았기 때문에 Swagger UI 같은 다른 도구와 통합하는 것은 직관적이지 않았습니다. 하지만 역시 매우 혁신적인 아이디어였습니다. + +흥미롭고 흔치 않은 기능이 하나 있습니다. 같은 framework로 API뿐 아니라 CLI도 만들 수 있습니다. + +동기식 Python 웹 framework의 이전 표준(WSGI) 기반이어서 Websockets와 다른 것들을 처리할 수는 없지만, 성능은 여전히 높습니다. + +/// info | 정보 + +Hug는 Timothy Crosley가 만들었습니다. Python 파일에서 import를 자동으로 정렬하는 훌륭한 도구인 <a href="https://github.com/timothycrosley/isort" class="external-link" target="_blank">`isort`</a>의 제작자이기도 합니다. + +/// + +/// check | **FastAPI**에 영감을 준 아이디어들 + +Hug는 APIStar의 일부에 영감을 주었고, 저는 APIStar와 함께 Hug를 가장 유망한 도구 중 하나로 보았습니다. + +Hug는 Python type hints로 파라미터를 선언하고, API를 정의하는 schema를 자동으로 생성하도록 **FastAPI**에 영감을 주었습니다. + +Hug는 헤더와 쿠키를 설정하기 위해 함수에 `response` 파라미터를 선언하도록 **FastAPI**에 영감을 주었습니다. + +/// + +### <a href="https://github.com/encode/apistar" class="external-link" target="_blank">APIStar</a> (<= 0.5) { #apistar-0-5 } + +**FastAPI**를 만들기로 결정하기 직전에 **APIStar** 서버를 발견했습니다. 찾고 있던 거의 모든 것을 갖추고 있었고 설계도 훌륭했습니다. + +NestJS와 Molten보다 앞서, Python type hints를 사용해 파라미터와 요청을 선언하는 framework 구현을 제가 처음 본 사례들 중 하나였습니다. Hug와 거의 같은 시기에 발견했습니다. 하지만 APIStar는 OpenAPI 표준을 사용했습니다. + +여러 위치에서 동일한 type hints를 기반으로 자동 데이터 검증, 데이터 serialization, OpenAPI schema 생성을 제공했습니다. + +Body schema 정의는 Pydantic처럼 동일한 Python type hints를 사용하지는 않았고 Marshmallow와 조금 더 비슷해서 에디터 지원은 그만큼 좋지 않았지만, 그래도 APIStar는 당시 사용할 수 있는 최선의 선택지였습니다. + +당시 최고의 성능 benchmark를 가졌습니다(Starlette에 의해서만 추월됨). + +처음에는 자동 API 문서화 웹 UI가 없었지만, Swagger UI를 추가할 수 있다는 것을 알고 있었습니다. + +의존성 주입 시스템도 있었습니다. 위에서 언급한 다른 도구들처럼 component의 사전 등록이 필요했지만, 여전히 훌륭한 기능이었습니다. + +보안 통합이 없어서 전체 프로젝트에서 사용해 볼 수는 없었습니다. 그래서 Flask-apispec 기반 full-stack generator로 갖추고 있던 모든 기능을 대체할 수 없었습니다. 그 기능을 추가하는 pull request를 만드는 것이 제 백로그에 있었습니다. + +하지만 이후 프로젝트의 초점이 바뀌었습니다. + +더 이상 API web framework가 아니게 되었는데, 제작자가 Starlette에 집중해야 했기 때문입니다. + +이제 APIStar는 web framework가 아니라 OpenAPI 사양을 검증하기 위한 도구 모음입니다. + +/// info | 정보 + +APIStar는 Tom Christie가 만들었습니다. 다음을 만든 사람과 동일합니다: + +* Django REST Framework +* Starlette(**FastAPI**의 기반) +* Uvicorn(Starlette와 **FastAPI**에서 사용) + +/// + +/// check | **FastAPI**에 영감을 준 것 + +존재하게 만들기. + +동일한 Python 타입으로 여러 가지(데이터 검증, serialization, 문서화)를 선언하면서 동시에 뛰어난 에디터 지원을 제공한다는 아이디어는 제가 매우 훌륭하다고 생각했습니다. + +그리고 오랫동안 비슷한 framework를 찾아 여러 대안을 테스트한 끝에, APIStar가 그때 이용 가능한 최선의 선택지였습니다. + +그 후 APIStar 서버가 더는 존재하지 않게 되고 Starlette가 만들어졌는데, 이는 그런 시스템을 위한 더 새롭고 더 나은 기반이었습니다. 이것이 **FastAPI**를 만들게 된 최종 영감이었습니다. + +저는 **FastAPI**를 APIStar의 "정신적 후계자"로 생각합니다. 동시에, 이 모든 이전 도구들에서 배운 것들을 바탕으로 기능, typing 시스템, 그리고 다른 부분들을 개선하고 확장했습니다. + +/// + +## **FastAPI**가 사용하는 것 { #used-by-fastapi } + +### <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> { #pydantic } + +Pydantic은 Python type hints를 기반으로 데이터 검증, serialization, 문서화(JSON Schema 사용)를 정의하는 라이브러리입니다. + +그 덕분에 매우 직관적입니다. + +Marshmallow와 비교할 수 있습니다. 다만 benchmark에서 Marshmallow보다 빠릅니다. 그리고 동일한 Python type hints를 기반으로 하므로 에디터 지원도 훌륭합니다. + +/// check | **FastAPI**가 이를 사용하는 목적 + +모든 데이터 검증, 데이터 serialization, 자동 모델 문서화(JSON Schema 기반)를 처리하기. + +그 다음 **FastAPI**는 그 JSON Schema 데이터를 가져와 OpenAPI에 포함시키며, 그 외에도 여러 작업을 수행합니다. + +/// + +### <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a> { #starlette } + +Starlette는 경량 <abbr title="The new standard for building asynchronous Python web applications - 비동기 Python 웹 애플리케이션을 구축하기 위한 새로운 표준">ASGI</abbr> framework/toolkit으로, 고성능 asyncio 서비스를 만들기에 이상적입니다. + +매우 단순하고 직관적입니다. 쉽게 확장할 수 있도록 설계되었고, 모듈식 component를 갖습니다. + +다음이 포함됩니다: + +* 정말 인상적인 성능. +* WebSocket 지원. +* 프로세스 내 백그라운드 작업. +* 시작 및 종료 이벤트. +* HTTPX 기반의 테스트 클라이언트. +* CORS, GZip, Static Files, Streaming responses. +* 세션 및 쿠키 지원. +* 100% 테스트 커버리지. +* 100% 타입 주석이 달린 코드베이스. +* 소수의 필수 의존성. + +Starlette는 현재 테스트된 Python framework 중 가장 빠릅니다. 단, framework가 아니라 서버인 Uvicorn이 더 빠릅니다. + +Starlette는 웹 microframework의 기본 기능을 모두 제공합니다. + +하지만 자동 데이터 검증, serialization, 문서화는 제공하지 않습니다. + +그것이 **FastAPI**가 위에 추가하는 핵심 중 하나이며, 모두 Python type hints(Pydantic 사용)를 기반으로 합니다. 여기에 더해 의존성 주입 시스템, 보안 유틸리티, OpenAPI schema 생성 등도 포함됩니다. + +/// note | 기술 세부사항 + +ASGI는 Django 코어 팀 멤버들이 개발 중인 새로운 "표준"입니다. 아직 "Python 표준"(PEP)은 아니지만, 그 방향으로 진행 중입니다. + +그럼에도 이미 여러 도구에서 "표준"으로 사용되고 있습니다. 이는 상호운용성을 크게 개선합니다. 예를 들어 Uvicorn을 다른 ASGI 서버(예: Daphne 또는 Hypercorn)로 교체할 수도 있고, `python-socketio` 같은 ASGI 호환 도구를 추가할 수도 있습니다. + +/// + +/// check | **FastAPI**가 이를 사용하는 목적 + +핵심 웹 부분을 모두 처리하기. 그 위에 기능을 추가하기. + +`FastAPI` 클래스 자체는 `Starlette` 클래스를 직접 상속합니다. + +따라서 Starlette로 할 수 있는 모든 것은 기본적으로 **FastAPI**로도 직접 할 수 있습니다. 즉, **FastAPI**는 사실상 Starlette에 강력한 기능을 더한 것입니다. + +/// + +### <a href="https://www.uvicorn.dev/" class="external-link" target="_blank">Uvicorn</a> { #uvicorn } + +Uvicorn은 uvloop과 httptools로 구축된 초고속 ASGI 서버입니다. + +web framework가 아니라 서버입니다. 예를 들어 경로 기반 routing을 위한 도구는 제공하지 않습니다. 그런 것은 Starlette(또는 **FastAPI**) 같은 framework가 위에서 제공합니다. + +Starlette와 **FastAPI**에서 권장하는 서버입니다. + +/// check | **FastAPI**가 이를 권장하는 방식 + +**FastAPI** 애플리케이션을 실행하기 위한 주요 웹 서버. + +또한 `--workers` 커맨드라인 옵션을 사용하면 비동기 멀티프로세스 서버로 실행할 수도 있습니다. + +자세한 내용은 [배포](deployment/index.md){.internal-link target=_blank} 섹션을 확인하세요. + +/// + +## 벤치마크와 속도 { #benchmarks-and-speed } + +Uvicorn, Starlette, FastAPI 사이의 차이를 이해하고 비교하려면 [벤치마크](benchmarks.md){.internal-link target=_blank} 섹션을 확인하세요. diff --git a/docs/ko/docs/deployment/concepts.md b/docs/ko/docs/deployment/concepts.md new file mode 100644 index 000000000..dd7edd1ba --- /dev/null +++ b/docs/ko/docs/deployment/concepts.md @@ -0,0 +1,321 @@ +# 배포 개념 { #deployments-concepts } + +**FastAPI** 애플리케이션(사실 어떤 종류의 웹 API든)을 배포할 때는, 여러분이 신경 써야 할 여러 개념이 있습니다. 그리고 이 개념들을 활용하면 **애플리케이션을 배포하기 위한 가장 적절한 방법**을 찾을 수 있습니다. + +중요한 개념 몇 가지는 다음과 같습니다: + +* 보안 - HTTPS +* 시작 시 실행 +* 재시작 +* 복제(실행 중인 프로세스 수) +* 메모리 +* 시작 전 사전 단계 + +이것들이 **배포**에 어떤 영향을 주는지 살펴보겠습니다. + +결국 최종 목표는 **API 클라이언트에 서비스를 제공**할 때 **보안**을 보장하고, **중단을 피하며**, **컴퓨팅 리소스**(예: 원격 서버/가상 머신)를 가능한 한 효율적으로 사용하는 것입니다. 🚀 + +여기서 이 **개념들**을 조금 더 설명하겠습니다. 그러면 서로 매우 다른 환경, 심지어 아직 존재하지 않는 **미래**의 환경에서도 API를 어떻게 배포할지 결정하는 데 필요한 **직관**을 얻을 수 있을 것입니다. + +이 개념들을 고려하면, 여러분은 **자신의 API**를 배포하기 위한 최선의 방법을 **평가하고 설계**할 수 있습니다. + +다음 장들에서는 FastAPI 애플리케이션을 배포하기 위한 더 **구체적인 레시피**를 제공하겠습니다. + +하지만 지금은, 이 중요한 **개념적 아이디어**들을 확인해 봅시다. 이 개념들은 다른 어떤 종류의 웹 API에도 동일하게 적용됩니다. 💡 + +## 보안 - HTTPS { #security-https } + +[이전 HTTPS 장](https.md){.internal-link target=_blank}에서 HTTPS가 API에 암호화를 제공하는 방식에 대해 배웠습니다. + +또한 HTTPS는 일반적으로 애플리케이션 서버 바깥의 **외부** 컴포넌트인 **TLS Termination Proxy**가 제공한다는 것도 확인했습니다. + +그리고 **HTTPS 인증서 갱신**을 담당하는 무언가가 필요합니다. 같은 컴포넌트가 그 역할을 할 수도 있고, 다른 무언가가 담당할 수도 있습니다. + +### HTTPS를 위한 도구 예시 { #example-tools-for-https } + +TLS Termination Proxy로 사용할 수 있는 도구는 예를 들어 다음과 같습니다: + +* Traefik + * 인증서 갱신을 자동으로 처리 ✨ +* Caddy + * 인증서 갱신을 자동으로 처리 ✨ +* Nginx + * 인증서 갱신을 위해 Certbot 같은 외부 컴포넌트 사용 +* HAProxy + * 인증서 갱신을 위해 Certbot 같은 외부 컴포넌트 사용 +* Nginx 같은 Ingress Controller를 사용하는 Kubernetes + * 인증서 갱신을 위해 cert-manager 같은 외부 컴포넌트 사용 +* 클라우드 제공자가 서비스 일부로 내부적으로 처리(아래를 읽어보세요 👇) + +또 다른 선택지는 HTTPS 설정을 포함해 더 많은 일을 대신해주는 **클라우드 서비스**를 사용하는 것입니다. 제약이 있거나 비용이 더 들 수도 있습니다. 하지만 그 경우에는 TLS Termination Proxy를 직접 설정할 필요가 없습니다. + +다음 장에서 구체적인 예시를 보여드리겠습니다. + +--- + +다음으로 고려할 개념들은 실제로 여러분의 API를 실행하는 프로그램(예: Uvicorn)과 관련된 내용입니다. + +## 프로그램과 프로세스 { #program-and-process } + +실행 중인 "**프로세스**"에 대해 많이 이야기하게 될 텐데, 이 말이 무엇을 의미하는지, 그리고 "**프로그램**"이라는 단어와 무엇이 다른지 명확히 해두는 것이 유용합니다. + +### 프로그램이란 { #what-is-a-program } + +**프로그램**이라는 단어는 보통 여러 가지를 가리키는 데 사용됩니다: + +* 여러분이 작성하는 **코드**, 즉 **Python 파일**들 +* 운영체제에서 **실행**할 수 있는 **파일**, 예: `python`, `python.exe`, `uvicorn` +* 운영체제에서 **실행 중**인 특정 프로그램으로, CPU를 사용하고 메모리에 내용을 저장합니다. 이것을 **프로세스**라고도 합니다. + +### 프로세스란 { #what-is-a-process } + +**프로세스**라는 단어는 보통 더 구체적으로, 운영체제에서 실행 중인 것(위 마지막 항목처럼)만을 가리키는 데 사용됩니다: + +* 운영체제에서 **실행 중**인 특정 프로그램 + * 파일이나 코드를 의미하는 것이 아니라, 운영체제가 **실제로 실행**하고 관리하는 대상을 **구체적으로** 의미합니다. +* 어떤 프로그램이든 어떤 코드든, **실행**될 때만 무언가를 **할 수 있습니다**. 즉, **프로세스가 실행 중**일 때입니다. +* 프로세스는 여러분이, 혹은 운영체제가 **종료**(또는 “kill”)할 수 있습니다. 그러면 실행이 멈추고, 더 이상 **아무것도 할 수 없습니다**. +* 컴퓨터에서 실행 중인 각 애플리케이션 뒤에는 프로세스가 있습니다. 실행 중인 프로그램, 각 창 등도 마찬가지입니다. 그리고 컴퓨터가 켜져 있는 동안 보통 많은 프로세스가 **동시에** 실행됩니다. +* **같은 프로그램**의 **여러 프로세스**가 동시에 실행될 수도 있습니다. + +운영체제의 “작업 관리자(task manager)”나 “시스템 모니터(system monitor)”(또는 비슷한 도구)를 확인해 보면, 이런 프로세스가 많이 실행 중인 것을 볼 수 있습니다. + +또 예를 들어, 같은 브라우저 프로그램(Firefox, Chrome, Edge 등)을 실행하는 프로세스가 여러 개 있는 것도 보일 가능성이 큽니다. 보통 탭마다 하나의 프로세스를 실행하고, 그 외에도 추가 프로세스 몇 개가 더 있습니다. + +<img class="shadow" src="/img/deployment/concepts/image01.png"> + +--- + +이제 **프로세스**와 **프로그램**의 차이를 알았으니, 배포에 대한 이야기를 계속해 보겠습니다. + +## 시작 시 실행 { #running-on-startup } + +대부분의 경우 웹 API를 만들면, 클라이언트가 언제나 접근할 수 있도록 **항상 실행**되고 중단되지 않기를 원합니다. 물론 특정 상황에서만 실행하고 싶은 특별한 이유가 있을 수는 있지만, 대부분은 지속적으로 실행되며 **사용 가능**한 상태이기를 원합니다. + +### 원격 서버에서 { #in-a-remote-server } + +원격 서버(클라우드 서버, 가상 머신 등)를 설정할 때, 가장 단순한 방법은 로컬 개발 때처럼 수동으로 `fastapi run`(Uvicorn을 사용합니다)이나 비슷한 명령을 실행하는 것입니다. + +이 방식은 동작하고, **개발 중에는** 유용합니다. + +하지만 서버에 대한 연결이 끊기면, 실행 중인 **프로세스**도 아마 종료될 것입니다. + +또 서버가 재시작되면(예: 업데이트 이후, 혹은 클라우드 제공자의 마이그레이션 이후) 여러분은 아마 **알아차리지 못할** 겁니다. 그 결과, 프로세스를 수동으로 다시 시작해야 한다는 사실도 모르게 됩니다. 그러면 API는 그냥 죽은 상태로 남습니다. 😱 + +### 시작 시 자동 실행 { #run-automatically-on-startup } + +일반적으로 서버 프로그램(예: Uvicorn)은 서버가 시작될 때 자동으로 시작되고, **사람의 개입** 없이도 FastAPI 앱을 실행하는 프로세스가 항상 실행 중이도록(예: FastAPI 앱을 실행하는 Uvicorn) 구성하고 싶을 것입니다. + +### 별도의 프로그램 { #separate-program } + +이를 위해 보통 애플리케이션이 시작 시 실행되도록 보장하는 **별도의 프로그램**을 둡니다. 그리고 많은 경우, 데이터베이스 같은 다른 컴포넌트나 애플리케이션도 함께 실행되도록 보장합니다. + +### 시작 시 실행을 위한 도구 예시 { #example-tools-to-run-at-startup } + +이 역할을 할 수 있는 도구 예시는 다음과 같습니다: + +* Docker +* Kubernetes +* Docker Compose +* Swarm Mode의 Docker +* Systemd +* Supervisor +* 클라우드 제공자가 서비스 일부로 내부적으로 처리 +* 기타... + +다음 장에서 더 구체적인 예시를 제공하겠습니다. + +## 재시작 { #restarts } + +애플리케이션이 시작 시 실행되도록 보장하는 것과 비슷하게, 장애가 발생했을 때 **재시작**되도록 보장하고 싶을 것입니다. + +### 우리는 실수합니다 { #we-make-mistakes } + +사람은 언제나 **실수**합니다. 소프트웨어에는 거의 *항상* 여기저기에 숨은 **버그**가 있습니다. 🐛 + +그리고 개발자는 버그를 발견하고 새로운 기능을 구현하면서 코드를 계속 개선합니다(새로운 버그도 추가할 수 있겠죠 😅). + +### 작은 오류는 자동으로 처리됨 { #small-errors-automatically-handled } + +FastAPI로 웹 API를 만들 때 코드에 오류가 있으면, FastAPI는 보통 그 오류를 발생시킨 단일 요청 안에만 문제를 가둡니다. 🛡 + +클라이언트는 해당 요청에 대해 **500 Internal Server Error**를 받지만, 애플리케이션은 완전히 크래시하지 않고 다음 요청부터는 계속 동작합니다. + +### 더 큰 오류 - 크래시 { #bigger-errors-crashes } + +그럼에도 불구하고, 우리가 작성한 코드가 **전체 애플리케이션을 크래시**시켜 Uvicorn과 Python 자체가 종료되는 경우가 있을 수 있습니다. 💥 + +그래도 한 군데 오류 때문에 애플리케이션이 죽은 채로 남아 있기를 바라지는 않을 것입니다. 망가진 경로 처리를 제외한 나머지 *경로 처리*라도 **계속 실행**되기를 원할 가능성이 큽니다. + +### 크래시 후 재시작 { #restart-after-crash } + +하지만 실행 중인 **프로세스**가 크래시하는 정말 심각한 오류의 경우에는, 적어도 몇 번은 프로세스를 **재시작**하도록 담당하는 외부 컴포넌트가 필요합니다... + +/// tip | 팁 + +...다만 애플리케이션 전체가 **즉시 계속 크래시**한다면, 무한히 재시작하는 것은 아마 의미가 없을 것입니다. 그런 경우에는 개발 중에, 또는 최소한 배포 직후에 알아차릴 가능성이 큽니다. + +그러니 여기서는, 특정한 경우에만 전체가 크래시할 수 있고 **미래**에도 그럴 수 있으며, 그래도 재시작하는 것이 의미 있는 주요 사례에 집중해 봅시다. + +/// + +애플리케이션을 재시작하는 역할은 **외부 컴포넌트**가 맡는 편이 보통 좋습니다. 그 시점에는 Uvicorn과 Python을 포함한 애플리케이션이 이미 크래시했기 때문에, 같은 앱의 같은 코드 안에서 이를 해결할 방법이 없기 때문입니다. + +### 자동 재시작을 위한 도구 예시 { #example-tools-to-restart-automatically } + +대부분의 경우 **시작 시 실행**에 사용한 도구가 자동 **재시작**도 함께 처리합니다. + +예를 들어 다음이 가능합니다: + +* Docker +* Kubernetes +* Docker Compose +* Swarm Mode의 Docker +* Systemd +* Supervisor +* 클라우드 제공자가 서비스 일부로 내부적으로 처리 +* 기타... + +## 복제 - 프로세스와 메모리 { #replication-processes-and-memory } + +FastAPI 애플리케이션은 Uvicorn을 실행하는 `fastapi` 명령 같은 서버 프로그램을 사용하면, **하나의 프로세스**로 실행하더라도 여러 클라이언트를 동시에 처리할 수 있습니다. + +하지만 많은 경우, 여러 워커 프로세스를 동시에 실행하고 싶을 것입니다. + +### 여러 프로세스 - 워커 { #multiple-processes-workers } + +단일 프로세스가 처리할 수 있는 것보다 클라이언트가 더 많고(예: 가상 머신이 그리 크지 않을 때), 서버 CPU에 **여러 코어**가 있다면, 같은 애플리케이션을 실행하는 **여러 프로세스**를 동시에 띄우고 요청을 분산시킬 수 있습니다. + +같은 API 프로그램을 **여러 프로세스**로 실행할 때, 이 프로세스들을 보통 **workers**라고 부릅니다. + +### 워커 프로세스와 포트 { #worker-processes-and-ports } + +[HTTPS에 대한 문서](https.md){.internal-link target=_blank}에서, 서버에서 하나의 포트와 IP 주소 조합에는 하나의 프로세스만 리스닝할 수 있다는 것을 기억하시나요? + +이것은 여전히 사실입니다. + +따라서 **여러 프로세스**를 동시에 실행하려면, 먼저 **포트에서 리스닝하는 단일 프로세스**가 있어야 하고, 그 프로세스가 어떤 방식으로든 각 워커 프로세스로 통신을 전달해야 합니다. + +### 프로세스당 메모리 { #memory-per-process } + +이제 프로그램이 메모리에 무언가를 로드한다고 해봅시다. 예를 들어 머신러닝 모델을 변수에 올리거나 큰 파일 내용을 변수에 올리는 경우입니다. 이런 것들은 서버의 **메모리(RAM)**를 어느 정도 사용합니다. + +그리고 여러 프로세스는 보통 **메모리를 공유하지 않습니다**. 즉, 각 실행 중인 프로세스는 자체 변수와 메모리를 갖습니다. 코드에서 메모리를 많이 사용한다면, **각 프로세스**가 그만큼의 메모리를 사용하게 됩니다. + +### 서버 메모리 { #server-memory } + +예를 들어 코드가 크기 **1 GB**의 머신러닝 모델을 로드한다고 해봅시다. API를 프로세스 하나로 실행하면 RAM을 최소 1GB 사용합니다. 그리고 **4개 프로세스**(워커 4개)를 시작하면 각각 1GB RAM을 사용합니다. 즉 총 **4 GB RAM**을 사용합니다. + +그런데 원격 서버나 가상 머신의 RAM이 3GB뿐이라면, 4GB를 넘게 로드하려고 할 때 문제가 생깁니다. 🚨 + +### 여러 프로세스 - 예시 { #multiple-processes-an-example } + +이 예시에서는 **Manager Process**가 두 개의 **Worker Processes**를 시작하고 제어합니다. + +이 Manager Process는 아마 IP의 **포트**에서 리스닝하는 역할을 합니다. 그리고 모든 통신을 워커 프로세스로 전달합니다. + +워커 프로세스들이 실제로 애플리케이션을 실행하며, **요청**을 받아 **응답**을 반환하는 주요 연산을 수행하고, RAM에 변수로 로드한 모든 내용을 담습니다. + +<img src="/img/deployment/concepts/process-ram.drawio.svg"> + +그리고 물론 같은 머신에는 애플리케이션 외에도 **다른 프로세스**들이 실행 중일 가능성이 큽니다. + +흥미로운 점은 각 프로세스의 **CPU 사용률**은 시간에 따라 크게 **변동**할 수 있지만, **메모리(RAM)**는 보통 대체로 **안정적**으로 유지된다는 것입니다. + +매번 비슷한 양의 연산을 수행하는 API이고 클라이언트가 많다면, **CPU 사용률**도 (급격히 오르내리기보다는) *안정적일* 가능성이 큽니다. + +### 복제 도구와 전략 예시 { #examples-of-replication-tools-and-strategies } + +이를 달성하는 접근 방식은 여러 가지가 있을 수 있으며, 다음 장들에서 Docker와 컨테이너를 설명할 때 구체적인 전략을 더 알려드리겠습니다. + +고려해야 할 주요 제약은 **공개 IP**의 **포트**를 처리하는 **단일** 컴포넌트가 있어야 한다는 점입니다. 그리고 그 컴포넌트는 복제된 **프로세스/워커**로 통신을 **전달**할 방법이 있어야 합니다. + +가능한 조합과 전략 몇 가지는 다음과 같습니다: + +* `--workers` 옵션을 사용한 **Uvicorn** + * 하나의 Uvicorn **프로세스 매니저**가 **IP**와 **포트**에서 리스닝하고, **여러 Uvicorn 워커 프로세스**를 시작합니다. +* **Kubernetes** 및 기타 분산 **컨테이너 시스템** + * **Kubernetes** 레이어의 무언가가 **IP**와 **포트**에서 리스닝합니다. 그리고 **여러 컨테이너**를 두어 복제하며, 각 컨테이너에는 **하나의 Uvicorn 프로세스**가 실행됩니다. +* 이를 대신 처리해주는 **클라우드 서비스** + * 클라우드 서비스가 **복제를 대신 처리**해줄 가능성이 큽니다. 실행할 **프로세스**나 사용할 **컨테이너 이미지**를 정의하게 해줄 수도 있지만, 어떤 경우든 대개 **단일 Uvicorn 프로세스**를 기준으로 하고, 클라우드 서비스가 이를 복제하는 역할을 맡습니다. + +/// tip | 팁 + +**컨테이너**, Docker, Kubernetes에 대한 일부 내용이 아직은 잘 이해되지 않아도 괜찮습니다. + +다음 장에서 컨테이너 이미지, Docker, Kubernetes 등을 더 설명하겠습니다: [컨테이너에서 FastAPI - Docker](docker.md){.internal-link target=_blank}. + +/// + +## 시작 전 사전 단계 { #previous-steps-before-starting } + +애플리케이션을 **시작하기 전에** 어떤 단계를 수행하고 싶은 경우가 많습니다. + +예를 들어 **데이터베이스 마이그레이션**을 실행하고 싶을 수 있습니다. + +하지만 대부분의 경우, 이런 단계는 **한 번만** 수행하고 싶을 것입니다. + +그래서 애플리케이션을 시작하기 전에 그 **사전 단계**를 수행할 **단일 프로세스**를 두고 싶을 것입니다. + +또한 이후에 애플리케이션 자체를 **여러 프로세스**(여러 워커)로 시작하더라도, 사전 단계를 수행하는 프로세스는 *반드시* 하나만 실행되도록 해야 합니다. 만약 사전 단계를 **여러 프로세스**가 수행하면, **병렬로** 실행하면서 작업이 **중복**될 수 있습니다. 그리고 데이터베이스 마이그레이션처럼 민감한 작업이라면 서로 충돌을 일으킬 수 있습니다. + +물론 사전 단계를 여러 번 실행해도 문제가 없는 경우도 있습니다. 그런 경우에는 처리하기가 훨씬 쉽습니다. + +/// tip | 팁 + +또한 설정에 따라, 어떤 경우에는 애플리케이션을 시작하기 전에 **사전 단계가 전혀 필요 없을** 수도 있다는 점을 기억하세요. + +그런 경우에는 이런 것들을 전혀 걱정할 필요가 없습니다. 🤷 + +/// + +### 사전 단계 전략 예시 { #examples-of-previous-steps-strategies } + +이는 여러분이 **시스템을 배포하는 방식**에 크게 좌우되며, 프로그램을 시작하는 방식, 재시작 처리 방식 등과도 연결되어 있을 가능성이 큽니다. + +가능한 아이디어는 다음과 같습니다: + +* 앱 컨테이너보다 먼저 실행되는 Kubernetes의 “Init Container” +* 사전 단계를 실행한 다음 애플리케이션을 시작하는 bash 스크립트 + * 이 bash 스크립트를 시작/재시작하고, 오류를 감지하는 등의 방법도 여전히 필요합니다. + +/// tip | 팁 + +컨테이너로 이를 처리하는 더 구체적인 예시는 다음 장에서 제공하겠습니다: [컨테이너에서 FastAPI - Docker](docker.md){.internal-link target=_blank}. + +/// + +## 리소스 활용 { #resource-utilization } + +서버는 여러분이 프로그램으로 소비하거나 **활용(utilize)**할 수 있는 **리소스**입니다. CPU의 계산 시간과 사용 가능한 RAM 메모리가 대표적입니다. + +시스템 리소스를 얼마나 소비/활용하고 싶으신가요? “많지 않게”라고 생각하기 쉽지만, 실제로는 **크래시하지 않는 선에서 가능한 한 많이** 사용하고 싶을 가능성이 큽니다. + +서버 3대를 비용을 내고 쓰고 있는데 RAM과 CPU를 조금만 사용한다면, 아마 **돈을 낭비**하고 💸, **서버 전력도 낭비**하고 🌎, 기타 등등이 될 수 있습니다. + +그 경우에는 서버를 2대만 두고, 각 서버의 리소스(CPU, 메모리, 디스크, 네트워크 대역폭 등)를 더 높은 비율로 사용하는 것이 더 나을 수 있습니다. + +반대로 서버 2대를 두고 CPU와 RAM을 **100%** 사용하고 있다면, 어느 시점에 프로세스 하나가 더 많은 메모리를 요청하게 되고, 서버는 디스크를 “메모리”처럼 사용해야 할 수도 있습니다(수천 배 느릴 수 있습니다). 또는 심지어 **크래시**할 수도 있습니다. 혹은 어떤 프로세스가 계산을 해야 하는데 CPU가 다시 비워질 때까지 기다려야 할 수도 있습니다. + +이 경우에는 **서버 한 대를 추가**로 확보하고 일부 프로세스를 그쪽에서 실행해, 모두가 **충분한 RAM과 CPU 시간**을 갖도록 하는 편이 더 낫습니다. + +또 어떤 이유로 API 사용량이 **급증(spike)**할 가능성도 있습니다. 바이럴이 되었거나, 다른 서비스나 봇이 사용하기 시작했을 수도 있습니다. 그런 경우를 대비해 추가 리소스를 확보해두고 싶을 수 있습니다. + +리소스 활용률 목표로 **임의의 수치**를 정할 수 있습니다. 예를 들어 **50%에서 90% 사이**처럼요. 요점은, 이런 것들이 배포를 조정할 때 측정하고 튜닝하는 주요 지표가 될 가능성이 크다는 것입니다. + +`htop` 같은 간단한 도구로 서버의 CPU와 RAM 사용량, 또는 각 프로세스별 사용량을 볼 수 있습니다. 혹은 서버 여러 대에 분산될 수도 있는 더 복잡한 모니터링 도구를 사용할 수도 있습니다. + +## 요약 { #recap } + +여기까지 애플리케이션 배포 방식을 결정할 때 염두에 두어야 할 주요 개념들을 읽었습니다: + +* 보안 - HTTPS +* 시작 시 실행 +* 재시작 +* 복제(실행 중인 프로세스 수) +* 메모리 +* 시작 전 사전 단계 + +이 아이디어들을 이해하고 적용하는 방법을 알면, 배포를 구성하고 조정할 때 필요한 직관을 얻는 데 도움이 될 것입니다. 🤓 + +다음 섹션에서는 따라 할 수 있는 가능한 전략의 더 구체적인 예시를 제공하겠습니다. 🚀 diff --git a/docs/ko/docs/deployment/fastapicloud.md b/docs/ko/docs/deployment/fastapicloud.md new file mode 100644 index 000000000..9a830b157 --- /dev/null +++ b/docs/ko/docs/deployment/fastapicloud.md @@ -0,0 +1,65 @@ +# FastAPI Cloud { #fastapi-cloud } + +**한 번의 명령**으로 FastAPI 앱을 <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>에 배포할 수 있습니다. 아직이라면 대기자 명단에 등록해 보세요. 🚀 + +## 로그인하기 { #login } + +먼저 **FastAPI Cloud** 계정이 이미 있는지 확인하세요(대기자 명단에서 초대해 드렸을 거예요 😉). + +그다음 로그인합니다: + +<div class="termy"> + +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +</div> + +## 배포하기 { #deploy } + +이제 **한 번의 명령**으로 앱을 배포합니다: + +<div class="termy"> + +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +</div> + +이게 전부입니다! 이제 해당 URL에서 앱에 접근할 수 있습니다. ✨ + +## FastAPI Cloud 소개 { #about-fastapi-cloud } + +**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>**는 **FastAPI**를 만든 동일한 저자와 팀이 구축했습니다. + +최소한의 노력으로 API를 **구축**, **배포**, **접근**하는 과정을 간소화합니다. + +FastAPI로 앱을 만들 때의 동일한 **개발자 경험**을, 클라우드에 **배포**할 때도 제공합니다. 🎉 + +또한 앱을 배포할 때 보통 필요한 대부분의 것들도 처리해 줍니다. 예를 들면: + +* HTTPS +* 요청을 기반으로 자동 스케일링하는 복제(Replication) +* 등 + +FastAPI Cloud는 *FastAPI and friends* 오픈 소스 프로젝트의 주요 스폰서이자 자금 지원 제공자입니다. ✨ + +## 다른 클라우드 제공업체에 배포하기 { #deploy-to-other-cloud-providers } + +FastAPI는 오픈 소스이며 표준을 기반으로 합니다. 원하는 어떤 클라우드 제공업체에도 FastAPI 앱을 배포할 수 있습니다. + +해당 클라우드 제공업체의 가이드를 따라 FastAPI 앱을 배포하세요. 🤓 + +## 자체 서버에 배포하기 { #deploy-your-own-server } + +또한 이 **Deployment** 가이드에서 이후에 모든 세부사항을 알려드릴 거예요. 그래서 무슨 일이 일어나고 있는지, 무엇이 필요하며, 본인의 서버를 포함해 직접 FastAPI 앱을 어떻게 배포하는지까지 이해할 수 있게 될 것입니다. 🤓 diff --git a/docs/ko/docs/deployment/https.md b/docs/ko/docs/deployment/https.md new file mode 100644 index 000000000..888ec6159 --- /dev/null +++ b/docs/ko/docs/deployment/https.md @@ -0,0 +1,231 @@ +# HTTPS 알아보기 { #about-https } + +HTTPS는 그냥 “켜져 있거나” 아니면 “꺼져 있는” 것이라고 생각하기 쉽습니다. + +하지만 실제로는 훨씬 더 복잡합니다. + +/// tip | 팁 + +바쁘거나 별로 신경 쓰고 싶지 않다면, 다음 섹션에서 다양한 기법으로 모든 것을 설정하는 단계별 안내를 계속 보세요. + +/// + +소비자 관점에서 **HTTPS의 기본을 배우려면** <a href="https://howhttps.works/" class="external-link" target="_blank">https://howhttps.works/</a>를 확인하세요. + +이제 **개발자 관점**에서 HTTPS를 생각할 때 염두에 두어야 할 여러 가지가 있습니다: + +* HTTPS를 사용하려면, **서버**가 **제3자**가 발급한 **"인증서(certificates)"**를 **보유**해야 합니다. + * 이 인증서는 실제로 제3자가 “생성”해 주는 것이고, 서버가 만드는 것이 아니라 제3자로부터 **발급/획득**하는 것입니다. +* 인증서에는 **유효 기간**이 있습니다. + * 즉, **만료**됩니다. + * 그리고 나면 제3자로부터 다시 **갱신**해서 **재발급/재획득**해야 합니다. +* 연결의 암호화는 **TCP 레벨**에서 일어납니다. + * 이는 **HTTP보다 한 계층 아래**입니다. + * 따라서 **인증서와 암호화** 처리는 **HTTP 이전**에 수행됩니다. +* **TCP는 "도메인"을 모릅니다.** IP 주소만 압니다. + * 어떤 **특정 도메인**을 요청했는지에 대한 정보는 **HTTP 데이터**에 들어 있습니다. +* **HTTPS 인증서**는 특정 **도메인**을 “인증”하지만, 프로토콜과 암호화는 TCP 레벨에서 일어나며, 어떤 도메인을 다루는지 **알기 전에** 처리됩니다. +* **기본적으로** 이는 IP 주소 하나당 **HTTPS 인증서 하나만** 둘 수 있다는 뜻입니다. + * 서버가 아무리 크든, 그 위에 올린 각 애플리케이션이 아무리 작든 상관없습니다. + * 하지만 이에 대한 **해결책**이 있습니다. +* **TLS** 프로토콜(HTTP 이전, TCP 레벨에서 암호화를 처리하는 것)에 대한 **확장** 중에 **<a href="https://en.wikipedia.org/wiki/Server_Name_Indication" class="external-link" target="_blank"><abbr title="Server Name Indication - 서버 이름 표시">SNI</abbr></a>**라는 것이 있습니다. + * 이 SNI 확장을 사용하면, 단일 서버(**단일 IP 주소**)에서 **여러 HTTPS 인증서**를 사용하고 **여러 HTTPS 도메인/애플리케이션**을 제공할 수 있습니다. + * 이를 위해서는 서버에서 **공개 IP 주소**로 리스닝하는 **하나의** 컴포넌트(프로그램)가 서버에 있는 **모든 HTTPS 인증서**에 접근할 수 있어야 합니다. +* 보안 연결을 얻은 **이후에도**, 통신 프로토콜 자체는 **여전히 HTTP**입니다. + * **HTTP 프로토콜**로 전송되더라도, 내용은 **암호화**되어 있습니다. + +일반적으로 서버(머신, 호스트 등)에는 **프로그램/HTTP 서버 하나**를 실행해 **HTTPS 관련 부분 전체**를 관리하게 합니다: **암호화된 HTTPS 요청**을 받고, 복호화된 **HTTP 요청**을 같은 서버에서 실행 중인 실제 HTTP 애플리케이션(이 경우 **FastAPI** 애플리케이션)으로 전달하고, 애플리케이션의 **HTTP 응답**을 받아 적절한 **HTTPS 인증서**로 **암호화**한 뒤 **HTTPS**로 클라이언트에 다시 보내는 역할입니다. 이런 서버를 흔히 **<a href="https://en.wikipedia.org/wiki/TLS_termination_proxy" class="external-link" target="_blank">TLS Termination Proxy</a>**라고 부릅니다. + +TLS Termination Proxy로 사용할 수 있는 옵션은 다음과 같습니다: + +* Traefik (인증서 갱신도 처리 가능) +* Caddy (인증서 갱신도 처리 가능) +* Nginx +* HAProxy + +## Let's Encrypt { #lets-encrypt } + +Let's Encrypt 이전에는 이러한 **HTTPS 인증서**가 신뢰할 수 있는 제3자에 의해 판매되었습니다. + +인증서를 획득하는 과정은 번거롭고, 꽤 많은 서류 작업이 필요했으며, 인증서도 상당히 비쌌습니다. + +하지만 그 후 **<a href="https://letsencrypt.org/" class="external-link" target="_blank">Let's Encrypt</a>**가 만들어졌습니다. + +이는 Linux Foundation의 프로젝트입니다. 표준 암호학적 보안을 모두 사용하는 **HTTPS 인증서**를 **무료로**, 자동화된 방식으로 제공합니다. 이 인증서들은 수명이 짧고(약 3개월) 그래서 유효 기간이 짧은 만큼 **실제로 보안이 더 좋아지기도** 합니다. + +도메인은 안전하게 검증되며 인증서는 자동으로 생성됩니다. 또한 이로 인해 인증서 갱신도 자동화할 수 있습니다. + +목표는 인증서의 발급과 갱신을 자동화하여 **무료로, 영구히, 안전한 HTTPS**를 사용할 수 있게 하는 것입니다. + +## 개발자를 위한 HTTPS { #https-for-developers } + +개발자에게 중요한 개념들을 중심으로, HTTPS API가 단계별로 어떻게 보일 수 있는지 예시를 들어 보겠습니다. + +### 도메인 이름 { #domain-name } + +아마도 시작은 **도메인 이름**을 **획득**하는 것일 겁니다. 그 다음 DNS 서버(아마 같은 클라우드 제공업체)에서 이를 설정합니다. + +대개 클라우드 서버(가상 머신) 같은 것을 사용하게 되고, 거기에는 <abbr title="That doesn't change - 변하지 않음">fixed</abbr> **공개 IP 주소**가 있습니다. + +DNS 서버(들)에서 **도메인**이 서버의 **공개 IP 주소**를 가리키도록 레코드(“`A record`”)를 설정합니다. + +보통은 처음 한 번, 모든 것을 설정할 때만 이 작업을 합니다. + +/// tip | 팁 + +도메인 이름 부분은 HTTPS보다 훨씬 이전 단계지만, 모든 것이 도메인과 IP 주소에 의존하므로 여기서 언급할 가치가 있습니다. + +/// + +### DNS { #dns } + +이제 실제 HTTPS 부분에 집중해 보겠습니다. + +먼저 브라우저는 **DNS 서버**에 질의하여, 여기서는 `someapp.example.com`이라는 **도메인에 대한 IP**가 무엇인지 확인합니다. + +DNS 서버는 브라우저에게 특정 **IP 주소**를 사용하라고 알려줍니다. 이는 DNS 서버에 설정해 둔, 서버가 사용하는 공개 IP 주소입니다. + +<img src="/img/deployment/https/https01.drawio.svg"> + +### TLS 핸드셰이크 시작 { #tls-handshake-start } + +그 다음 브라우저는 **포트 443**(HTTPS 포트)에서 해당 IP 주소와 통신합니다. + +통신의 첫 부분은 클라이언트와 서버 사이의 연결을 설정하고, 사용할 암호화 키 등을 결정하는 과정입니다. + +<img src="/img/deployment/https/https02.drawio.svg"> + +클라이언트와 서버가 TLS 연결을 설정하기 위해 상호작용하는 이 과정을 **TLS 핸드셰이크**라고 합니다. + +### SNI 확장을 사용하는 TLS { #tls-with-sni-extension } + +서버에서는 특정 **IP 주소**의 특정 **포트**에서 **하나의 프로세스만** 리스닝할 수 있습니다. 같은 IP 주소에서 다른 포트로 리스닝하는 프로세스는 있을 수 있지만, IP 주소와 포트 조합마다 하나만 가능합니다. + +TLS(HTTPS)는 기본적으로 특정 포트 `443`을 사용합니다. 따라서 우리가 필요한 포트는 이것입니다. + +이 포트에서 하나의 프로세스만 리스닝할 수 있으므로, 그 역할을 하는 프로세스는 **TLS Termination Proxy**가 됩니다. + +TLS Termination Proxy는 하나 이상의 **TLS 인증서**(HTTPS 인증서)에 접근할 수 있습니다. + +앞에서 설명한 **SNI 확장**을 사용해, TLS Termination Proxy는 이 연결에 사용할 수 있는 TLS(HTTPS) 인증서들 중에서 클라이언트가 기대하는 도메인과 일치하는 것을 확인해 선택합니다. + +이 경우에는 `someapp.example.com`에 대한 인증서를 사용합니다. + +<img src="/img/deployment/https/https03.drawio.svg"> + +클라이언트는 이미 해당 TLS 인증서를 생성한 주체(여기서는 Let's Encrypt이지만, 이는 뒤에서 다시 보겠습니다)를 **신뢰**하므로, 인증서가 유효한지 **검증**할 수 있습니다. + +그 다음 인증서를 사용해 클라이언트와 TLS Termination Proxy는 나머지 **TCP 통신**을 어떻게 **암호화할지 결정**합니다. 이로써 **TLS 핸드셰이크** 단계가 완료됩니다. + +이후 클라이언트와 서버는 TLS가 제공하는 **암호화된 TCP 연결**을 갖게 됩니다. 그리고 그 연결을 사용해 실제 **HTTP 통신**을 시작할 수 있습니다. + +이것이 바로 **HTTPS**입니다. 순수(암호화되지 않은) TCP 연결 대신 **안전한 TLS 연결** 안에서 **HTTP**를 그대로 사용하는 것입니다. + +/// tip | 팁 + +통신의 암호화는 HTTP 레벨이 아니라 **TCP 레벨**에서 일어난다는 점에 주의하세요. + +/// + +### HTTPS 요청 { #https-request } + +이제 클라이언트와 서버(구체적으로는 브라우저와 TLS Termination Proxy)가 **암호화된 TCP 연결**을 갖게 되었으니 **HTTP 통신**을 시작할 수 있습니다. + +따라서 클라이언트는 **HTTPS 요청**을 보냅니다. 이는 암호화된 TLS 연결을 통해 전달되는 HTTP 요청일 뿐입니다. + +<img src="/img/deployment/https/https04.drawio.svg"> + +### 요청 복호화 { #decrypt-the-request } + +TLS Termination Proxy는 합의된 암호화를 사용해 **요청을 복호화**하고, 애플리케이션을 실행 중인 프로세스(예: FastAPI 애플리케이션을 실행하는 Uvicorn 프로세스)에 **일반(복호화된) HTTP 요청**을 전달합니다. + +<img src="/img/deployment/https/https05.drawio.svg"> + +### HTTP 응답 { #http-response } + +애플리케이션은 요청을 처리하고 **일반(암호화되지 않은) HTTP 응답**을 TLS Termination Proxy로 보냅니다. + +<img src="/img/deployment/https/https06.drawio.svg"> + +### HTTPS 응답 { #https-response } + +그 다음 TLS Termination Proxy는 이전에 합의한 암호화( `someapp.example.com` 인증서로 시작된 것)를 사용해 **응답을 암호화**하고, 브라우저로 다시 보냅니다. + +이후 브라우저는 응답이 유효한지, 올바른 암호화 키로 암호화되었는지 등을 확인합니다. 그런 다음 **응답을 복호화**하고 처리합니다. + +<img src="/img/deployment/https/https07.drawio.svg"> + +클라이언트(브라우저)는 앞서 **HTTPS 인증서**로 합의한 암호화를 사용하고 있으므로, 해당 응답이 올바른 서버에서 왔다는 것을 알 수 있습니다. + +### 여러 애플리케이션 { #multiple-applications } + +같은 서버(또는 여러 서버)에는 예를 들어 다른 API 프로그램이나 데이터베이스처럼 **여러 애플리케이션**이 있을 수 있습니다. + +특정 IP와 포트 조합은 하나의 프로세스만 처리할 수 있지만(예시에서는 TLS Termination Proxy), 다른 애플리케이션/프로세스도 **공개 IP와 포트 조합**을 동일하게 쓰려고만 하지 않는다면 서버에서 함께 실행될 수 있습니다. + +<img src="/img/deployment/https/https08.drawio.svg"> + +이렇게 하면 TLS Termination Proxy가 **여러 도메인**에 대한 HTTPS와 인증서를 **여러 애플리케이션**에 대해 처리하고, 각 경우에 맞는 애플리케이션으로 요청을 전달할 수 있습니다. + +### 인증서 갱신 { #certificate-renewal } + +미래의 어느 시점에는 각 인증서가 **만료**됩니다(획득 후 약 3개월). + +그 다음에는 또 다른 프로그램(경우에 따라 별도 프로그램일 수도 있고, 경우에 따라 같은 TLS Termination Proxy일 수도 있습니다)이 Let's Encrypt와 통신하여 인증서를 갱신합니다. + +<img src="/img/deployment/https/https.drawio.svg"> + +**TLS 인증서**는 IP 주소가 아니라 **도메인 이름**과 **연결**되어 있습니다. + +따라서 인증서를 갱신하려면, 갱신 프로그램이 권한 기관(Let's Encrypt)에게 해당 도메인을 실제로 **“소유”하고 제어하고 있음**을 **증명**해야 합니다. + +이를 위해, 그리고 다양한 애플리케이션 요구를 수용하기 위해 여러 방법이 있습니다. 널리 쓰이는 방법은 다음과 같습니다: + +* **일부 DNS 레코드 수정**. + * 이를 위해서는 갱신 프로그램이 DNS 제공업체의 API를 지원해야 하므로, 사용하는 DNS 제공업체에 따라 가능할 수도, 아닐 수도 있습니다. +* 도메인과 연결된 공개 IP 주소에서 **서버로 실행**(적어도 인증서 발급 과정 동안). + * 앞에서 말했듯 특정 IP와 포트에서는 하나의 프로세스만 리스닝할 수 있습니다. + * 이것이 동일한 TLS Termination Proxy가 인증서 갱신 과정까지 처리할 때 매우 유용한 이유 중 하나입니다. + * 그렇지 않으면 TLS Termination Proxy를 잠시 중지하고, 갱신 프로그램을 시작해 인증서를 획득한 다음, TLS Termination Proxy에 인증서를 설정하고, 다시 TLS Termination Proxy를 재시작해야 할 수도 있습니다. 이는 TLS Termination Proxy가 꺼져 있는 동안 앱(들)을 사용할 수 없으므로 이상적이지 않습니다. + +앱을 계속 제공하면서 이 갱신 과정을 처리할 수 있는 것은, 애플리케이션 서버(예: Uvicorn)에서 TLS 인증서를 직접 쓰는 대신 TLS Termination Proxy로 HTTPS를 처리하는 **별도의 시스템**을 두고 싶어지는 주요 이유 중 하나입니다. + +## 프록시 전달 헤더 { #proxy-forwarded-headers } + +프록시를 사용해 HTTPS를 처리할 때, **애플리케이션 서버**(예: FastAPI CLI를 통한 Uvicorn)는 HTTPS 과정에 대해 아무것도 알지 못하고 **TLS Termination Proxy**와는 일반 HTTP로 통신합니다. + +이 **프록시**는 보통 요청을 **애플리케이션 서버**에 전달하기 전에, 요청이 프록시에 의해 **전달(forwarded)**되고 있음을 애플리케이션 서버가 알 수 있도록 일부 HTTP 헤더를 즉석에서 설정합니다. + +/// note | 기술 세부사항 + +프록시 헤더는 다음과 같습니다: + +* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For" class="external-link" target="_blank">X-Forwarded-For</a> +* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto" class="external-link" target="_blank">X-Forwarded-Proto</a> +* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host" class="external-link" target="_blank">X-Forwarded-Host</a> + +/// + +그럼에도 불구하고 **애플리케이션 서버**는 자신이 신뢰할 수 있는 **프록시** 뒤에 있다는 것을 모르므로, 기본적으로는 그 헤더들을 신뢰하지 않습니다. + +하지만 **애플리케이션 서버**가 **프록시**가 보낸 *forwarded* 헤더를 신뢰하도록 설정할 수 있습니다. FastAPI CLI를 사용하고 있다면, *CLI Option* `--forwarded-allow-ips`를 사용해 어떤 IP에서 온 *forwarded* 헤더를 신뢰할지 지정할 수 있습니다. + +예를 들어 **애플리케이션 서버**가 신뢰하는 **프록시**로부터만 통신을 받는다면, `--forwarded-allow-ips="*"`로 설정해 들어오는 모든 IP를 신뢰하게 할 수 있습니다. 어차피 **프록시**가 사용하는 IP에서만 요청을 받게 될 것이기 때문입니다. + +이렇게 하면 애플리케이션은 자신이 사용하는 공개 URL이 무엇인지, HTTPS를 사용하는지, 도메인이 무엇인지 등을 알 수 있습니다. + +예를 들어 리다이렉트를 올바르게 처리하는 데 유용합니다. + +/// tip | 팁 + +이에 대해서는 [프록시 뒤에서 실행하기 - 프록시 전달 헤더 활성화](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} 문서에서 더 알아볼 수 있습니다. + +/// + +## 요약 { #recap } + +**HTTPS**는 매우 중요하며, 대부분의 경우 상당히 **핵심적**입니다. 개발자가 HTTPS와 관련해 해야 하는 노력의 대부분은 결국 **이 개념들을 이해**하고 그것들이 어떻게 동작하는지 파악하는 것입니다. + +하지만 **개발자를 위한 HTTPS**의 기본 정보를 알고 나면, 여러 도구를 쉽게 조합하고 설정하여 모든 것을 간단하게 관리할 수 있습니다. + +다음 장들에서는 **FastAPI** 애플리케이션을 위한 **HTTPS** 설정 방법을 여러 구체적인 예시로 보여드리겠습니다. 🔒 diff --git a/docs/ko/docs/deployment/manually.md b/docs/ko/docs/deployment/manually.md new file mode 100644 index 000000000..e85dd02a3 --- /dev/null +++ b/docs/ko/docs/deployment/manually.md @@ -0,0 +1,157 @@ +# 서버를 수동으로 실행하기 { #run-a-server-manually } + +## `fastapi run` 명령 사용하기 { #use-the-fastapi-run-command } + +요약하면, `fastapi run`을 사용해 FastAPI 애플리케이션을 서비스하세요: + +<div class="termy"> + +```console +$ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid">main.py</u> + + <span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting production server 🚀 + + Searching for package file structure from directories + with <font color="#3465A4">__init__.py</font> files + Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py + + <span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with + the following code: + + <u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> + + <span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font> + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000/docs</u></font> + + Logs: + + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>2306215</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font> <b>(</b>Press CTRL+C + to quit<b>)</b> +``` + +</div> + +대부분의 경우에는 이것으로 동작합니다. 😎 + +예를 들어 이 명령은 컨테이너나 서버 등에서 **FastAPI** 앱을 시작할 때 사용할 수 있습니다. + +## ASGI 서버 { #asgi-servers } + +이제 조금 더 자세히 살펴보겠습니다. + +FastAPI는 <abbr title="Asynchronous Server Gateway Interface">ASGI</abbr>라고 불리는, Python 웹 프레임워크와 서버를 만들기 위한 표준을 사용합니다. FastAPI는 ASGI 웹 프레임워크입니다. + +원격 서버 머신에서 **FastAPI** 애플리케이션(또는 다른 ASGI 애플리케이션)을 실행하기 위해 필요한 핵심 요소는 **Uvicorn** 같은 ASGI 서버 프로그램입니다. `fastapi` 명령에는 기본으로 이것이 포함되어 있습니다. + +다음을 포함해 여러 대안이 있습니다: + +* <a href="https://www.uvicorn.dev/" class="external-link" target="_blank">Uvicorn</a>: 고성능 ASGI 서버. +* <a href="https://hypercorn.readthedocs.io/" class="external-link" target="_blank">Hypercorn</a>: HTTP/2 및 Trio 등 여러 기능과 호환되는 ASGI 서버. +* <a href="https://github.com/django/daphne" class="external-link" target="_blank">Daphne</a>: Django Channels를 위해 만들어진 ASGI 서버. +* <a href="https://github.com/emmett-framework/granian" class="external-link" target="_blank">Granian</a>: Python 애플리케이션을 위한 Rust HTTP 서버. +* <a href="https://unit.nginx.org/howto/fastapi/" class="external-link" target="_blank">NGINX Unit</a>: NGINX Unit은 가볍고 다용도로 사용할 수 있는 웹 애플리케이션 런타임입니다. + +## 서버 머신과 서버 프로그램 { #server-machine-and-server-program } + +이름에 관해 기억해 둘 작은 디테일이 있습니다. 💡 + +"**server**"라는 단어는 보통 원격/클라우드 컴퓨터(물리 또는 가상 머신)와, 그 머신에서 실행 중인 프로그램(예: Uvicorn) 둘 다를 가리키는 데 사용됩니다. + +일반적으로 "server"를 읽을 때, 이 두 가지 중 하나를 의미할 수 있다는 점을 기억하세요. + +원격 머신을 가리킬 때는 **server**라고 부르는 것이 일반적이지만, **machine**, **VM**(virtual machine), **node**라고 부르기도 합니다. 이것들은 보통 Linux를 실행하는 원격 머신의 한 형태를 뜻하며, 그곳에서 프로그램을 실행합니다. + +## 서버 프로그램 설치하기 { #install-the-server-program } + +FastAPI를 설치하면 프로덕션 서버인 Uvicorn이 함께 설치되며, `fastapi run` 명령으로 시작할 수 있습니다. + +하지만 ASGI 서버를 수동으로 설치할 수도 있습니다. + +[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 만들고 활성화한 다음, 서버 애플리케이션을 설치하세요. + +예를 들어 Uvicorn을 설치하려면: + +<div class="termy"> + +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +</div> + +다른 어떤 ASGI 서버 프로그램도 비슷한 과정이 적용됩니다. + +/// tip | 팁 + +`standard`를 추가하면 Uvicorn이 권장되는 추가 의존성 몇 가지를 설치하고 사용합니다. + +여기에는 `asyncio`를 고성능으로 대체할 수 있는 드롭인 대체재인 `uvloop`가 포함되며, 큰 동시성 성능 향상을 제공합니다. + +`pip install "fastapi[standard]"` 같은 방식으로 FastAPI를 설치하면 `uvicorn[standard]`도 함께 설치됩니다. + +/// + +## 서버 프로그램 실행하기 { #run-the-server-program } + +ASGI 서버를 수동으로 설치했다면, 보통 FastAPI 애플리케이션을 임포트하기 위해 특별한 형식의 import string을 전달해야 합니다: + +<div class="termy"> + +```console +$ uvicorn main:app --host 0.0.0.0 --port 80 + +<span style="color: green;">INFO</span>: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) +``` + +</div> + +/// note | 참고 + +`uvicorn main:app` 명령은 다음을 가리킵니다: + +* `main`: 파일 `main.py`(Python "module"). +* `app`: `main.py` 안에서 `app = FastAPI()` 라인으로 생성된 객체. + +이는 다음과 동일합니다: + +```Python +from main import app +``` + +/// + +각 ASGI 서버 프로그램의 대안도 비슷한 명령을 갖고 있으며, 자세한 내용은 각자의 문서를 참고하세요. + +/// warning | 경고 + +Uvicorn과 다른 서버는 개발 중에 유용한 `--reload` 옵션을 지원합니다. + +`--reload` 옵션은 훨씬 더 많은 리소스를 소비하고, 더 불안정합니다. + +**개발** 중에는 큰 도움이 되지만, **프로덕션**에서는 사용하지 **말아야** 합니다. + +/// + +## 배포 개념 { #deployment-concepts } + +이 예제들은 서버 프로그램(예: Uvicorn)을 실행하여 **단일 프로세스**를 시작하고, 사전에 정한 포트(예: `80`)에서 모든 IP(`0.0.0.0`)로 들어오는 요청을 받도록 합니다. + +이것이 기본 아이디어입니다. 하지만 보통은 다음과 같은 추가 사항들도 처리해야 합니다: + +* 보안 - HTTPS +* 시작 시 자동 실행 +* 재시작 +* 복제(실행 중인 프로세스 수) +* 메모리 +* 시작 전 선행 단계 + +다음 장들에서 이 각각의 개념을 어떻게 생각해야 하는지, 그리고 이를 다루기 위한 전략의 구체적인 예시를 더 알려드리겠습니다. 🚀 diff --git a/docs/ko/docs/how-to/authentication-error-status-code.md b/docs/ko/docs/how-to/authentication-error-status-code.md new file mode 100644 index 000000000..47120cae6 --- /dev/null +++ b/docs/ko/docs/how-to/authentication-error-status-code.md @@ -0,0 +1,17 @@ +# 이전 403 인증 오류 상태 코드 사용하기 { #use-old-403-authentication-error-status-codes } + +FastAPI 버전 `0.122.0` 이전에는, 통합 보안 유틸리티가 인증 실패 후 클라이언트에 오류를 반환할 때 HTTP 상태 코드 `403 Forbidden`을 사용했습니다. + +FastAPI 버전 `0.122.0`부터는 더 적절한 HTTP 상태 코드 `401 Unauthorized`를 사용하며, HTTP 명세인 <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-3.1" class="external-link" target="_blank">RFC 7235</a>, <a href="https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized" class="external-link" target="_blank">RFC 9110</a>를 따라 응답에 합리적인 `WWW-Authenticate` 헤더를 반환합니다. + +하지만 어떤 이유로든 클라이언트가 이전 동작에 의존하고 있다면, 보안 클래스에서 `make_not_authenticated_error` 메서드를 오버라이드하여 이전 동작으로 되돌릴 수 있습니다. + +예를 들어, 기본값인 `401 Unauthorized` 오류 대신 `403 Forbidden` 오류를 반환하는 `HTTPBearer`의 서브클래스를 만들 수 있습니다: + +{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *} + +/// tip | 팁 + +함수는 예외를 `raise`하는 것이 아니라 예외 인스턴스를 `return`한다는 점에 유의하세요. 예외를 발생시키는(`raise`) 작업은 내부 코드의 나머지 부분에서 수행됩니다. + +/// diff --git a/docs/ko/docs/how-to/custom-docs-ui-assets.md b/docs/ko/docs/how-to/custom-docs-ui-assets.md new file mode 100644 index 000000000..d6383c29c --- /dev/null +++ b/docs/ko/docs/how-to/custom-docs-ui-assets.md @@ -0,0 +1,185 @@ +# 커스텀 Docs UI 정적 에셋(자체 호스팅) { #custom-docs-ui-static-assets-self-hosting } + +API 문서는 **Swagger UI**와 **ReDoc**을 사용하며, 각각 JavaScript와 CSS 파일이 필요합니다. + +기본적으로 이러한 파일은 <abbr title="Content Delivery Network - 콘텐츠 전송 네트워크: 일반적으로 여러 서버로 구성되어 JavaScript와 CSS 같은 정적 파일을 제공하는 서비스입니다. 보통 클라이언트에 더 가까운 서버에서 파일을 제공해 성능을 향상시키는 데 사용됩니다.">CDN</abbr>에서 제공됩니다. + +하지만 이를 커스터마이징할 수 있으며, 특정 CDN을 지정하거나 파일을 직접 제공할 수도 있습니다. + +## JavaScript와 CSS용 커스텀 CDN { #custom-cdn-for-javascript-and-css } + +예를 들어 다른 <abbr title="Content Delivery Network">CDN</abbr>을 사용하고 싶다고 해봅시다. 예를 들면 `https://unpkg.com/`을 사용하려는 경우입니다. + +이는 예를 들어 특정 국가에서 일부 URL을 제한하는 경우에 유용할 수 있습니다. + +### 자동 문서 비활성화하기 { #disable-the-automatic-docs } + +첫 번째 단계는 자동 문서를 비활성화하는 것입니다. 기본적으로 자동 문서는 기본 CDN을 사용하기 때문입니다. + +비활성화하려면 `FastAPI` 앱을 생성할 때 해당 URL을 `None`으로 설정하세요: + +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[8] *} + +### 커스텀 문서 포함하기 { #include-the-custom-docs } + +이제 커스텀 문서를 위한 *경로 처리*를 만들 수 있습니다. + +FastAPI 내부 함수를 재사용해 문서용 HTML 페이지를 생성하고, 필요한 인자를 전달할 수 있습니다: + +* `openapi_url`: 문서 HTML 페이지가 API의 OpenAPI 스키마를 가져올 수 있는 URL입니다. 여기서는 `app.openapi_url` 속성을 사용할 수 있습니다. +* `title`: API의 제목입니다. +* `oauth2_redirect_url`: 기본값을 사용하려면 여기서 `app.swagger_ui_oauth2_redirect_url`을 사용할 수 있습니다. +* `swagger_js_url`: Swagger UI 문서의 HTML이 **JavaScript** 파일을 가져올 수 있는 URL입니다. 커스텀 CDN URL입니다. +* `swagger_css_url`: Swagger UI 문서의 HTML이 **CSS** 파일을 가져올 수 있는 URL입니다. 커스텀 CDN URL입니다. + +ReDoc도 마찬가지입니다... + +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[2:6,11:19,22:24,27:33] *} + +/// tip | 팁 + +`swagger_ui_redirect`에 대한 *경로 처리*는 OAuth2를 사용할 때 도움이 되는 헬퍼입니다. + +API를 OAuth2 provider와 통합하면 인증을 수행한 뒤 획득한 자격 증명으로 API 문서로 다시 돌아올 수 있습니다. 그리고 실제 OAuth2 인증을 사용해 API와 상호작용할 수 있습니다. + +Swagger UI가 이 과정을 백그라운드에서 처리해 주지만, 이를 위해 이 "redirect" 헬퍼가 필요합니다. + +/// + +### 테스트용 *경로 처리* 만들기 { #create-a-path-operation-to-test-it } + +이제 모든 것이 제대로 동작하는지 테스트할 수 있도록 *경로 처리*를 하나 만드세요: + +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[36:38] *} + +### 테스트하기 { #test-it } + +이제 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>에서 문서에 접속한 뒤 페이지를 새로고침하면, 새 CDN에서 에셋을 불러오는 것을 확인할 수 있습니다. + +## 문서용 JavaScript와 CSS 자체 호스팅하기 { #self-hosting-javascript-and-css-for-docs } + +JavaScript와 CSS를 자체 호스팅하는 것은 예를 들어, 오프라인 상태이거나 외부 인터넷에 접근할 수 없는 환경, 또는 로컬 네트워크에서도 앱이 계속 동작해야 할 때 유용할 수 있습니다. + +여기서는 동일한 FastAPI 앱에서 해당 파일을 직접 제공하고, 문서가 이를 사용하도록 설정하는 방법을 살펴봅니다. + +### 프로젝트 파일 구조 { #project-file-structure } + +프로젝트 파일 구조가 다음과 같다고 해봅시다: + +``` +. +├── app +│ ├── __init__.py +│ ├── main.py +``` + +이제 해당 정적 파일을 저장할 디렉터리를 만드세요. + +새 파일 구조는 다음과 같을 수 있습니다: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +└── static/ +``` + +### 파일 다운로드하기 { #download-the-files } + +문서에 필요한 정적 파일을 다운로드해서 `static/` 디렉터리에 넣으세요. + +각 링크를 우클릭한 뒤 "링크를 다른 이름으로 저장..."과 비슷한 옵션을 선택하면 될 것입니다. + +**Swagger UI**는 다음 파일을 사용합니다: + +* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js" class="external-link" target="_blank">`swagger-ui-bundle.js`</a> +* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css" class="external-link" target="_blank">`swagger-ui.css`</a> + +**ReDoc**은 다음 파일을 사용합니다: + +* <a href="https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js" class="external-link" target="_blank">`redoc.standalone.js`</a> + +이후 파일 구조는 다음과 같을 수 있습니다: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +└── static + ├── redoc.standalone.js + ├── swagger-ui-bundle.js + └── swagger-ui.css +``` + +### 정적 파일 제공하기 { #serve-the-static-files } + +* `StaticFiles`를 import합니다. +* 특정 경로에 `StaticFiles()` 인스턴스를 "마운트(mount)"합니다. + +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[7,11] *} + +### 정적 파일 테스트하기 { #test-the-static-files } + +애플리케이션을 시작하고 <a href="http://127.0.0.1:8000/static/redoc.standalone.js" class="external-link" target="_blank">http://127.0.0.1:8000/static/redoc.standalone.js</a>로 이동하세요. + +**ReDoc**용 매우 긴 JavaScript 파일이 보일 것입니다. + +예를 들어 다음과 같이 시작할 수 있습니다: + +```JavaScript +/*! For license information please see redoc.standalone.js.LICENSE.txt */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("null")): +... +``` + +이는 앱에서 정적 파일을 제공할 수 있고, 문서용 정적 파일을 올바른 위치에 배치했다는 것을 확인해 줍니다. + +이제 문서가 이 정적 파일을 사용하도록 앱을 설정할 수 있습니다. + +### 정적 파일을 위한 자동 문서 비활성화하기 { #disable-the-automatic-docs-for-static-files } + +커스텀 CDN을 사용할 때와 마찬가지로, 첫 단계는 자동 문서를 비활성화하는 것입니다. 자동 문서는 기본적으로 CDN을 사용합니다. + +비활성화하려면 `FastAPI` 앱을 생성할 때 해당 URL을 `None`으로 설정하세요: + +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[9] *} + +### 정적 파일을 위한 커스텀 문서 포함하기 { #include-the-custom-docs-for-static-files } + +그리고 커스텀 CDN을 사용할 때와 동일한 방식으로, 이제 커스텀 문서를 위한 *경로 처리*를 만들 수 있습니다. + +다시 한 번, FastAPI 내부 함수를 재사용해 문서용 HTML 페이지를 생성하고, 필요한 인자를 전달할 수 있습니다: + +* `openapi_url`: 문서 HTML 페이지가 API의 OpenAPI 스키마를 가져올 수 있는 URL입니다. 여기서는 `app.openapi_url` 속성을 사용할 수 있습니다. +* `title`: API의 제목입니다. +* `oauth2_redirect_url`: 기본값을 사용하려면 여기서 `app.swagger_ui_oauth2_redirect_url`을 사용할 수 있습니다. +* `swagger_js_url`: Swagger UI 문서의 HTML이 **JavaScript** 파일을 가져올 수 있는 URL입니다. **이제는 여러분의 앱이 직접 제공하는 파일입니다**. +* `swagger_css_url`: Swagger UI 문서의 HTML이 **CSS** 파일을 가져올 수 있는 URL입니다. **이제는 여러분의 앱이 직접 제공하는 파일입니다**. + +ReDoc도 마찬가지입니다... + +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[2:6,14:22,25:27,30:36] *} + +/// tip | 팁 + +`swagger_ui_redirect`에 대한 *경로 처리*는 OAuth2를 사용할 때 도움이 되는 헬퍼입니다. + +API를 OAuth2 provider와 통합하면 인증을 수행한 뒤 획득한 자격 증명으로 API 문서로 다시 돌아올 수 있습니다. 그리고 실제 OAuth2 인증을 사용해 API와 상호작용할 수 있습니다. + +Swagger UI가 이 과정을 백그라운드에서 처리해 주지만, 이를 위해 이 "redirect" 헬퍼가 필요합니다. + +/// + +### 정적 파일 테스트용 *경로 처리* 만들기 { #create-a-path-operation-to-test-static-files } + +이제 모든 것이 제대로 동작하는지 테스트할 수 있도록 *경로 처리*를 하나 만드세요: + +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[39:41] *} + +### 정적 파일 UI 테스트하기 { #test-static-files-ui } + +이제 WiFi 연결을 끊고 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>에서 문서에 접속한 뒤 페이지를 새로고침해 보세요. + +인터넷이 없어도 API 문서를 보고, API와 상호작용할 수 있을 것입니다. diff --git a/docs/ko/docs/how-to/custom-request-and-route.md b/docs/ko/docs/how-to/custom-request-and-route.md new file mode 100644 index 000000000..335193bb3 --- /dev/null +++ b/docs/ko/docs/how-to/custom-request-and-route.md @@ -0,0 +1,109 @@ +# 커스텀 Request 및 APIRoute 클래스 { #custom-request-and-apiroute-class } + +일부 경우에는 `Request`와 `APIRoute` 클래스에서 사용되는 로직을 오버라이드하고 싶을 수 있습니다. + +특히, 이는 middleware에 있는 로직의 좋은 대안이 될 수 있습니다. + +예를 들어, 애플리케이션에서 처리되기 전에 요청 바디를 읽거나 조작하고 싶을 때가 그렇습니다. + +/// danger | 위험 + +이 기능은 "고급" 기능입니다. + +**FastAPI**를 이제 막 시작했다면 이 섹션은 건너뛰는 것이 좋습니다. + +/// + +## 사용 사례 { #use-cases } + +사용 사례에는 다음이 포함됩니다: + +* JSON이 아닌 요청 바디를 JSON으로 변환하기(예: <a href="https://msgpack.org/index.html" class="external-link" target="_blank">`msgpack`</a>). +* gzip으로 압축된 요청 바디 압축 해제하기. +* 모든 요청 바디를 자동으로 로깅하기. + +## 커스텀 요청 바디 인코딩 처리하기 { #handling-custom-request-body-encodings } + +커스텀 `Request` 서브클래스를 사용해 gzip 요청의 압축을 해제하는 방법을 살펴보겠습니다. + +그리고 그 커스텀 요청 클래스를 사용하기 위한 `APIRoute` 서브클래스도 함께 보겠습니다. + +### 커스텀 `GzipRequest` 클래스 만들기 { #create-a-custom-gziprequest-class } + +/// tip | 팁 + +이 예시는 동작 방식 시연을 위한 장난감 예제입니다. Gzip 지원이 필요하다면 제공되는 [`GzipMiddleware`](../advanced/middleware.md#gzipmiddleware){.internal-link target=_blank}를 사용할 수 있습니다. + +/// + +먼저, `GzipRequest` 클래스를 만듭니다. 이 클래스는 `Request.body()` 메서드를 덮어써서, 적절한 헤더가 있는 경우 바디를 압축 해제합니다. + +헤더에 `gzip`이 없으면 바디를 압축 해제하려고 시도하지 않습니다. + +이렇게 하면 동일한 route 클래스가 gzip으로 압축된 요청과 압축되지 않은 요청을 모두 처리할 수 있습니다. + +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *} + +### 커스텀 `GzipRoute` 클래스 만들기 { #create-a-custom-gziproute-class } + +다음으로, `GzipRequest`를 활용하는 `fastapi.routing.APIRoute`의 커스텀 서브클래스를 만듭니다. + +이번에는 `APIRoute.get_route_handler()` 메서드를 오버라이드합니다. + +이 메서드는 함수를 반환합니다. 그리고 그 함수가 요청을 받아 응답을 반환합니다. + +여기서는 원본 요청으로부터 `GzipRequest`를 만들기 위해 이를 사용합니다. + +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *} + +/// note | 기술 세부사항 + +`Request`에는 `request.scope` 속성이 있는데, 이는 요청과 관련된 메타데이터를 담고 있는 Python `dict`입니다. + +`Request`에는 또한 `request.receive`가 있는데, 이는 요청의 바디를 "받기(receive)" 위한 함수입니다. + +`scope` `dict`와 `receive` 함수는 모두 ASGI 명세의 일부입니다. + +그리고 이 두 가지, `scope`와 `receive`가 새로운 `Request` 인스턴스를 만드는 데 필요한 것들입니다. + +`Request`에 대해 더 알아보려면 <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">Starlette의 Requests 문서</a>를 확인하세요. + +/// + +`GzipRequest.get_route_handler`가 반환하는 함수가 다르게 하는 유일한 것은 `Request`를 `GzipRequest`로 변환하는 것입니다. + +이렇게 하면, 우리의 `GzipRequest`가 *경로 처리*로 전달하기 전에(필요하다면) 데이터의 압축 해제를 담당하게 됩니다. + +그 이후의 모든 처리 로직은 동일합니다. + +하지만 `GzipRequest.body`에서 변경을 했기 때문에, 필요할 때 **FastAPI**가 로드하는 시점에 요청 바디는 자동으로 압축 해제됩니다. + +## 예외 핸들러에서 요청 바디 접근하기 { #accessing-the-request-body-in-an-exception-handler } + +/// tip | 팁 + +같은 문제를 해결하려면 `RequestValidationError`에 대한 커스텀 핸들러에서 `body`를 사용하는 편이 아마 훨씬 더 쉽습니다([오류 처리하기](../tutorial/handling-errors.md#use-the-requestvalidationerror-body){.internal-link target=_blank}). + +하지만 이 예시도 여전히 유효하며, 내부 컴포넌트와 상호작용하는 방법을 보여줍니다. + +/// + +같은 접근 방식을 사용해 예외 핸들러에서 요청 바디에 접근할 수도 있습니다. + +필요한 것은 `try`/`except` 블록 안에서 요청을 처리하는 것뿐입니다: + +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *} + +예외가 발생하더라도 `Request` 인스턴스는 여전히 스코프 안에 남아 있으므로, 오류를 처리할 때 요청 바디를 읽고 활용할 수 있습니다: + +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *} + +## 라우터에서의 커스텀 `APIRoute` 클래스 { #custom-apiroute-class-in-a-router } + +`APIRouter`의 `route_class` 파라미터를 설정할 수도 있습니다: + +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *} + +이 예시에서는 `router` 아래의 *경로 처리*들이 커스텀 `TimedRoute` 클래스를 사용하며, 응답을 생성하는 데 걸린 시간을 담은 추가 `X-Response-Time` 헤더가 응답에 포함됩니다: + +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *} diff --git a/docs/ko/docs/how-to/extending-openapi.md b/docs/ko/docs/how-to/extending-openapi.md new file mode 100644 index 000000000..d04d6c23e --- /dev/null +++ b/docs/ko/docs/how-to/extending-openapi.md @@ -0,0 +1,80 @@ +# OpenAPI 확장하기 { #extending-openapi } + +생성된 OpenAPI 스키마를 수정해야 하는 경우가 있습니다. + +이 섹션에서 그 방법을 살펴보겠습니다. + +## 일반적인 과정 { #the-normal-process } + +일반적인(기본) 과정은 다음과 같습니다. + +`FastAPI` 애플리케이션(인스턴스)에는 OpenAPI 스키마를 반환해야 하는 `.openapi()` 메서드가 있습니다. + +애플리케이션 객체를 생성하는 과정에서 `/openapi.json`(또는 `openapi_url`에 설정한 경로)용 *경로 처리*가 등록됩니다. + +이 경로 처리는 애플리케이션의 `.openapi()` 메서드 결과를 JSON 응답으로 반환할 뿐입니다. + +기본적으로 `.openapi()` 메서드는 프로퍼티 `.openapi_schema`에 내용이 있는지 확인하고, 있으면 그 내용을 반환합니다. + +없으면 `fastapi.openapi.utils.get_openapi`에 있는 유틸리티 함수를 사용해 생성합니다. + +그리고 `get_openapi()` 함수는 다음을 파라미터로 받습니다: + +* `title`: 문서에 표시되는 OpenAPI 제목. +* `version`: API 버전. 예: `2.5.0`. +* `openapi_version`: 사용되는 OpenAPI 스펙 버전. 기본값은 최신인 `3.1.0`. +* `summary`: API에 대한 짧은 요약. +* `description`: API 설명. markdown을 포함할 수 있으며 문서에 표시됩니다. +* `routes`: 라우트 목록. 각각 등록된 *경로 처리*입니다. `app.routes`에서 가져옵니다. + +/// info | 정보 + +`summary` 파라미터는 OpenAPI 3.1.0 이상에서 사용할 수 있으며, FastAPI 0.99.0 이상에서 지원됩니다. + +/// + +## 기본값 덮어쓰기 { #overriding-the-defaults } + +위 정보를 바탕으로, 동일한 유틸리티 함수를 사용해 OpenAPI 스키마를 생성하고 필요한 각 부분을 덮어쓸 수 있습니다. + +예를 들어, <a href="https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md#x-logo" class="external-link" target="_blank">커스텀 로고를 포함하기 위한 ReDoc의 OpenAPI 확장</a>을 추가해 보겠습니다. + +### 일반적인 **FastAPI** { #normal-fastapi } + +먼저, 평소처럼 **FastAPI** 애플리케이션을 모두 작성합니다: + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[1,4,7:9] *} + +### OpenAPI 스키마 생성하기 { #generate-the-openapi-schema } + +그다음 `custom_openapi()` 함수 안에서, 동일한 유틸리티 함수를 사용해 OpenAPI 스키마를 생성합니다: + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[2,15:21] *} + +### OpenAPI 스키마 수정하기 { #modify-the-openapi-schema } + +이제 OpenAPI 스키마의 `info` "object"에 커스텀 `x-logo`를 추가하여 ReDoc 확장을 더할 수 있습니다: + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[22:24] *} + +### OpenAPI 스키마 캐시하기 { #cache-the-openapi-schema } + +생성한 스키마를 저장하기 위한 "cache"로 `.openapi_schema` 프로퍼티를 사용할 수 있습니다. + +이렇게 하면 사용자가 API 문서를 열 때마다 애플리케이션이 스키마를 매번 생성하지 않아도 됩니다. + +스키마는 한 번만 생성되고, 이후 요청에서는 같은 캐시된 스키마가 사용됩니다. + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[13:14,25:26] *} + +### 메서드 오버라이드하기 { #override-the-method } + +이제 `.openapi()` 메서드를 새 함수로 교체할 수 있습니다. + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[29] *} + +### 확인하기 { #check-it } + +<a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>로 이동하면 커스텀 로고(이 예시에서는 **FastAPI** 로고)를 사용하는 것을 확인할 수 있습니다: + +<img src="/img/tutorial/extending-openapi/image01.png"> diff --git a/docs/ko/docs/how-to/general.md b/docs/ko/docs/how-to/general.md new file mode 100644 index 000000000..a18dc68a2 --- /dev/null +++ b/docs/ko/docs/how-to/general.md @@ -0,0 +1,39 @@ +# 일반 - 사용 방법 - 레시피 { #general-how-to-recipes } + +일반적이거나 자주 나오는 질문에 대해, 문서의 다른 위치로 안내하는 몇 가지 포인터를 소개합니다. + +## 데이터 필터링 - 보안 { #filter-data-security } + +반환하면 안 되는 데이터를 과도하게 반환하지 않도록 하려면, [튜토리얼 - 응답 모델 - 반환 타입](../tutorial/response-model.md){.internal-link target=_blank} 문서를 읽어보세요. + +## 문서화 태그 - OpenAPI { #documentation-tags-openapi } + +*경로 처리*에 태그를 추가하고, 문서 UI에서 이를 그룹화하려면 [튜토리얼 - 경로 처리 구성 - 태그](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank} 문서를 읽어보세요. + +## 문서화 요약 및 설명 - OpenAPI { #documentation-summary-and-description-openapi } + +*경로 처리*에 요약과 설명을 추가하고, 문서 UI에 표시하려면 [튜토리얼 - 경로 처리 구성 - 요약 및 설명](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank} 문서를 읽어보세요. + +## 문서화 응답 설명 - OpenAPI { #documentation-response-description-openapi } + +문서 UI에 표시되는 응답의 설명을 정의하려면 [튜토리얼 - 경로 처리 구성 - 응답 설명](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank} 문서를 읽어보세요. + +## 문서화 *경로 처리* 지원 중단하기 - OpenAPI { #documentation-deprecate-a-path-operation-openapi } + +*경로 처리*를 지원 중단(deprecate)으로 표시하고, 문서 UI에 보여주려면 [튜토리얼 - 경로 처리 구성 - 지원 중단](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank} 문서를 읽어보세요. + +## 어떤 데이터든 JSON 호환으로 변환하기 { #convert-any-data-to-json-compatible } + +어떤 데이터든 JSON 호환 형식으로 변환하려면 [튜토리얼 - JSON 호환 인코더](../tutorial/encoder.md){.internal-link target=_blank} 문서를 읽어보세요. + +## OpenAPI 메타데이터 - 문서 { #openapi-metadata-docs } + +라이선스, 버전, 연락처 등의 정보를 포함해 OpenAPI 스키마에 메타데이터를 추가하려면 [튜토리얼 - 메타데이터와 문서 URL](../tutorial/metadata.md){.internal-link target=_blank} 문서를 읽어보세요. + +## OpenAPI 사용자 정의 URL { #openapi-custom-url } + +OpenAPI URL을 커스터마이즈(또는 제거)하려면 [튜토리얼 - 메타데이터와 문서 URL](../tutorial/metadata.md#openapi-url){.internal-link target=_blank} 문서를 읽어보세요. + +## OpenAPI 문서 URL { #openapi-docs-urls } + +자동으로 생성되는 문서 사용자 인터페이스에서 사용하는 URL을 업데이트하려면 [튜토리얼 - 메타데이터와 문서 URL](../tutorial/metadata.md#docs-urls){.internal-link target=_blank} 문서를 읽어보세요. diff --git a/docs/ko/docs/how-to/graphql.md b/docs/ko/docs/how-to/graphql.md new file mode 100644 index 000000000..3cc467eb7 --- /dev/null +++ b/docs/ko/docs/how-to/graphql.md @@ -0,0 +1,60 @@ +# GraphQL { #graphql } + +**FastAPI**는 **ASGI** 표준을 기반으로 하므로, ASGI와도 호환되는 어떤 **GraphQL** 라이브러리든 매우 쉽게 통합할 수 있습니다. + +같은 애플리케이션에서 일반 FastAPI **경로 처리**와 GraphQL을 함께 조합할 수 있습니다. + +/// tip | 팁 + +**GraphQL**은 몇 가지 매우 특정한 사용 사례를 해결합니다. + +일반적인 **web API**와 비교했을 때 **장점**과 **단점**이 있습니다. + +여러분의 사용 사례에서 **이점**이 **단점**을 상쇄하는지 꼭 평가해 보세요. 🤓 + +/// + +## GraphQL 라이브러리 { #graphql-libraries } + +다음은 **ASGI** 지원이 있는 **GraphQL** 라이브러리들입니다. **FastAPI**와 함께 사용할 수 있습니다: + +* <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry</a> 🍓 + * <a href="https://strawberry.rocks/docs/integrations/fastapi" class="external-link" target="_blank">FastAPI용 문서</a> 제공 +* <a href="https://ariadnegraphql.org/" class="external-link" target="_blank">Ariadne</a> + * <a href="https://ariadnegraphql.org/docs/fastapi-integration" class="external-link" target="_blank">FastAPI용 문서</a> 제공 +* <a href="https://tartiflette.io/" class="external-link" target="_blank">Tartiflette</a> + * ASGI 통합을 제공하기 위해 <a href="https://tartiflette.github.io/tartiflette-asgi/" class="external-link" target="_blank">Tartiflette ASGI</a> 사용 +* <a href="https://graphene-python.org/" class="external-link" target="_blank">Graphene</a> + * <a href="https://github.com/ciscorn/starlette-graphene3" class="external-link" target="_blank">starlette-graphene3</a> 사용 + +## Strawberry로 GraphQL 사용하기 { #graphql-with-strawberry } + +**GraphQL**로 작업해야 하거나 작업하고 싶다면, <a href="https://strawberry.rocks/" class="external-link" target="_blank">**Strawberry**</a>를 **권장**합니다. **FastAPI**의 설계와 가장 가깝고, 모든 것이 **type annotations**에 기반해 있기 때문입니다. + +사용 사례에 따라 다른 라이브러리를 선호할 수도 있지만, 제게 묻는다면 아마 **Strawberry**를 먼저 시도해 보라고 제안할 것입니다. + +다음은 Strawberry를 FastAPI와 통합하는 방법에 대한 간단한 미리보기입니다: + +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} + +<a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry 문서</a>에서 Strawberry에 대해 더 알아볼 수 있습니다. + +또한 <a href="https://strawberry.rocks/docs/integrations/fastapi" class="external-link" target="_blank">FastAPI에서 Strawberry 사용</a>에 대한 문서도 확인해 보세요. + +## Starlette의 예전 `GraphQLApp` { #older-graphqlapp-from-starlette } + +이전 버전의 Starlette에는 <a href="https://graphene-python.org/" class="external-link" target="_blank">Graphene</a>과 통합하기 위한 `GraphQLApp` 클래스가 포함되어 있었습니다. + +이것은 Starlette에서 deprecated 되었지만, 이를 사용하던 코드가 있다면 같은 사용 사례를 다루고 **거의 동일한 인터페이스**를 가진 <a href="https://github.com/ciscorn/starlette-graphene3" class="external-link" target="_blank">starlette-graphene3</a>로 쉽게 **마이그레이션**할 수 있습니다. + +/// tip | 팁 + +GraphQL이 필요하다면, 커스텀 클래스와 타입 대신 type annotations에 기반한 <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry</a>를 여전히 확인해 보시길 권장합니다. + +/// + +## 더 알아보기 { #learn-more } + +<a href="https://graphql.org/" class="external-link" target="_blank">공식 GraphQL 문서</a>에서 **GraphQL**에 대해 더 알아볼 수 있습니다. + +또한 위에서 설명한 각 라이브러리에 대해서도 해당 링크에서 더 자세히 읽어볼 수 있습니다. diff --git a/docs/ko/docs/how-to/index.md b/docs/ko/docs/how-to/index.md new file mode 100644 index 000000000..9321c488b --- /dev/null +++ b/docs/ko/docs/how-to/index.md @@ -0,0 +1,13 @@ +# How To - 레시피 { #how-to-recipes } + +여기에서는 **여러 주제**에 대한 다양한 레시피(“how to” 가이드)를 볼 수 있습니다. + +대부분의 아이디어는 어느 정도 **서로 독립적**이며, 대부분의 경우 **여러분의 프로젝트**에 직접 적용되는 경우에만 학습하면 됩니다. + +프로젝트에 흥미롭고 유용해 보이는 것이 있다면 확인해 보세요. 그렇지 않다면 아마 건너뛰어도 됩니다. + +/// tip | 팁 + +**FastAPI를 구조적으로 학습**하고 싶다면(권장), 대신 [튜토리얼 - 사용자 가이드](../tutorial/index.md){.internal-link target=_blank}를 장별로 읽어보세요. + +/// diff --git a/docs/ko/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/ko/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md new file mode 100644 index 000000000..6e528ecaf --- /dev/null +++ b/docs/ko/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -0,0 +1,135 @@ +# Pydantic v1에서 Pydantic v2로 마이그레이션하기 { #migrate-from-pydantic-v1-to-pydantic-v2 } + +오래된 FastAPI 앱이 있다면 Pydantic 버전 1을 사용하고 있을 수 있습니다. + +FastAPI 0.100.0 버전은 Pydantic v1 또는 v2 중 하나를 지원했습니다. 설치되어 있는 쪽을 사용했습니다. + +FastAPI 0.119.0 버전에서는 v2로의 마이그레이션을 쉽게 하기 위해, Pydantic v2 내부에서 Pydantic v1을(`pydantic.v1`로) 부분적으로 지원하기 시작했습니다. + +FastAPI 0.126.0 버전에서는 Pydantic v1 지원을 중단했지만, `pydantic.v1`은 잠시 동안 계속 지원했습니다. + +/// warning | 경고 + +Pydantic 팀은 **Python 3.14**부터 최신 Python 버전에서 Pydantic v1 지원을 중단했습니다. + +여기에는 `pydantic.v1`도 포함되며, Python 3.14 이상에서는 더 이상 지원되지 않습니다. + +Python의 최신 기능을 사용하려면 Pydantic v2를 사용하고 있는지 확인해야 합니다. + +/// + +Pydantic v1을 사용하는 오래된 FastAPI 앱이 있다면, 여기서는 이를 Pydantic v2로 마이그레이션하는 방법과 점진적 마이그레이션을 돕는 **FastAPI 0.119.0의 기능**을 소개하겠습니다. + +## 공식 가이드 { #official-guide } + +Pydantic에는 v1에서 v2로의 공식 <a href="https://docs.pydantic.dev/latest/migration/" class="external-link" target="_blank">Migration Guide</a>가 있습니다. + +여기에는 무엇이 바뀌었는지, 검증이 이제 어떻게 더 정확하고 엄격해졌는지, 가능한 주의사항 등도 포함되어 있습니다. + +변경된 내용을 더 잘 이해하기 위해 읽어보면 좋습니다. + +## 테스트 { #tests } + +앱에 대한 [tests](../tutorial/testing.md){.internal-link target=_blank}가 있는지 확인하고, 지속적 통합(CI)에서 테스트를 실행하세요. + +이렇게 하면 업그레이드를 진행하면서도 모든 것이 기대한 대로 계속 동작하는지 확인할 수 있습니다. + +## `bump-pydantic` { #bump-pydantic } + +많은 경우, 커스터마이징 없이 일반적인 Pydantic 모델을 사용하고 있다면 Pydantic v1에서 Pydantic v2로의 마이그레이션 과정 대부분을 자동화할 수 있습니다. + +같은 Pydantic 팀이 제공하는 <a href="https://github.com/pydantic/bump-pydantic" class="external-link" target="_blank">`bump-pydantic`</a>를 사용할 수 있습니다. + +이 도구는 변경해야 하는 코드의 대부분을 자동으로 바꾸는 데 도움을 줍니다. + +그 다음 테스트를 실행해서 모든 것이 동작하는지 확인하면 됩니다. 잘 된다면 끝입니다. 😎 + +## v2 안의 Pydantic v1 { #pydantic-v1-in-v2 } + +Pydantic v2는 Pydantic v1의 모든 것을 서브모듈 `pydantic.v1`로 포함합니다. 하지만 이는 Python 3.13보다 높은 버전에서는 더 이상 지원되지 않습니다. + +즉, Pydantic v2의 최신 버전을 설치한 뒤, 이 서브모듈에서 예전 Pydantic v1 구성 요소를 import하여 예전 Pydantic v1을 설치한 것처럼 사용할 수 있습니다. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} + +### v2 안의 Pydantic v1에 대한 FastAPI 지원 { #fastapi-support-for-pydantic-v1-in-v2 } + +FastAPI 0.119.0부터는 v2로의 마이그레이션을 쉽게 하기 위해, Pydantic v2 내부의 Pydantic v1에 대해서도 부분적인 지원이 있습니다. + +따라서 Pydantic을 최신 v2로 업그레이드하고, import를 `pydantic.v1` 서브모듈을 사용하도록 바꾸면, 많은 경우 그대로 동작합니다. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} + +/// warning | 경고 + +Pydantic 팀이 Python 3.14부터 최신 Python 버전에서 Pydantic v1을 더 이상 지원하지 않으므로, `pydantic.v1`을 사용하는 것 역시 Python 3.14 이상에서는 지원되지 않는다는 점을 염두에 두세요. + +/// + +### 같은 앱에서 Pydantic v1과 v2 함께 사용하기 { #pydantic-v1-and-v2-on-the-same-app } + +Pydantic에서는 Pydantic v2 모델의 필드를 Pydantic v1 모델로 정의하거나 그 반대로 하는 것을 **지원하지 않습니다**. + +```mermaid +graph TB + subgraph "❌ Not Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V1Field["Pydantic v1 Model"] + end + subgraph V1["Pydantic v1 Model"] + V2Field["Pydantic v2 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +...하지만 같은 앱에서 Pydantic v1과 v2를 사용하되, 모델을 분리해서 둘 수는 있습니다. + +```mermaid +graph TB + subgraph "✅ Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V2Field["Pydantic v2 Model"] + end + subgraph V1["Pydantic v1 Model"] + V1Field["Pydantic v1 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +어떤 경우에는 FastAPI 앱의 같은 **경로 처리**에서 Pydantic v1과 v2 모델을 함께 사용하는 것도 가능합니다: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} + +위 예제에서 입력 모델은 Pydantic v1 모델이고, 출력 모델(`response_model=ItemV2`로 정의됨)은 Pydantic v2 모델입니다. + +### Pydantic v1 파라미터 { #pydantic-v1-parameters } + +Pydantic v1 모델과 함께 `Body`, `Query`, `Form` 등 파라미터용 FastAPI 전용 도구 일부를 사용해야 한다면, Pydantic v2로의 마이그레이션을 마칠 때까지 `fastapi.temp_pydantic_v1_params`에서 import할 수 있습니다: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} + +### 단계적으로 마이그레이션하기 { #migrate-in-steps } + +/// tip | 팁 + +먼저 `bump-pydantic`로 시도해 보세요. 테스트가 통과하고 잘 동작한다면, 한 번의 명령으로 끝입니다. ✨ + +/// + +`bump-pydantic`가 여러분의 사용 사례에 맞지 않는다면, 같은 앱에서 Pydantic v1과 v2 모델을 모두 지원하는 기능을 이용해 Pydantic v2로 점진적으로 마이그레이션할 수 있습니다. + +먼저 Pydantic을 최신 v2로 업그레이드하고, 모든 모델의 import를 `pydantic.v1`을 사용하도록 바꿀 수 있습니다. + +그 다음 Pydantic v1에서 v2로 모델을 그룹 단위로, 점진적인 단계로 마이그레이션을 시작하면 됩니다. 🚶 diff --git a/docs/ko/docs/how-to/separate-openapi-schemas.md b/docs/ko/docs/how-to/separate-openapi-schemas.md new file mode 100644 index 000000000..055429c26 --- /dev/null +++ b/docs/ko/docs/how-to/separate-openapi-schemas.md @@ -0,0 +1,102 @@ +# 입력과 출력에 대해 OpenAPI 스키마를 분리할지 여부 { #separate-openapi-schemas-for-input-and-output-or-not } + +**Pydantic v2**가 릴리스된 이후, 생성되는 OpenAPI는 이전보다 조금 더 정확하고 **올바르게** 만들어집니다. 😎 + +실제로 어떤 경우에는, 같은 Pydantic 모델에 대해 OpenAPI 안에 **두 개의 JSON Schema**가 생기기도 합니다. **기본값(default value)**이 있는지 여부에 따라, 입력용과 출력용으로 나뉩니다. + +이것이 어떻게 동작하는지, 그리고 필요하다면 어떻게 변경할 수 있는지 살펴보겠습니다. + +## 입력과 출력을 위한 Pydantic 모델 { #pydantic-models-for-input-and-output } + +예를 들어, 다음처럼 기본값이 있는 Pydantic 모델이 있다고 해보겠습니다: + +{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:7] hl[7] *} + +### 입력용 모델 { #model-for-input } + +이 모델을 다음처럼 입력으로 사용하면: + +{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:15] hl[14] *} + +...`description` 필드는 **필수가 아닙니다**. `None`이라는 기본값이 있기 때문입니다. + +### 문서에서의 입력 모델 { #input-model-in-docs } + +문서에서 `description` 필드에 **빨간 별표**가 없고, 필수로 표시되지 않는 것을 확인할 수 있습니다: + +<div class="screenshot"> +<img src="/img/tutorial/separate-openapi-schemas/image01.png"> +</div> + +### 출력용 모델 { #model-for-output } + +하지만 같은 모델을 다음처럼 출력으로 사용하면: + +{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py hl[19] *} + +...`description`에 기본값이 있기 때문에, 그 필드에 대해 **아무것도 반환하지 않더라도** 여전히 그 **기본값**이 들어가게 됩니다. + +### 출력 응답 데이터용 모델 { #model-for-output-response-data } + +문서에서 직접 동작시켜 응답을 확인해 보면, 코드가 `description` 필드 중 하나에 아무것도 추가하지 않았더라도 JSON 응답에는 기본값(`null`)이 포함되어 있습니다: + +<div class="screenshot"> +<img src="/img/tutorial/separate-openapi-schemas/image02.png"> +</div> + +이는 해당 필드가 **항상 값을 가진다는 것**을 의미합니다. 다만 그 값이 때로는 `None`(JSON에서는 `null`)일 수 있습니다. + +즉, API를 사용하는 클라이언트는 값이 존재하는지 여부를 확인할 필요가 없고, **필드가 항상 존재한다고 가정**할 수 있습니다. 다만 어떤 경우에는 기본값 `None`이 들어갑니다. + +이를 OpenAPI에서 표현하는 방법은, 그 필드를 **required**로 표시하는 것입니다. 항상 존재하기 때문입니다. + +이 때문에, 하나의 모델이라도 **입력용인지 출력용인지**에 따라 JSON Schema가 달라질 수 있습니다: + +* **입력**에서는 `description`이 **필수가 아님** +* **출력**에서는 **필수임** (그리고 값은 `None`일 수도 있으며, JSON 용어로는 `null`) + +### 문서에서의 출력용 모델 { #model-for-output-in-docs } + +문서에서 출력 모델을 확인해 보면, `name`과 `description` **둘 다** **빨간 별표**로 **필수**로 표시되어 있습니다: + +<div class="screenshot"> +<img src="/img/tutorial/separate-openapi-schemas/image03.png"> +</div> + +### 문서에서의 입력과 출력 모델 { #model-for-input-and-output-in-docs } + +또 OpenAPI에서 사용 가능한 모든 Schemas(JSON Schemas)를 확인해 보면, `Item-Input` 하나와 `Item-Output` 하나, 이렇게 두 개가 있는 것을 볼 수 있습니다. + +`Item-Input`에서는 `description`이 **필수가 아니며**, 빨간 별표가 없습니다. + +하지만 `Item-Output`에서는 `description`이 **필수이며**, 빨간 별표가 있습니다. + +<div class="screenshot"> +<img src="/img/tutorial/separate-openapi-schemas/image04.png"> +</div> + +**Pydantic v2**의 이 기능 덕분에 API 문서는 더 **정밀**해지고, 자동 생성된 클라이언트와 SDK가 있다면 그것들도 더 정밀해져서 더 나은 **developer experience**와 일관성을 제공할 수 있습니다. 🎉 + +## 스키마를 분리하지 않기 { #do-not-separate-schemas } + +이제 어떤 경우에는 **입력과 출력에 대해 같은 스키마를 사용**하고 싶을 수도 있습니다. + +가장 대표적인 경우는, 이미 자동 생성된 클라이언트 코드/SDK가 있고, 아직은 그 자동 생성된 클라이언트 코드/SDK들을 전부 업데이트하고 싶지 않은 경우입니다. 언젠가는 업데이트해야 할 가능성이 높지만, 지금 당장은 아닐 수도 있습니다. + +그런 경우에는, **FastAPI**에서 `separate_input_output_schemas=False` 파라미터로 이 기능을 비활성화할 수 있습니다. + +/// info | 정보 + +`separate_input_output_schemas` 지원은 FastAPI `0.102.0`에 추가되었습니다. 🤓 + +/// + +{* ../../docs_src/separate_openapi_schemas/tutorial002_py310.py hl[10] *} + +### 문서에서 입력과 출력 모델에 같은 스키마 사용 { #same-schema-for-input-and-output-models-in-docs } + +이제 모델에 대해 입력과 출력 모두에 사용되는 단일 스키마(오직 `Item`만)가 생성되며, `description`은 **필수가 아닌 것**으로 표시됩니다: + +<div class="screenshot"> +<img src="/img/tutorial/separate-openapi-schemas/image05.png"> +</div> diff --git a/docs/ko/docs/how-to/testing-database.md b/docs/ko/docs/how-to/testing-database.md new file mode 100644 index 000000000..2d7798d70 --- /dev/null +++ b/docs/ko/docs/how-to/testing-database.md @@ -0,0 +1,7 @@ +# 데이터베이스 테스트하기 { #testing-a-database } + +데이터베이스, SQL, SQLModel에 대해서는 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel 문서</a>에서 학습할 수 있습니다. 🤓 + +<a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/" class="external-link" target="_blank">FastAPI에서 SQLModel을 사용하는 방법에 대한 미니 튜토리얼</a>도 있습니다. ✨ + +해당 튜토리얼에는 <a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/tests/" class="external-link" target="_blank">SQL 데이터베이스 테스트</a>에 대한 섹션도 포함되어 있습니다. 😎 diff --git a/docs/ko/docs/tutorial/bigger-applications.md b/docs/ko/docs/tutorial/bigger-applications.md new file mode 100644 index 000000000..cfc3900d4 --- /dev/null +++ b/docs/ko/docs/tutorial/bigger-applications.md @@ -0,0 +1,504 @@ +# 더 큰 애플리케이션 - 여러 파일 { #bigger-applications-multiple-files } + +애플리케이션이나 웹 API를 만들 때, 모든 것을 하나의 파일에 담을 수 있는 경우는 드뭅니다. + +**FastAPI**는 모든 유연성을 유지하면서도 애플리케이션을 구조화할 수 있게 해주는 편리한 도구를 제공합니다. + +/// info | 정보 + +Flask를 사용해 보셨다면, 이는 Flask의 Blueprints에 해당하는 개념입니다. + +/// + +## 예시 파일 구조 { #an-example-file-structure } + +다음과 같은 파일 구조가 있다고 해봅시다: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   ├── dependencies.py +│   └── routers +│   │ ├── __init__.py +│   │ ├── items.py +│   │ └── users.py +│   └── internal +│   ├── __init__.py +│   └── admin.py +``` + +/// tip | 팁 + +`__init__.py` 파일이 여러 개 있습니다: 각 디렉터리 또는 하위 디렉터리에 하나씩 있습니다. + +이 파일들이 한 파일의 코드를 다른 파일로 import할 수 있게 해줍니다. + +예를 들어 `app/main.py`에는 다음과 같은 줄이 있을 수 있습니다: + +``` +from app.routers import items +``` + +/// + +* `app` 디렉터리에는 모든 것이 들어 있습니다. 그리고 비어 있는 파일 `app/__init__.py`가 있어 "Python package"(“Python modules”의 모음)인 `app`이 됩니다. +* `app/main.py` 파일이 있습니다. Python package(`__init__.py` 파일이 있는 디렉터리) 안에 있으므로, 이 package의 "module"입니다: `app.main`. +* `app/dependencies.py` 파일도 있습니다. `app/main.py`와 마찬가지로 "module"입니다: `app.dependencies`. +* `app/routers/` 하위 디렉터리가 있고, 여기에 또 `__init__.py` 파일이 있으므로 "Python subpackage"입니다: `app.routers`. +* `app/routers/items.py` 파일은 `app/routers/` package 안에 있으므로, submodule입니다: `app.routers.items`. +* `app/routers/users.py`도 동일하게 또 다른 submodule입니다: `app.routers.users`. +* `app/internal/` 하위 디렉터리도 있고 여기에 `__init__.py`가 있으므로 또 다른 "Python subpackage"입니다: `app.internal`. +* 그리고 `app/internal/admin.py` 파일은 또 다른 submodule입니다: `app.internal.admin`. + +<img src="/img/tutorial/bigger-applications/package.drawio.svg"> + +같은 파일 구조에 주석을 추가하면 다음과 같습니다: + +```bash +. +├── app # "app" is a Python package +│   ├── __init__.py # this file makes "app" a "Python package" +│   ├── main.py # "main" module, e.g. import app.main +│   ├── dependencies.py # "dependencies" module, e.g. import app.dependencies +│   └── routers # "routers" is a "Python subpackage" +│   │ ├── __init__.py # makes "routers" a "Python subpackage" +│   │ ├── items.py # "items" submodule, e.g. import app.routers.items +│   │ └── users.py # "users" submodule, e.g. import app.routers.users +│   └── internal # "internal" is a "Python subpackage" +│   ├── __init__.py # makes "internal" a "Python subpackage" +│   └── admin.py # "admin" submodule, e.g. import app.internal.admin +``` + +## `APIRouter` { #apirouter } + +사용자만 처리하는 전용 파일이 `/app/routers/users.py`의 submodule이라고 해봅시다. + +코드를 정리하기 위해 사용자와 관련된 *path operations*를 나머지 코드와 분리해 두고 싶을 것입니다. + +하지만 이것은 여전히 같은 **FastAPI** 애플리케이션/웹 API의 일부입니다(같은 "Python Package"의 일부입니다). + +`APIRouter`를 사용해 해당 모듈의 *path operations*를 만들 수 있습니다. + +### `APIRouter` import하기 { #import-apirouter } + +`FastAPI` 클래스와 동일한 방식으로 import하고 "instance"를 생성합니다: + +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} + +### `APIRouter`로 *path operations* 만들기 { #path-operations-with-apirouter } + +그 다음 이를 사용해 *path operations*를 선언합니다. + +`FastAPI` 클래스를 사용할 때와 동일한 방식으로 사용합니다: + +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} + +`APIRouter`는 "미니 `FastAPI`" 클래스라고 생각할 수 있습니다. + +동일한 옵션들이 모두 지원됩니다. + +동일한 `parameters`, `responses`, `dependencies`, `tags` 등등. + +/// tip | 팁 + +이 예시에서는 변수 이름이 `router`이지만, 원하는 이름으로 지어도 됩니다. + +/// + +이제 이 `APIRouter`를 메인 `FastAPI` 앱에 포함(include)할 것이지만, 먼저 dependencies와 다른 `APIRouter` 하나를 확인해 보겠습니다. + +## Dependencies { #dependencies } + +애플리케이션의 여러 위치에서 사용되는 dependencies가 일부 필요하다는 것을 알 수 있습니다. + +그래서 이를 별도의 `dependencies` 모듈(`app/dependencies.py`)에 둡니다. + +이제 간단한 dependency를 사용해 커스텀 `X-Token` 헤더를 읽어 보겠습니다: + +{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} + +/// tip | 팁 + +이 예시를 단순화하기 위해 임의로 만든 헤더를 사용하고 있습니다. + +하지만 실제 상황에서는 통합된 [Security 유틸리티](security/index.md){.internal-link target=_blank}를 사용하는 것이 더 좋은 결과를 얻을 수 있습니다. + +/// + +## `APIRouter`가 있는 또 다른 모듈 { #another-module-with-apirouter } + +애플리케이션의 "items"를 처리하는 전용 endpoint들도 `app/routers/items.py` 모듈에 있다고 해봅시다. + +여기에는 다음에 대한 *path operations*가 있습니다: + +* `/items/` +* `/items/{item_id}` + +구조는 `app/routers/users.py`와 완전히 동일합니다. + +하지만 우리는 조금 더 똑똑하게, 코드를 약간 단순화하고 싶습니다. + +이 모듈의 모든 *path operations*에는 다음이 동일하게 적용됩니다: + +* 경로 `prefix`: `/items`. +* `tags`: (태그 하나: `items`). +* 추가 `responses`. +* `dependencies`: 모두 우리가 만든 `X-Token` dependency가 필요합니다. + +따라서 각 *path operation*마다 매번 모두 추가하는 대신, `APIRouter`에 한 번에 추가할 수 있습니다. + +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} + +각 *path operation*의 경로는 다음처럼 `/`로 시작해야 하므로: + +```Python hl_lines="1" +@router.get("/{item_id}") +async def read_item(item_id: str): + ... +``` + +...prefix에는 마지막 `/`가 포함되면 안 됩니다. + +따라서 이 경우 prefix는 `/items`입니다. + +또한 이 router에 포함된 모든 *path operations*에 적용될 `tags` 목록과 추가 `responses`도 넣을 수 있습니다. + +그리고 router의 모든 *path operations*에 추가될 `dependencies` 목록도 추가할 수 있으며, 해당 경로들로 들어오는 각 요청마다 실행/해결됩니다. + +/// tip | 팁 + +[*path operation decorator의 dependencies*](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}와 마찬가지로, *path operation function*에 어떤 값도 전달되지 않습니다. + +/// + +최종적으로 item 경로는 다음과 같습니다: + +* `/items/` +* `/items/{item_id}` + +...의도한 그대로입니다. + +* 단일 문자열 `"items"`를 포함하는 태그 목록으로 표시됩니다. + * 이 "tags"는 자동 대화형 문서 시스템(OpenAPI 사용)에 특히 유용합니다. +* 모두 미리 정의된 `responses`를 포함합니다. +* 이 모든 *path operations*는 실행되기 전에 `dependencies` 목록이 평가/실행됩니다. + * 특정 *path operation*에 dependencies를 추가로 선언하면 **그것들도 실행됩니다**. + * router dependencies가 먼저 실행되고, 그 다음에 [decorator의 `dependencies`](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, 그리고 일반 파라미터 dependencies가 실행됩니다. + * [`scopes`가 있는 `Security` dependencies](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}도 추가할 수 있습니다. + +/// tip | 팁 + +`APIRouter`에 `dependencies`를 두는 것은 예를 들어 전체 *path operations* 그룹에 인증을 요구할 때 사용할 수 있습니다. 각 경로 처리에 개별적으로 dependencies를 추가하지 않아도 됩니다. + +/// + +/// check | 확인 + +`prefix`, `tags`, `responses`, `dependencies` 파라미터는 (다른 많은 경우와 마찬가지로) 코드 중복을 피하도록 도와주는 **FastAPI**의 기능입니다. + +/// + +### dependencies import하기 { #import-the-dependencies } + +이 코드는 모듈 `app.routers.items`, 파일 `app/routers/items.py`에 있습니다. + +그리고 dependency 함수는 모듈 `app.dependencies`, 파일 `app/dependencies.py`에서 가져와야 합니다. + +그래서 dependencies에 대해 `..`를 사용하는 상대 import를 사용합니다: + +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *} + +#### 상대 import가 동작하는 방식 { #how-relative-imports-work } + +/// tip | 팁 + +import가 동작하는 방식을 완벽히 알고 있다면, 아래 다음 섹션으로 넘어가세요. + +/// + +다음과 같이 점 하나 `.`를 쓰면: + +```Python +from .dependencies import get_token_header +``` + +의미는 다음과 같습니다: + +* 이 모듈(파일 `app/routers/items.py`)이 속한 같은 package(디렉터리 `app/routers/`)에서 시작해서... +* `dependencies` 모듈(가상의 파일 `app/routers/dependencies.py`)을 찾고... +* 그 안에서 함수 `get_token_header`를 import합니다. + +하지만 그 파일은 존재하지 않습니다. dependencies는 `app/dependencies.py` 파일에 있습니다. + +우리 앱/파일 구조를 다시 떠올려 보세요: + +<img src="/img/tutorial/bigger-applications/package.drawio.svg"> + +--- + +다음처럼 점 두 개 `..`를 쓰면: + +```Python +from ..dependencies import get_token_header +``` + +의미는 다음과 같습니다: + +* 이 모듈(파일 `app/routers/items.py`)이 속한 같은 package(디렉터리 `app/routers/`)에서 시작해서... +* 상위 package(디렉터리 `app/`)로 올라가고... +* 그 안에서 `dependencies` 모듈(파일 `app/dependencies.py`)을 찾고... +* 그 안에서 함수 `get_token_header`를 import합니다. + +이렇게 하면 제대로 동작합니다! 🎉 + +--- + +같은 방식으로 점 세 개 `...`를 사용했다면: + +```Python +from ...dependencies import get_token_header +``` + +의미는 다음과 같습니다: + +* 이 모듈(파일 `app/routers/items.py`)이 속한 같은 package(디렉터리 `app/routers/`)에서 시작해서... +* 상위 package(디렉터리 `app/`)로 올라가고... +* 그 package의 상위로 또 올라가는데(상위 package가 없습니다, `app`이 최상위입니다 😱)... +* 그 안에서 `dependencies` 모듈(파일 `app/dependencies.py`)을 찾고... +* 그 안에서 함수 `get_token_header`를 import합니다. + +이는 `app/` 위쪽의 어떤 package(자신의 `__init__.py` 파일 등을 가진)에 대한 참조가 됩니다. 하지만 우리는 그런 것이 없습니다. 그래서 이 예시에서는 에러가 발생합니다. 🚨 + +이제 어떻게 동작하는지 알았으니, 앱이 얼마나 복잡하든 상대 import를 사용할 수 있습니다. 🤓 + +### 커스텀 `tags`, `responses`, `dependencies` 추가하기 { #add-some-custom-tags-responses-and-dependencies } + +`APIRouter`에 이미 prefix `/items`와 `tags=["items"]`를 추가했기 때문에 각 *path operation*에 이를 추가하지 않습니다. + +하지만 특정 *path operation*에만 적용될 _추가_ `tags`를 더할 수도 있고, 그 *path operation* 전용의 추가 `responses`도 넣을 수 있습니다: + +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} + +/// tip | 팁 + +이 마지막 경로 처리는 `["items", "custom"]` 태그 조합을 갖게 됩니다. + +그리고 문서에는 `404`용 응답과 `403`용 응답, 두 가지 모두가 표시됩니다. + +/// + +## 메인 `FastAPI` { #the-main-fastapi } + +이제 `app/main.py` 모듈을 봅시다. + +여기에서 `FastAPI` 클래스를 import하고 사용합니다. + +이 파일은 모든 것을 하나로 엮는 애플리케이션의 메인 파일이 될 것입니다. + +그리고 대부분의 로직이 각자의 특정 모듈로 분리되어 있으므로, 메인 파일은 꽤 단순해집니다. + +### `FastAPI` import하기 { #import-fastapi } + +평소처럼 `FastAPI` 클래스를 import하고 생성합니다. + +또한 각 `APIRouter`의 dependencies와 결합될 [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank}도 선언할 수 있습니다: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} + +### `APIRouter` import하기 { #import-the-apirouter } + +이제 `APIRouter`가 있는 다른 submodule들을 import합니다: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *} + +`app/routers/users.py`와 `app/routers/items.py` 파일은 같은 Python package `app`에 속한 submodule들이므로, 점 하나 `.`를 사용해 "상대 import"로 가져올 수 있습니다. + +### import가 동작하는 방식 { #how-the-importing-works } + +다음 구문은: + +```Python +from .routers import items, users +``` + +의미는 다음과 같습니다: + +* 이 모듈(파일 `app/main.py`)이 속한 같은 package(디렉터리 `app/`)에서 시작해서... +* subpackage `routers`(디렉터리 `app/routers/`)를 찾고... +* 그 안에서 submodule `items`(파일 `app/routers/items.py`)와 `users`(파일 `app/routers/users.py`)를 import합니다... + +`items` 모듈에는 `router` 변수(`items.router`)가 있습니다. 이는 `app/routers/items.py` 파일에서 만든 것과 동일하며 `APIRouter` 객체입니다. + +그리고 `users` 모듈도 같은 방식입니다. + +다음처럼 import할 수도 있습니다: + +```Python +from app.routers import items, users +``` + +/// info | 정보 + +첫 번째 버전은 "상대 import"입니다: + +```Python +from .routers import items, users +``` + +두 번째 버전은 "절대 import"입니다: + +```Python +from app.routers import items, users +``` + +Python Packages와 Modules에 대해 더 알아보려면 <a href="https://docs.python.org/3/tutorial/modules.html" class="external-link" target="_blank">Modules에 대한 Python 공식 문서</a>를 읽어보세요. + +/// + +### 이름 충돌 피하기 { #avoid-name-collisions } + +submodule `items`를 직접 import하고, 그 안의 `router` 변수만 import하지는 않습니다. + +이는 submodule `users`에도 `router`라는 이름의 변수가 있기 때문입니다. + +만약 다음처럼 순서대로 import했다면: + +```Python +from .routers.items import router +from .routers.users import router +``` + +`users`의 `router`가 `items`의 `router`를 덮어써서 동시에 사용할 수 없게 됩니다. + +따라서 같은 파일에서 둘 다 사용할 수 있도록 submodule들을 직접 import합니다: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *} + +### `users`와 `items`용 `APIRouter` 포함하기 { #include-the-apirouters-for-users-and-items } + +이제 submodule `users`와 `items`의 `router`를 포함해 봅시다: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *} + +/// info | 정보 + +`users.router`는 `app/routers/users.py` 파일 안의 `APIRouter`를 담고 있습니다. + +`items.router`는 `app/routers/items.py` 파일 안의 `APIRouter`를 담고 있습니다. + +/// + +`app.include_router()`로 각 `APIRouter`를 메인 `FastAPI` 애플리케이션에 추가할 수 있습니다. + +그 router의 모든 route가 애플리케이션의 일부로 포함됩니다. + +/// note Technical Details | 기술 세부사항 + +내부적으로는 `APIRouter`에 선언된 각 *path operation*마다 *path operation*을 실제로 생성합니다. + +즉, 내부적으로는 모든 것이 동일한 하나의 앱인 것처럼 동작합니다. + +/// + +/// check | 확인 + +router를 포함(include)할 때 성능을 걱정할 필요는 없습니다. + +이 작업은 마이크로초 단위이며 시작 시에만 발생합니다. + +따라서 성능에 영향을 주지 않습니다. ⚡ + +/// + +### 커스텀 `prefix`, `tags`, `responses`, `dependencies`로 `APIRouter` 포함하기 { #include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies } + +이제 조직에서 `app/internal/admin.py` 파일을 받았다고 가정해 봅시다. + +여기에는 조직에서 여러 프로젝트 간에 공유하는 관리자용 *path operations*가 있는 `APIRouter`가 들어 있습니다. + +이 예시에서는 매우 단순하게 만들겠습니다. 하지만 조직 내 다른 프로젝트와 공유되기 때문에, 이를 수정할 수 없어 `prefix`, `dependencies`, `tags` 등을 `APIRouter`에 직접 추가할 수 없다고 해봅시다: + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +하지만 `APIRouter`를 포함할 때 커스텀 `prefix`를 지정해 모든 *path operations*가 `/admin`으로 시작하게 하고, 이 프로젝트에서 이미 가진 `dependencies`로 보호하고, `tags`와 `responses`도 포함하고 싶습니다. + +원래 `APIRouter`를 수정하지 않고도 `app.include_router()`에 파라미터를 전달해서 이를 선언할 수 있습니다: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *} + +이렇게 하면 원래 `APIRouter`는 수정되지 않으므로, 조직 내 다른 프로젝트에서도 동일한 `app/internal/admin.py` 파일을 계속 공유할 수 있습니다. + +결과적으로 우리 앱에서 `admin` 모듈의 각 *path operations*는 다음을 갖게 됩니다: + +* prefix `/admin`. +* tag `admin`. +* dependency `get_token_header`. +* 응답 `418`. 🍵 + +하지만 이는 우리 앱에서 그 `APIRouter`에만 영향을 주며, 이를 사용하는 다른 코드에는 영향을 주지 않습니다. + +따라서 다른 프로젝트들은 같은 `APIRouter`를 다른 인증 방식으로 사용할 수도 있습니다. + +### *path operation* 포함하기 { #include-a-path-operation } + +*path operations*를 `FastAPI` 앱에 직접 추가할 수도 있습니다. + +여기서는 가능하다는 것을 보여주기 위해... 그냥 해봅니다 🤷: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *} + +그리고 `app.include_router()`로 추가한 다른 모든 *path operations*와 함께 올바르게 동작합니다. + +/// info | 정보 + +**참고**: 이는 매우 기술적인 세부사항이라 아마 **그냥 건너뛰어도 됩니다**. + +--- + +`APIRouter`는 "mount"되는 것이 아니며, 애플리케이션의 나머지 부분과 격리되어 있지 않습니다. + +이는 OpenAPI 스키마와 사용자 인터페이스에 그들의 *path operations*를 포함시키고 싶기 때문입니다. + +나머지와 독립적으로 격리해 "mount"할 수 없으므로, *path operations*는 직접 포함되는 것이 아니라 "clone"(재생성)됩니다. + +/// + +## 자동 API 문서 확인하기 { #check-the-automatic-api-docs } + +이제 앱을 실행하세요: + +<div class="termy"> + +```console +$ fastapi dev app/main.py + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +그리고 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>에서 문서를 여세요. + +올바른 경로(및 prefix)와 올바른 태그를 사용해, 모든 submodule의 경로를 포함한 자동 API 문서를 볼 수 있습니다: + +<img src="/img/tutorial/bigger-applications/image01.png"> + +## 같은 router를 다른 `prefix`로 여러 번 포함하기 { #include-the-same-router-multiple-times-with-different-prefix } + +`.include_router()`를 사용해 *같은* router를 서로 다른 prefix로 여러 번 포함할 수도 있습니다. + +예를 들어 `/api/v1`과 `/api/latest`처럼 서로 다른 prefix로 동일한 API를 노출할 때 유용할 수 있습니다. + +이는 고급 사용 방식이라 실제로 필요하지 않을 수도 있지만, 필요할 때를 위해 제공됩니다. + +## `APIRouter`에 다른 `APIRouter` 포함하기 { #include-an-apirouter-in-another } + +`APIRouter`를 `FastAPI` 애플리케이션에 포함할 수 있는 것과 같은 방식으로, 다음을 사용해 `APIRouter`를 다른 `APIRouter`에 포함할 수 있습니다: + +```Python +router.include_router(other_router) +``` + +`FastAPI` 앱에 `router`를 포함하기 전에 수행해야 하며, 그래야 `other_router`의 *path operations*도 함께 포함됩니다. diff --git a/docs/ko/docs/tutorial/body-updates.md b/docs/ko/docs/tutorial/body-updates.md new file mode 100644 index 000000000..3719e1ffa --- /dev/null +++ b/docs/ko/docs/tutorial/body-updates.md @@ -0,0 +1,100 @@ +# Body - 업데이트 { #body-updates } + +## `PUT`으로 교체 업데이트하기 { #update-replacing-with-put } + +항목을 업데이트하려면 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT" class="external-link" target="_blank">HTTP `PUT`</a> 작업을 사용할 수 있습니다. + +`jsonable_encoder`를 사용해 입력 데이터를 JSON으로 저장할 수 있는 데이터로 변환할 수 있습니다(예: NoSQL 데이터베이스 사용 시). 예를 들어 `datetime`을 `str`로 변환하는 경우입니다. + +{* ../../docs_src/body_updates/tutorial001_py310.py hl[28:33] *} + +`PUT`은 기존 데이터를 **대체**해야 하는 데이터를 받는 데 사용합니다. + +### 대체 시 주의사항 { #warning-about-replacing } + +즉, `PUT`으로 항목 `bar`를 업데이트하면서 다음과 같은 body를 보낸다면: + +```Python +{ + "name": "Barz", + "price": 3, + "description": None, +} +``` + +이미 저장된 속성 `"tax": 20.2`가 포함되어 있지 않기 때문에, 입력 모델은 `"tax": 10.5`라는 기본값을 사용하게 됩니다. + +그리고 데이터는 그 “새로운” `tax` 값 `10.5`로 저장됩니다. + +## `PATCH`로 부분 업데이트하기 { #partial-updates-with-patch } + +<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH" class="external-link" target="_blank">HTTP `PATCH`</a> 작업을 사용해 데이터를 *부분적으로* 업데이트할 수도 있습니다. + +이는 업데이트하려는 데이터만 보내고, 나머지는 그대로 두는 것을 의미합니다. + +/// note | 참고 + +`PATCH`는 `PUT`보다 덜 일반적으로 사용되고 덜 알려져 있습니다. + +그리고 많은 팀이 부분 업데이트에도 `PUT`만 사용합니다. + +여러분은 원하는 방식으로 **자유롭게** 사용할 수 있으며, **FastAPI**는 어떤 제한도 강제하지 않습니다. + +다만 이 가이드는 의도된 사용 방식이 대략 어떻게 되는지를 보여줍니다. + +/// + +### Pydantic의 `exclude_unset` 파라미터 사용하기 { #using-pydantics-exclude-unset-parameter } + +부분 업데이트를 받으려면 Pydantic 모델의 `.model_dump()`에서 `exclude_unset` 파라미터를 사용하는 것이 매우 유용합니다. + +예: `item.model_dump(exclude_unset=True)`. + +이는 `item` 모델을 만들 때 실제로 설정된 데이터만 포함하는 `dict`를 생성하고, 기본값은 제외합니다. + +그 다음 이를 사용해 (요청에서 전송되어) 설정된 데이터만 포함하고 기본값은 생략한 `dict`를 만들 수 있습니다: + +{* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} + +### Pydantic의 `update` 파라미터 사용하기 { #using-pydantics-update-parameter } + +이제 `.model_copy()`를 사용해 기존 모델의 복사본을 만들고, 업데이트할 데이터가 들어있는 `dict`를 `update` 파라미터로 전달할 수 있습니다. + +예: `stored_item_model.model_copy(update=update_data)`: + +{* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} + +### 부분 업데이트 요약 { #partial-updates-recap } + +정리하면, 부분 업데이트를 적용하려면 다음을 수행합니다: + +* (선택 사항) `PUT` 대신 `PATCH`를 사용합니다. +* 저장된 데이터를 조회합니다. +* 그 데이터를 Pydantic 모델에 넣습니다. +* 입력 모델에서 기본값이 제외된 `dict`를 생성합니다(`exclude_unset` 사용). + * 이렇게 하면 모델의 기본값으로 이미 저장된 값을 덮어쓰지 않고, 사용자가 실제로 설정한 값만 업데이트할 수 있습니다. +* 저장된 모델의 복사본을 만들고, 받은 부분 업데이트로 해당 속성들을 갱신합니다(`update` 파라미터 사용). +* 복사한 모델을 DB에 저장할 수 있는 형태로 변환합니다(예: `jsonable_encoder` 사용). + * 이는 모델의 `.model_dump()` 메서드를 다시 사용하는 것과 비슷하지만, JSON으로 변환 가능한 데이터 타입으로 값이 확실히 변환되도록 보장합니다(예: `datetime` → `str`). +* 데이터를 DB에 저장합니다. +* 업데이트된 모델을 반환합니다. + +{* ../../docs_src/body_updates/tutorial002_py310.py hl[28:35] *} + +/// tip | 팁 + +동일한 기법을 HTTP `PUT` 작업에서도 실제로 사용할 수 있습니다. + +하지만 여기의 예시는 이런 사용 사례를 위해 만들어진 `PATCH`를 사용합니다. + +/// + +/// note | 참고 + +입력 모델은 여전히 검증된다는 점에 유의하세요. + +따라서 모든 속성을 생략할 수 있는 부분 업데이트를 받으려면, 모든 속성이 optional로 표시된(기본값을 가지거나 `None`을 기본값으로 가지는) 모델이 필요합니다. + +**업데이트**를 위한 “모든 값이 optional인” 모델과, **생성**을 위한 “필수 값이 있는” 모델을 구분하려면 [추가 모델](extra-models.md){.internal-link target=_blank}에 설명된 아이디어를 사용할 수 있습니다. + +/// diff --git a/docs/ko/docs/tutorial/dependencies/sub-dependencies.md b/docs/ko/docs/tutorial/dependencies/sub-dependencies.md new file mode 100644 index 000000000..d81ccf00d --- /dev/null +++ b/docs/ko/docs/tutorial/dependencies/sub-dependencies.md @@ -0,0 +1,105 @@ +# 하위 의존성 { #sub-dependencies } + +**하위 의존성**을 가지는 의존성을 만들 수 있습니다. + +필요한 만큼 **깊게** 중첩할 수도 있습니다. + +이것을 해결하는 일은 **FastAPI**가 알아서 처리합니다. + +## 첫 번째 의존성 "dependable" { #first-dependency-dependable } + +다음과 같이 첫 번째 의존성("dependable")을 만들 수 있습니다: + +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[8:9] *} + +이 의존성은 선택적 쿼리 파라미터 `q`를 `str`로 선언하고, 그대로 반환합니다. + +매우 단순한 예시(그다지 유용하진 않음)이지만, 하위 의존성이 어떻게 동작하는지에 집중하는 데 도움이 됩니다. + +## 두 번째 의존성 "dependable"과 "dependant" { #second-dependency-dependable-and-dependant } + +그다음, 또 다른 의존성 함수("dependable")를 만들 수 있는데, 이 함수는 동시에 자기 자신의 의존성도 선언합니다(그래서 "dependant"이기도 합니다): + +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[13] *} + +선언된 파라미터를 살펴보겠습니다: + +* 이 함수 자체가 의존성("dependable")이지만, 다른 의존성도 하나 선언합니다(즉, 다른 무언가에 "의존"합니다). + * `query_extractor`에 의존하며, 그 반환값을 파라미터 `q`에 할당합니다. +* 또한 선택적 `last_query` 쿠키를 `str`로 선언합니다. + * 사용자가 쿼리 `q`를 제공하지 않았다면, 이전에 쿠키에 저장해 둔 마지막 쿼리를 사용합니다. + +## 의존성 사용하기 { #use-the-dependency } + +그다음 다음과 같이 의존성을 사용할 수 있습니다: + +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[23] *} + +/// info | 정보 + +*경로 처리 함수*에서는 `query_or_cookie_extractor`라는 의존성 하나만 선언하고 있다는 점에 주목하세요. + +하지만 **FastAPI**는 `query_or_cookie_extractor`를 호출하는 동안 그 결과를 전달하기 위해, 먼저 `query_extractor`를 해결해야 한다는 것을 알고 있습니다. + +/// + +```mermaid +graph TB + +query_extractor(["query_extractor"]) +query_or_cookie_extractor(["query_or_cookie_extractor"]) + +read_query["/items/"] + +query_extractor --> query_or_cookie_extractor --> read_query +``` + +## 같은 의존성을 여러 번 사용하기 { #using-the-same-dependency-multiple-times } + +같은 *경로 처리*에 대해 의존성 중 하나가 여러 번 선언되는 경우(예: 여러 의존성이 공통 하위 의존성을 갖는 경우), **FastAPI**는 그 하위 의존성을 요청당 한 번만 호출해야 한다는 것을 알고 있습니다. + +그리고 같은 요청에 대해 동일한 의존성을 여러 번 호출하는 대신, 반환값을 <abbr title="계산/생성된 값을 저장해 두었다가, 다시 계산하지 않고 재사용하기 위한 유틸리티/시스템.">"cache"</abbr>에 저장하고, 그 요청에서 해당 값이 필요한 모든 "dependants"에 전달합니다. + +고급 시나리오로, 같은 요청에서 "cached" 값을 쓰는 대신 매 단계마다(아마도 여러 번) 의존성이 호출되어야 한다는 것을 알고 있다면, `Depends`를 사용할 때 `use_cache=False` 파라미터를 설정할 수 있습니다: + +//// tab | Python 3.9+ + +```Python hl_lines="1" +async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]): + return {"fresh_value": fresh_value} +``` + +//// + +//// tab | Python 3.9+ 비 Annotated + +/// tip | 팁 + +가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다. + +/// + +```Python hl_lines="1" +async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): + return {"fresh_value": fresh_value} +``` + +//// + +## 정리 { #recap } + +여기서 사용한 그럴듯한 용어들을 제외하면, **Dependency Injection** 시스템은 꽤 단순합니다. + +*경로 처리 함수*와 같은 형태의 함수들일 뿐입니다. + +하지만 여전히 매우 강력하며, 임의로 깊게 중첩된 의존성 "그래프"(트리)를 선언할 수 있습니다. + +/// tip | 팁 + +이 단순한 예시만 보면 그다지 유용해 보이지 않을 수도 있습니다. + +하지만 **보안**에 관한 챕터에서 이것이 얼마나 유용한지 보게 될 것입니다. + +또한 얼마나 많은 코드를 아껴주는지도 보게 될 것입니다. + +/// diff --git a/docs/ko/docs/tutorial/handling-errors.md b/docs/ko/docs/tutorial/handling-errors.md new file mode 100644 index 000000000..7cc37e80c --- /dev/null +++ b/docs/ko/docs/tutorial/handling-errors.md @@ -0,0 +1,244 @@ +# 오류 처리 { #handling-errors } + +API를 사용하는 클라이언트에 오류를 알려야 하는 상황은 많이 있습니다. + +이 클라이언트는 프론트엔드가 있는 브라우저일 수도 있고, 다른 사람이 작성한 코드일 수도 있고, IoT 장치일 수도 있습니다. + +클라이언트에 다음과 같은 내용을 알려야 할 수도 있습니다: + +* 클라이언트가 해당 작업을 수행할 충분한 권한이 없습니다. +* 클라이언트가 해당 리소스에 접근할 수 없습니다. +* 클라이언트가 접근하려고 한 항목이 존재하지 않습니다. +* 등등. + +이런 경우 보통 **400**번대(400에서 499) 범위의 **HTTP 상태 코드**를 반환합니다. + +이는 200번대 HTTP 상태 코드(200에서 299)와 비슷합니다. "200" 상태 코드는 어떤 형태로든 요청이 "성공"했음을 의미합니다. + +400번대 상태 코드는 클라이언트 측에서 오류가 발생했음을 의미합니다. + +**"404 Not Found"** 오류(그리고 농담들)도 다들 기억하시죠? + +## `HTTPException` 사용하기 { #use-httpexception } + +클라이언트에 오류가 포함된 HTTP 응답을 반환하려면 `HTTPException`을 사용합니다. + +### `HTTPException` 가져오기 { #import-httpexception } + +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[1] *} + +### 코드에서 `HTTPException` 발생시키기 { #raise-an-httpexception-in-your-code } + +`HTTPException`은 API와 관련된 추가 데이터를 가진 일반적인 Python 예외입니다. + +Python 예외이므로 `return` 하는 것이 아니라 `raise` 합니다. + +이는 또한, *경로 처리 함수* 내부에서 호출하는 유틸리티 함수 안에서 `HTTPException`을 `raise`하면, *경로 처리 함수*의 나머지 코드는 실행되지 않고 즉시 해당 요청이 종료되며 `HTTPException`의 HTTP 오류가 클라이언트로 전송된다는 뜻입니다. + +값을 반환하는 것보다 예외를 발생시키는 것의 이점은 의존성과 보안에 대한 섹션에서 더 분명해집니다. + +이 예시에서는, 클라이언트가 존재하지 않는 ID로 항목을 요청하면 상태 코드 `404`로 예외를 발생시킵니다: + +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[11] *} + +### 결과 응답 { #the-resulting-response } + +클라이언트가 `http://example.com/items/foo`( `item_id` `"foo"`)를 요청하면, HTTP 상태 코드 200과 다음 JSON 응답을 받습니다: + +```JSON +{ + "item": "The Foo Wrestlers" +} +``` + +하지만 클라이언트가 `http://example.com/items/bar`(존재하지 않는 `item_id` `"bar"`)를 요청하면, HTTP 상태 코드 404("not found" 오류)와 다음 JSON 응답을 받습니다: + +```JSON +{ + "detail": "Item not found" +} +``` + +/// tip | 팁 + +`HTTPException`을 발생시킬 때 `detail` 파라미터로 `str`만 전달할 수 있는 것이 아니라, JSON으로 변환할 수 있는 어떤 값이든 전달할 수 있습니다. + +`dict`, `list` 등을 전달할 수 있습니다. + +이들은 **FastAPI**가 자동으로 처리해 JSON으로 변환합니다. + +/// + +## 커스텀 헤더 추가하기 { #add-custom-headers } + +HTTP 오류에 커스텀 헤더를 추가할 수 있으면 유용한 상황이 있습니다. 예를 들어 특정 보안 유형에서 그렇습니다. + +아마 코드에서 직접 사용할 일은 거의 없을 것입니다. + +하지만 고급 시나리오에서 필요하다면 커스텀 헤더를 추가할 수 있습니다: + +{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *} + +## 커스텀 예외 핸들러 설치하기 { #install-custom-exception-handlers } + +<a href="https://www.starlette.dev/exceptions/" class="external-link" target="_blank">Starlette의 동일한 예외 유틸리티</a>를 사용해 커스텀 예외 핸들러를 추가할 수 있습니다. + +여러분(또는 사용하는 라이브러리)이 `raise`할 수 있는 커스텀 예외 `UnicornException`이 있다고 가정해 봅시다. + +그리고 이 예외를 FastAPI에서 전역적으로 처리하고 싶다고 해봅시다. + +`@app.exception_handler()`로 커스텀 예외 핸들러를 추가할 수 있습니다: + +{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *} + +여기서 `/unicorns/yolo`를 요청하면, *경로 처리*가 `UnicornException`을 `raise`합니다. + +하지만 `unicorn_exception_handler`가 이를 처리합니다. + +따라서 HTTP 상태 코드 `418`과 다음 JSON 내용을 가진 깔끔한 오류를 받게 됩니다: + +```JSON +{"message": "Oops! yolo did something. There goes a rainbow..."} +``` + +/// note | 기술 세부사항 + +`from starlette.requests import Request`와 `from starlette.responses import JSONResponse`를 사용할 수도 있습니다. + +**FastAPI**는 개발자의 편의를 위해 `starlette.responses`를 `fastapi.responses`로도 동일하게 제공합니다. 하지만 사용 가능한 대부분의 응답은 Starlette에서 직접 옵니다. `Request`도 마찬가지입니다. + +/// + +## 기본 예외 핸들러 오버라이드하기 { #override-the-default-exception-handlers } + +**FastAPI**에는 몇 가지 기본 예외 핸들러가 있습니다. + +이 핸들러들은 `HTTPException`을 `raise`했을 때, 그리고 요청에 유효하지 않은 데이터가 있을 때 기본 JSON 응답을 반환하는 역할을 합니다. + +이 예외 핸들러들을 여러분의 것으로 오버라이드할 수 있습니다. + +### 요청 검증 예외 오버라이드하기 { #override-request-validation-exceptions } + +요청에 유효하지 않은 데이터가 포함되면, **FastAPI**는 내부적으로 `RequestValidationError`를 `raise`합니다. + +그리고 이에 대한 기본 예외 핸들러도 포함되어 있습니다. + +이를 오버라이드하려면 `RequestValidationError`를 가져오고, `@app.exception_handler(RequestValidationError)`로 예외 핸들러를 데코레이트해 사용하세요. + +예외 핸들러는 `Request`와 예외를 받습니다. + +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *} + +이제 `/items/foo`로 이동하면, 다음과 같은 기본 JSON 오류 대신: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +다음과 같은 텍스트 버전을 받게 됩니다: + +``` +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer +``` + +### `HTTPException` 오류 핸들러 오버라이드하기 { #override-the-httpexception-error-handler } + +같은 방식으로 `HTTPException` 핸들러도 오버라이드할 수 있습니다. + +예를 들어, 이런 오류들에 대해 JSON 대신 일반 텍스트 응답을 반환하고 싶을 수 있습니다: + +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *} + +/// note | 기술 세부사항 + +`from starlette.responses import PlainTextResponse`를 사용할 수도 있습니다. + +**FastAPI**는 개발자의 편의를 위해 `starlette.responses`를 `fastapi.responses`로도 동일하게 제공합니다. 하지만 사용 가능한 대부분의 응답은 Starlette에서 직접 옵니다. + +/// + +/// warning | 경고 + +`RequestValidationError`에는 검증 오류가 발생한 파일 이름과 줄 정보가 포함되어 있어, 원한다면 관련 정보와 함께 로그에 표시할 수 있다는 점을 유념하세요. + +하지만 이는 단순히 문자열로 변환해 그 정보를 그대로 반환하면 시스템에 대한 일부 정보를 누설할 수 있다는 뜻이기도 합니다. 그래서 여기의 코드는 각 오류를 독립적으로 추출해 보여줍니다. + +/// + +### `RequestValidationError`의 body 사용하기 { #use-the-requestvalidationerror-body } + +`RequestValidationError`에는 유효하지 않은 데이터와 함께 받은 `body`가 포함됩니다. + +앱을 개발하는 동안 body를 로그로 남기고 디버그하거나, 사용자에게 반환하는 등으로 사용할 수 있습니다. + +{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *} + +이제 다음처럼 유효하지 않은 item을 보내보세요: + +```JSON +{ + "title": "towel", + "size": "XL" +} +``` + +받은 body를 포함해 데이터가 유효하지 않다고 알려주는 응답을 받게 됩니다: + +```JSON hl_lines="12-15" +{ + "detail": [ + { + "loc": [ + "body", + "size" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ], + "body": { + "title": "towel", + "size": "XL" + } +} +``` + +#### FastAPI의 `HTTPException` vs Starlette의 `HTTPException` { #fastapis-httpexception-vs-starlettes-httpexception } + +**FastAPI**에는 자체 `HTTPException`이 있습니다. + +그리고 **FastAPI**의 `HTTPException` 오류 클래스는 Starlette의 `HTTPException` 오류 클래스를 상속합니다. + +유일한 차이는 **FastAPI**의 `HTTPException`은 `detail` 필드에 JSON으로 변환 가능한 어떤 데이터든 받을 수 있는 반면, Starlette의 `HTTPException`은 문자열만 받을 수 있다는 점입니다. + +따라서 코드에서는 평소처럼 **FastAPI**의 `HTTPException`을 계속 `raise`하면 됩니다. + +하지만 예외 핸들러를 등록할 때는 Starlette의 `HTTPException`에 대해 등록해야 합니다. + +이렇게 하면 Starlette 내부 코드의 어떤 부분, 또는 Starlette 확장/플러그인이 Starlette `HTTPException`을 `raise`하더라도, 여러분의 핸들러가 이를 잡아서 처리할 수 있습니다. + +이 예시에서는 동일한 코드에서 두 `HTTPException`을 모두 사용할 수 있도록, Starlette의 예외를 `StarletteHTTPException`으로 이름을 바꿉니다: + +```Python +from starlette.exceptions import HTTPException as StarletteHTTPException +``` + +### **FastAPI**의 예외 핸들러 재사용하기 { #reuse-fastapis-exception-handlers } + +예외를 사용하면서 **FastAPI**의 동일한 기본 예외 핸들러도 함께 사용하고 싶다면, `fastapi.exception_handlers`에서 기본 예외 핸들러를 가져와 재사용할 수 있습니다: + +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} + +이 예시에서는 매우 표현력 있는 메시지로 오류를 출력만 하고 있지만, 요지는 이해하셨을 겁니다. 예외를 사용한 뒤 기본 예외 핸들러를 그대로 재사용할 수 있습니다. diff --git a/docs/ko/docs/tutorial/security/first-steps.md b/docs/ko/docs/tutorial/security/first-steps.md new file mode 100644 index 000000000..4c9181b31 --- /dev/null +++ b/docs/ko/docs/tutorial/security/first-steps.md @@ -0,0 +1,203 @@ +# 보안 - 첫 단계 { #security-first-steps } + +어떤 도메인에 **backend** API가 있다고 가정해 보겠습니다. + +그리고 다른 도메인에 **frontend**가 있거나, 같은 도메인의 다른 경로에 있거나(또는 모바일 애플리케이션에 있을 수도 있습니다). + +그리고 frontend가 **username**과 **password**를 사용해 backend에 인증할 수 있는 방법이 필요하다고 해봅시다. + +**FastAPI**와 함께 **OAuth2**를 사용해서 이를 구현할 수 있습니다. + +하지만 필요한 작은 정보 조각들을 찾기 위해 길고 긴 전체 스펙을 읽느라 시간을 쓰지 않도록 하겠습니다. + +보안을 처리하기 위해 **FastAPI**가 제공하는 도구들을 사용해 봅시다. + +## 어떻게 보이는지 { #how-it-looks } + +먼저 코드를 그냥 사용해서 어떻게 동작하는지 보고, 그다음에 무슨 일이 일어나는지 이해하러 다시 돌아오겠습니다. + +## `main.py` 만들기 { #create-main-py } + +예제를 파일 `main.py`에 복사하세요: + +{* ../../docs_src/security/tutorial001_an_py39.py *} + +## 실행하기 { #run-it } + +/// info | 정보 + +<a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a> 패키지는 `pip install "fastapi[standard]"` 명령을 실행하면 **FastAPI**와 함께 자동으로 설치됩니다. + +하지만 `pip install fastapi` 명령을 사용하면 `python-multipart` 패키지가 기본으로 포함되지 않습니다. + +수동으로 설치하려면, [가상 환경](../../virtual-environments.md){.internal-link target=_blank}을 만들고 활성화한 다음, 아래로 설치하세요: + +```console +$ pip install python-multipart +``` + +이는 **OAuth2**가 `username`과 `password`를 보내기 위해 "form data"를 사용하기 때문입니다. + +/// + +다음으로 예제를 실행하세요: + +<div class="termy"> + +```console +$ fastapi dev main.py + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +## 확인하기 { #check-it } + +대화형 문서로 이동하세요: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. + +다음과 비슷한 화면이 보일 것입니다: + +<img src="/img/tutorial/security/image01.png"> + +/// check | Authorize 버튼! + +반짝이는 새 "Authorize" 버튼이 이미 있습니다. + +그리고 *경로 처리*에는 오른쪽 상단에 클릭할 수 있는 작은 자물쇠가 있습니다. + +/// + +그리고 이를 클릭하면 `username`과 `password`(그리고 다른 선택적 필드들)를 입력할 수 있는 작은 인증 폼이 나타납니다: + +<img src="/img/tutorial/security/image02.png"> + +/// note | 참고 + +폼에 무엇을 입력하든 아직은 동작하지 않습니다. 하지만 곧 여기까지 구현할 것입니다. + +/// + +물론 이것은 최종 사용자를 위한 frontend는 아니지만, 모든 API를 대화형으로 문서화하는 훌륭한 자동 도구입니다. + +frontend 팀(그게 본인일 수도 있습니다)이 사용할 수 있습니다. + +서드파티 애플리케이션과 시스템에서도 사용할 수 있습니다. + +그리고 동일한 애플리케이션을 디버그하고, 확인하고, 테스트하기 위해 본인이 사용할 수도 있습니다. + +## `password` 플로우 { #the-password-flow } + +이제 조금 돌아가서 이것들이 무엇인지 이해해 봅시다. + +`password` "flow"는 보안과 인증을 처리하기 위해 OAuth2에서 정의한 여러 방식("flows") 중 하나입니다. + +OAuth2는 backend 또는 API가 사용자를 인증하는 서버와 독립적일 수 있도록 설계되었습니다. + +하지만 이 경우에는 같은 **FastAPI** 애플리케이션이 API와 인증을 모두 처리합니다. + +따라서, 단순화된 관점에서 다시 정리해보면: + +* 사용자가 frontend에서 `username`과 `password`를 입력하고 `Enter`를 누릅니다. +* frontend(사용자의 브라우저에서 실행됨)는 해당 `username`과 `password`를 우리 API의 특정 URL로 보냅니다(`tokenUrl="token"`로 선언됨). +* API는 `username`과 `password`를 확인하고 "token"으로 응답합니다(아직 아무것도 구현하지 않았습니다). + * "token"은 나중에 이 사용자를 검증하는 데 사용할 수 있는 어떤 내용이 담긴 문자열일 뿐입니다. + * 보통 token은 일정 시간이 지나면 만료되도록 설정합니다. + * 그래서 사용자는 나중에 어느 시점엔 다시 로그인해야 합니다. + * 그리고 token이 도난당하더라도 위험이 더 낮습니다. 대부분의 경우 영구적으로 항상 동작하는 키와는 다릅니다. +* frontend는 그 token을 임시로 어딘가에 저장합니다. +* 사용자가 frontend에서 클릭해서 frontend 웹 앱의 다른 섹션으로 이동합니다. +* frontend는 API에서 더 많은 데이터를 가져와야 합니다. + * 하지만 그 특정 endpoint에는 인증이 필요합니다. + * 그래서 우리 API에 인증하기 위해 `Authorization` 헤더를, 값은 `Bearer `에 token을 더한 형태로 보냅니다. + * token에 `foobar`가 들어 있다면 `Authorization` 헤더의 내용은 `Bearer foobar`가 됩니다. + +## **FastAPI**의 `OAuth2PasswordBearer` { #fastapis-oauth2passwordbearer } + +**FastAPI**는 이런 보안 기능을 구현하기 위해, 서로 다른 추상화 수준에서 여러 도구를 제공합니다. + +이 예제에서는 **OAuth2**의 **Password** 플로우와 **Bearer** token을 사용합니다. 이를 위해 `OAuth2PasswordBearer` 클래스를 사용합니다. + +/// info | 정보 + +"bearer" token만이 유일한 선택지는 아닙니다. + +하지만 이 사용 사례에는 가장 적합한 선택입니다. + +또한 OAuth2 전문가로서 왜 다른 옵션이 더 적합한지 정확히 아는 경우가 아니라면, 대부분의 사용 사례에도 가장 적합할 가능성이 큽니다. + +그런 경우를 위해서도 **FastAPI**는 이를 구성할 수 있는 도구를 제공합니다. + +/// + +`OAuth2PasswordBearer` 클래스의 인스턴스를 만들 때 `tokenUrl` 파라미터를 전달합니다. 이 파라미터에는 클라이언트(사용자의 브라우저에서 실행되는 frontend)가 token을 받기 위해 `username`과 `password`를 보낼 URL이 들어 있습니다. + +{* ../../docs_src/security/tutorial001_an_py39.py hl[8] *} + +/// tip | 팁 + +여기서 `tokenUrl="token"`은 아직 만들지 않은 상대 URL `token`을 가리킵니다. 상대 URL이므로 `./token`과 동일합니다. + +상대 URL을 사용하므로, 예를 들어 API가 `https://example.com/`에 있다면 `https://example.com/token`을 가리킵니다. 하지만 API가 `https://example.com/api/v1/`에 있다면 `https://example.com/api/v1/token`을 가리킵니다. + +상대 URL을 사용하는 것은 [프록시 뒤에서](../../advanced/behind-a-proxy.md){.internal-link target=_blank} 같은 고급 사용 사례에서도 애플리케이션이 계속 동작하도록 보장하는 데 중요합니다. + +/// + +이 파라미터는 그 endpoint / *경로 처리*를 만들지는 않지만, URL `/token`이 클라이언트가 token을 얻기 위해 사용해야 할 URL이라고 선언합니다. 이 정보는 OpenAPI에 사용되고, 이어서 대화형 API 문서 시스템에서도 사용됩니다. + +곧 실제 경로 처리를 만들 것입니다. + +/// info | 정보 + +엄격한 "Pythonista"라면 `token_url` 대신 `tokenUrl` 같은 파라미터 이름 스타일이 마음에 들지 않을 수도 있습니다. + +이는 OpenAPI 스펙에서 사용하는 이름과 동일하게 맞춘 것이기 때문입니다. 그래서 이런 보안 스킴에 대해 더 조사해야 할 때, 그대로 복사해서 붙여 넣어 더 많은 정보를 찾을 수 있습니다. + +/// + +`oauth2_scheme` 변수는 `OAuth2PasswordBearer`의 인스턴스이지만, "callable"이기도 합니다. + +다음처럼 호출될 수 있습니다: + +```Python +oauth2_scheme(some, parameters) +``` + +따라서 `Depends`와 함께 사용할 수 있습니다. + +### 사용하기 { #use-it } + +이제 `Depends`로 `oauth2_scheme`를 의존성에 전달할 수 있습니다. + +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} + +이 의존성은 `str`을 제공하고, 그 값은 *경로 처리 함수*의 파라미터 `token`에 할당됩니다. + +**FastAPI**는 이 의존성을 사용해 OpenAPI 스키마(및 자동 API 문서)에 "security scheme"를 정의할 수 있다는 것을 알게 됩니다. + +/// info | 기술 세부사항 + +**FastAPI**는 (의존성에 선언된) `OAuth2PasswordBearer` 클래스를 사용해 OpenAPI에서 보안 스킴을 정의할 수 있다는 것을 알고 있습니다. 이는 `OAuth2PasswordBearer`가 `fastapi.security.oauth2.OAuth2`를 상속하고, 이것이 다시 `fastapi.security.base.SecurityBase`를 상속하기 때문입니다. + +OpenAPI(및 자동 API 문서)와 통합되는 모든 보안 유틸리티는 `SecurityBase`를 상속하며, 그래서 **FastAPI**가 이를 OpenAPI에 어떻게 통합할지 알 수 있습니다. + +/// + +## 무엇을 하는지 { #what-it-does } + +요청에서 `Authorization` 헤더를 찾아, 값이 `Bearer `에 어떤 token이 붙은 형태인지 확인한 뒤, 그 token을 `str`로 반환합니다. + +`Authorization` 헤더가 없거나, 값에 `Bearer ` token이 없다면, 곧바로 401 상태 코드 오류(`UNAUTHORIZED`)로 응답합니다. + +오류를 반환하기 위해 token이 존재하는지 직접 확인할 필요조차 없습니다. 함수가 실행되었다면 그 token에는 `str`이 들어 있다고 확신할 수 있습니다. + +대화형 문서에서 이미 시도해 볼 수 있습니다: + +<img src="/img/tutorial/security/image03.png"> + +아직 token의 유효성을 검증하진 않지만, 이것만으로도 시작은 된 셈입니다. + +## 요약 { #recap } + +즉, 추가로 3~4줄만으로도 이미 원시적인 형태의 보안을 갖추게 됩니다. diff --git a/docs/ko/docs/tutorial/security/index.md b/docs/ko/docs/tutorial/security/index.md new file mode 100644 index 000000000..2320b0657 --- /dev/null +++ b/docs/ko/docs/tutorial/security/index.md @@ -0,0 +1,106 @@ +# 보안 { #security } + +보안, 인증(authentication), 인가(authorization)를 처리하는 방법은 매우 다양합니다. + +그리고 보통 복잡하고 "어려운" 주제이기도 합니다. + +많은 프레임워크와 시스템에서 보안과 인증만 처리하는 데도 큰 노력과 코드가 필요합니다(많은 경우 작성된 전체 코드의 50% 이상이 될 수도 있습니다). + +**FastAPI**는 모든 보안 명세를 전부 공부하고 배울 필요 없이, 표준적인 방식으로 쉽고 빠르게 **보안(Security)** 을 다룰 수 있도록 여러 도구를 제공합니다. + +하지만 먼저, 몇 가지 작은 개념을 확인해 보겠습니다. + +## 급하신가요? { #in-a-hurry } + +이 용어들에 관심이 없고 사용자명과 비밀번호 기반 인증을 사용한 보안을 *지금 당장* 추가하기만 하면 된다면, 다음 장들로 넘어가세요. + +## OAuth2 { #oauth2 } + +OAuth2는 인증과 인가를 처리하는 여러 방법을 정의하는 명세입니다. + +상당히 방대한 명세이며 여러 복잡한 사용 사례를 다룹니다. + +"제3자"를 사용해 인증하는 방법도 포함합니다. + +바로 `"Facebook, Google, X (Twitter), GitHub로 로그인"` 같은 시스템들이 내부적으로 사용하는 방식입니다. + +### OAuth 1 { #oauth-1 } + +OAuth 1도 있었는데, 이는 OAuth2와 매우 다르고 통신을 암호화하는 방법까지 직접 명세에 포함했기 때문에 더 복잡했습니다. + +요즘에는 그다지 인기 있거나 사용되지는 않습니다. + +OAuth2는 통신을 어떻게 암호화할지는 명세하지 않고, 애플리케이션이 HTTPS로 제공될 것을 기대합니다. + +/// tip | 팁 + +**배포**에 대한 섹션에서 Traefik과 Let's Encrypt를 사용해 무료로 HTTPS를 설정하는 방법을 볼 수 있습니다. + +/// + +## OpenID Connect { #openid-connect } + +OpenID Connect는 **OAuth2**를 기반으로 한 또 다른 명세입니다. + +OAuth2에서 비교적 모호한 부분을 일부 구체화하여 상호 운용성을 높이려는 확장입니다. + +예를 들어, Google 로그인은 OpenID Connect를 사용합니다(내부적으로는 OAuth2를 사용). + +하지만 Facebook 로그인은 OpenID Connect를 지원하지 않습니다. 자체적인 변형의 OAuth2를 사용합니다. + +### OpenID("OpenID Connect"가 아님) { #openid-not-openid-connect } + +"OpenID"라는 명세도 있었습니다. 이는 **OpenID Connect**와 같은 문제를 해결하려고 했지만, OAuth2를 기반으로 하지 않았습니다. + +따라서 완전히 별도의 추가 시스템이었습니다. + +요즘에는 그다지 인기 있거나 사용되지는 않습니다. + +## OpenAPI { #openapi } + +OpenAPI(이전에는 Swagger로 알려짐)는 API를 구축하기 위한 공개 명세입니다(현재 Linux Foundation의 일부). + +**FastAPI**는 **OpenAPI**를 기반으로 합니다. + +이 덕분에 여러 자동 대화형 문서 인터페이스, 코드 생성 등과 같은 기능을 사용할 수 있습니다. + +OpenAPI에는 여러 보안 "scheme"을 정의하는 방법이 있습니다. + +이를 사용하면 이러한 대화형 문서 시스템을 포함해, 표준 기반 도구들을 모두 활용할 수 있습니다. + +OpenAPI는 다음 보안 scheme들을 정의합니다: + +* `apiKey`: 다음에서 전달될 수 있는 애플리케이션 전용 키: + * 쿼리 파라미터 + * 헤더 + * 쿠키 +* `http`: 표준 HTTP 인증 시스템, 예: + * `bearer`: `Authorization` 헤더에 `Bearer ` + 토큰 값을 넣는 방식. OAuth2에서 유래했습니다. + * HTTP Basic 인증 + * HTTP Digest 등 +* `oauth2`: 보안을 처리하는 모든 OAuth2 방식(이를 "flow"라고 부릅니다). + * 이 flow들 중 여러 개는 OAuth 2.0 인증 제공자(예: Google, Facebook, X (Twitter), GitHub 등)를 구축하는 데 적합합니다: + * `implicit` + * `clientCredentials` + * `authorizationCode` + * 하지만 같은 애플리케이션에서 직접 인증을 처리하는 데 완벽하게 사용할 수 있는 특정 "flow"도 하나 있습니다: + * `password`: 다음 장들에서 이에 대한 예시를 다룹니다. +* `openIdConnect`: OAuth2 인증 데이터를 자동으로 탐색(discover)하는 방법을 정의합니다. + * 이 자동 탐색은 OpenID Connect 명세에서 정의됩니다. + + +/// tip | 팁 + +Google, Facebook, X (Twitter), GitHub 등 다른 인증/인가 제공자를 통합하는 것도 가능하며 비교적 쉽습니다. + +가장 복잡한 문제는 그런 인증/인가 제공자 자체를 구축하는 것이지만, **FastAPI**는 어려운 작업을 대신 처리해 주면서 이를 쉽게 할 수 있는 도구를 제공합니다. + +/// + +## **FastAPI** 유틸리티 { #fastapi-utilities } + +FastAPI는 `fastapi.security` 모듈에서 각 보안 scheme에 대한 여러 도구를 제공하며, 이러한 보안 메커니즘을 더 쉽게 사용할 수 있게 해줍니다. + +다음 장들에서는 **FastAPI**가 제공하는 도구를 사용해 API에 보안을 추가하는 방법을 보게 될 것입니다. + +또한 대화형 문서 시스템에 어떻게 자동으로 통합되는지도 확인하게 됩니다. From 23bcfa094d9baefbf0150cbcddd1b77ec33a23fc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:54:26 +0000 Subject: [PATCH 084/243] =?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 233fdb743..912f215f0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Translations +* 🌐 Update translations for ko (add-missing). PR [#14699](https://github.com/fastapi/fastapi/pull/14699) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for ko (update-outdated). PR [#14589](https://github.com/fastapi/fastapi/pull/14589) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for uk (update-outdated). PR [#14587](https://github.com/fastapi/fastapi/pull/14587) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for es (update-outdated). PR [#14686](https://github.com/fastapi/fastapi/pull/14686) by [@tiangolo](https://github.com/tiangolo). From fb15bba819a71df10fee28891c6c7c29fdf48d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Fri, 16 Jan 2026 03:57:08 -0800 Subject: [PATCH 085/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20LLM=20prompt=20?= =?UTF-8?q?instructions=20file=20for=20French=20(#14618)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/fr/llm-prompt.md | 154 ++++++++++++++++++++++-------------------- 1 file changed, 79 insertions(+), 75 deletions(-) diff --git a/docs/fr/llm-prompt.md b/docs/fr/llm-prompt.md index 5ff18c4e8..228500fe2 100644 --- a/docs/fr/llm-prompt.md +++ b/docs/fr/llm-prompt.md @@ -6,123 +6,127 @@ Language code: fr. ### Grammar to use when talking to the reader -Use the formal grammar (use «vous» instead of «tu»). +Use the formal grammar (use `vous` instead of `tu`). + +Additionally, in instructional sentences, prefer the present tense for obligations: + +- Prefer `vous devez …` over `vous devrez …`, unless the English source explicitly refers to a future requirement. + +- When translating “make sure (that) … is …”, prefer the indicative after `vous assurer que` (e.g. `Vous devez vous assurer qu'il est …`) instead of the subjunctive (e.g. `qu'il soit …`). ### Quotes -1) Convert neutral double quotes («"») and English double typographic quotes («“» and «”») to French guillemets (««» and «»»). +- Convert neutral double quotes (`"`) to French guillemets (`«` and `»`). -2) In the French docs, guillemets are written without extra spaces: use «texte», not « texte ». - -3) Do not convert quotes inside code blocks, inline code, paths, URLs, or anything wrapped in backticks. +- Do not convert quotes inside code blocks, inline code, paths, URLs, or anything wrapped in backticks. Examples: - Source (English): +Source (English): - ««« - "Hello world" - “Hello Universe” - "He said: 'Hello'" - "The module is `__main__`" - »»» +``` +"Hello world" +“Hello Universe” +"He said: 'Hello'" +"The module is `__main__`" +``` - Result (French): +Result (French): - ««« - «Hello world» - «Hello Universe» - «He said: 'Hello'» - «The module is `__main__`» - »»» +``` +"Hello world" +“Hello Universe” +"He said: 'Hello'" +"The module is `__main__`" +``` ### Ellipsis -1) Make sure there is a space between an ellipsis and a word following or preceding the ellipsis. +- Make sure there is a space between an ellipsis and a word following or preceding the ellipsis. Examples: - Source (English): +Source (English): - ««« - ...as we intended. - ...this would work: - ...etc. - others... - More to come... - »»» +``` +...as we intended. +...this would work: +...etc. +others... +More to come... +``` - Result (French): +Result (French): - ««« - ... comme prévu. - ... cela fonctionnerait : - ... etc. - D'autres ... - La suite ... - »»» +``` +... comme prévu. +... cela fonctionnerait : +... etc. +D'autres ... +La suite ... +``` -2) This does not apply in URLs, code blocks, and code snippets. Do not remove or add spaces there. +- This does not apply in URLs, code blocks, and code snippets. Do not remove or add spaces there. ### Headings -1) Prefer translating headings using the infinitive form (as is common in the existing French docs): «Créer…», «Utiliser…», «Ajouter…». +- Prefer translating headings using the infinitive form (as is common in the existing French docs): `Créer…`, `Utiliser…`, `Ajouter…`. -2) For headings that are instructions written in imperative in English (e.g. “Go check …”), keep them in imperative in French, using the formal grammar (e.g. «Allez voir …»). - -3) Keep heading punctuation as in the source. In particular, keep occurrences of literal « - » (space-hyphen-space) as « - » (the existing French docs use a hyphen here). +- For headings that are instructions written in imperative in English (e.g. `Go check …`), keep them in imperative in French, using the formal grammar (e.g. `Allez voir …`). ### French instructions about technical terms -Do not try to translate everything. In particular, keep common programming terms when that is the established usage in the French docs (e.g. «framework», «endpoint», «plug-in», «payload»). Use French where the existing docs already consistently use French (e.g. «requête», «réponse»). +Do not try to translate everything. In particular, keep common programming terms (e.g. `framework`, `endpoint`, `plug-in`, `payload`). Keep class names, function names, modules, file names, and CLI commands unchanged. ### List of English terms and their preferred French translations -Below is a list of English terms and their preferred French translations, separated by a colon («:»). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them. +Below is a list of English terms and their preferred French translations, separated by a colon (:). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them. -* «/// note | Technical Details»: «/// note | Détails techniques» -* «/// note»: «/// note | Remarque» -* «/// tip»: «/// tip | Astuce» -* «/// warning»: «/// warning | Attention» -* «/// check»: «/// check | vérifier» -* «/// info»: «/// info» +- /// note | Technical Details»: /// note | Détails techniques +- /// note: /// note | Remarque +- /// tip: /// tip | Astuce +- /// warning: /// warning | Alertes +- /// check: /// check | Vérifications +- /// info: /// info -* «the docs»: «les documents» -* «the documentation»: «la documentation» +- the docs: les documents +- the documentation: la documentation -* «framework»: «framework» (do not translate to «cadre») -* «performance»: «performance» +- Exclude from OpenAPI: Exclusion d'OpenAPI -* «type hints»: «annotations de type» -* «type annotations»: «annotations de type» +- framework: framework (do not translate to cadre) +- performance: performance -* «autocomplete»: «autocomplétion» -* «autocompletion»: «autocomplétion» +- type hints: annotations de type +- type annotations: annotations de type -* «the request» (what the client sends to the server): «la requête» -* «the response» (what the server sends back to the client): «la réponse» +- autocomplete: autocomplétion +- autocompletion: autocomplétion -* «the request body»: «le corps de la requête» -* «the response body»: «le corps de la réponse» +- the request (what the client sends to the server): la requête +- the response (what the server sends back to the client): la réponse -* «path operation»: «opération de chemin» -* «path operations» (plural): «opérations de chemin» -* «path operation function»: «fonction de chemin» -* «path operation decorator»: «décorateur d'opération de chemin» +- the request body: le corps de la requête +- the response body: le corps de la réponse -* «path parameter»: «paramètre de chemin» -* «query parameter»: «paramètre de requête» +- path operation: chemin d'accès +- path operations (plural): chemins d'accès +- path operation function: fonction de chemin d'accès +- path operation decorator: décorateur de chemin d'accès -* «the `Request`»: «`Request`» (keep as code identifier) -* «the `Response`»: «`Response`» (keep as code identifier) +- path parameter: paramètre de chemin +- query parameter: paramètre de requête -* «deployment»: «déploiement» -* «to upgrade»: «mettre à niveau» +- the `Request`: `Request` (keep as code identifier) +- the `Response`: `Response` (keep as code identifier) -* «deprecated»: «déprécié» -* «to deprecate»: «déprécier» +- deployment: déploiement +- to upgrade: mettre à niveau -* «cheat sheet»: «aide-mémoire» -* «plug-in»: «plug-in» +- deprecated: déprécié +- to deprecate: déprécier + +- cheat sheet: aide-mémoire +- plug-in: plug-in From 8fa635c718fe71fa2e761b7fb33978d756e77a45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:57:32 +0000 Subject: [PATCH 086/243] =?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 912f215f0..404a4e41d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Translations +* 🌐 Update LLM prompt instructions file for French. PR [#14618](https://github.com/fastapi/fastapi/pull/14618) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for ko (add-missing). PR [#14699](https://github.com/fastapi/fastapi/pull/14699) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for ko (update-outdated). PR [#14589](https://github.com/fastapi/fastapi/pull/14589) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for uk (update-outdated). PR [#14587](https://github.com/fastapi/fastapi/pull/14587) by [@tiangolo](https://github.com/tiangolo). From 9fec72687f48db48d72bff6ba2aa023fe792206b Mon Sep 17 00:00:00 2001 From: Rafael de Oliveira Marques <rafaelomarques@gmail.com> Date: Fri, 16 Jan 2026 09:27:02 -0300 Subject: [PATCH 087/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20portuguese=20ll?= =?UTF-8?q?m-prompt.md=20(#14702)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem <svlandeg@users.noreply.github.com> --- docs/pt/llm-prompt.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/pt/llm-prompt.md b/docs/pt/llm-prompt.md index 3f5208e91..dd72e8d16 100644 --- a/docs/pt/llm-prompt.md +++ b/docs/pt/llm-prompt.md @@ -10,6 +10,26 @@ Keep existing translations as they are if the term is already translated. When translating documentation into Portuguese, use neutral and widely understandable language. Although Portuguese originated in Portugal and has its largest number of speakers in Brazil, it is also an official language in several countries and regions, such as Equatorial Guinea, Mozambique, Angola, Cape Verde, and São Tomé and Príncipe. Avoid words or expressions that are specific to a single country or region. +Only keep parentheses if they exist in the source text. Do not add parentheses to terms that do not have them. + +### Avoiding Repetition in Translation + +When translating sentences, avoid unnecessary repetition of words or phrases that are implied in context. +- Merge repeated words naturally while keeping the meaning. +- Do **not** introduce extra words to replace repeated phrases unnecessarily. +- Keep translations fluent and concise, but maintain the original meaning. + +**Example:** + +Source: +Let's see how that works and how to change it if you need to do that. + +Avoid translating literally as: +Vamos ver como isso funciona e como alterar isso se você precisar fazer isso. + +Better translation: +Vamos ver como isso funciona e como alterar se você precisar. + --- For the next terms, use the following translations: @@ -22,10 +42,11 @@ For the next terms, use the following translations: * /// note: /// note | Nota * /// tip: /// tip | Dica * /// warning: /// warning | Atenção -* (you should): (você deveria) +* you should: você deveria * async context manager: gerenciador de contexto assíncrono * autocomplete: autocompletar * autocompletion: preenchimento automático +* auto-completion: preenchimento automático * bug: bug * context manager: gerenciador de contexto * cross domain: cross domain (do not translate to "domínio cruzado") From 0c7f2b66d769d05df42ea73f85205a3566ba127b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:27:29 +0000 Subject: [PATCH 088/243] =?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 404a4e41d..d3f80fac9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Translations +* 🌐 Update portuguese llm-prompt.md. PR [#14702](https://github.com/fastapi/fastapi/pull/14702) by [@ceb10n](https://github.com/ceb10n). * 🌐 Update LLM prompt instructions file for French. PR [#14618](https://github.com/fastapi/fastapi/pull/14618) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for ko (add-missing). PR [#14699](https://github.com/fastapi/fastapi/pull/14699) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for ko (update-outdated). PR [#14589](https://github.com/fastapi/fastapi/pull/14589) by [@tiangolo](https://github.com/tiangolo). From 536a5bafe74a77f207b57cf6fb0cf3059121fffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 19 Jan 2026 12:40:17 -0800 Subject: [PATCH 089/243] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20Lam?= =?UTF-8?q?bdaTest=20changes=20to=20TestMu=20AI=20(#14734)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors.yml | 6 +++--- docs/en/docs/img/sponsors/testmu.png | Bin 0 -> 5725 bytes 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 docs/en/docs/img/sponsors/testmu.png diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 50b114530..c0bdb15f6 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -65,9 +65,9 @@ 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 + - url: https://www.testmu.ai/?utm_source=fastapi&utm_medium=partner&utm_campaign=sponsor&utm_term=opensource&utm_content=webpage + title: TestMu AI. The Native AI-Agentic Cloud Platform to Supercharge Quality Engineering. + img: https://fastapi.tiangolo.com/img/sponsors/testmu.png - url: https://requestly.com/fastapi title: All-in-one platform to Test, Mock and Intercept APIs. Built for speed, privacy and offline support. img: https://fastapi.tiangolo.com/img/sponsors/requestly.png diff --git a/docs/en/docs/img/sponsors/testmu.png b/docs/en/docs/img/sponsors/testmu.png new file mode 100644 index 0000000000000000000000000000000000000000..5603b04faed5e917bd0ea78f1684ba3c7a747718 GIT binary patch literal 5725 zcmd6L_cvT`w6`7vBf5#+iOyGzE?S}l(Pc#RE_x4%8iPa-MvdO0M2kK~8-hsG=msN1 z57FHx>-`7b-|n5Y&YZQ*oO#;bpR#wnfu1HgDU=ik2Zvl+OWhFM_rYaHOa#7LK6VU% z8;O^exepEw1?u00JK$Mn2Ocu|J}~n&@^tVGc=o~`Cm<j|*xAF?=lL@)dtuKPj)>j+ zP#hfEEp2rb<G}2loDc(J?Abt<V~y2X{jfE$4uJqJyN<enu^X2KqU2?QDS@jf+^-?E zE^+Fo)VQ3UWM86$Qv;)Id()&vbp{V%KbMBs5I1e2dLk(?zA(LrVB7F+=M`!xmsP}Z z*4joT`6%3Bl=UL}Z`g@!>{ag3)7-z&_{!ZjqU!N$&bPpwh=N-56}#<+myBe0Py}CX z;uv^rh}3#TaU~cyVn{w9Da9Ujlf}e6!*dW-C;MM~^{wpiodNF@EjJ~|Bc4>$H5V0$ zOw#tzeRcwe&tJxlJUl$4wjnefPa%clo4#H8x52qbj+jrE84Iwdjw^|!q6u>Jq@9*f z(Tx{BW@lyjSYJ~*HU3oTvqw7PCB?-lo0z1Jh)10N$YZ+3R9k(aLL!l2xm|$=9HTk% z)7Z1gN>kL>*jT~d7Gxla8MQu~k+b{PIp0{|;9}|K>Hc!Zi*?fLljbh+A?X2cv(FCX zmseN(7b6Jix`4e!l`|G$Jbe6vQTaUs5j<65|DB)F=jXw(S8nqycV2wSzL}q&Ph^(& zml4uiO?*C@E!)eoxVT7h(e`3JK^GQreU#@uRa0JGcXv#AaAd@9=n7h)R&4k-E{=FB z1#M?C92^|{Zg<xM`CocQ1~mkdtkQRfkx^R`3hxeh{CM1`OEZaOI75Wj;d5rflABcO zlS-50;bfD_;!tB#Q+Xkc(vC>RF(Nb?ZPwz&Zg1J<F9iOIQtXG{jeY-~=5iZ$1NCBh zLoX6zQO)9YRSJK^gQ~Y5CRh+n!qocyGW<Q)3|UxM(C2Txi6uFe<&=|yf|pC&?n=3B zW<W9@#mvuJWh;a<!7y}m6E6SJ30X*TJ*PxH@b#6XqN1wGMTkDbTU}lCI^LRmmZUT2 zb8lIs^AxYKu`w(+z|XHQkx_!r(#VKfKtRClyt=Z|M<;Ylj-Q`D?#&yuprD|x?a^F? zyDKXzYLt`vbzUo~EnmKvE0h`w)H#iF@GKYDO@FfX^7j696l!3QqTD?>IeCg<y>a@n zmm?pLO}X$nIX3oIx(IC16ZaksY5Um*!rzfA*S@{IT{K|V3Ss~K`}bP=VY-IqW{8pT zRFDT@=+4jo5Lit(l)Dxzab+m&9{#yv5{4}mtxOz=w!>q$Qw|r@DmLV!A;o!#v|%tX z@~<r_E`B!G<lYjnmy7b_(aVnO?d@exblJ>Q43q0mWK^5Au&^k*b6i_1`t^et{>|xt zQjGll1guFnDl#L3zCW2Yd4J#Q^x^_FG6K)TxH>x{t1X)i%+2GIk|<)Y`UVC##l@+v zudgpJ3rs4bH42enZw8MaNA~w?bW#j>pRS8Upd*@ia;vHY8yXuQH|Z;$^?KX~89q2T z*!{`6jVXZ@$E3;(*VNQBc8G|8fQ^&$Rejp~_xU9y{SF=7-O5F!rAAGwtG1yxSAQu6 zoZtNZ?N&iJJ~<h!5OQMkqeM5fD;FapXxT)@tC!922Nf(TiVML3kq7DXHS53IHSLF_ zJt)))7CX=-b{woA2_^SfXpOF~7pI}8FDxq~CVGXeP>NTNa{f`WQmS;t5AuSNk&y%j zgDH~qI7xmiEW~g2T+(>(fG{B;!R!2wv#+0DBu1uU*0Xx)1mtIyPB8oH<(azY-?opX zYEm&WYin!ywY8MVtV&HGu%4bCQO8lHwspbRuU`|l3c9<y`x9RF+!i6;a+ZXa63Yk* zl7WPJU)|o$L`X<z*5t;hk<2nOC*<j3`CLeioZ3st+t2TTo*qTIpm|hpuQ~{?3L`x| zJs6ISi;Dmt=YyP|00#%h{=oq2?bqGjOdF#)Zvf2LWo6A%z7X6GvN=6J@0p&a|9284 zk20ek7itvb=i|ub=%omb-v#^Jl3*_yQxsr-4OK{j!~6dH`r*Ctl@YOoi*uV!FyFSW zJ@Xt2Paj^7nsJ9)k~QUWf{*|XILISmmkB);%|al&57)G-tlQF+usb{O#6+_8o9hd` zv)xFx#kVn~FO3EI5T2d!@VnBsL+|-b`dq6=d*i9=o}q}=Ha18JvQkoB>LR4rxw#*B zjk-zQ{{BEVNeuw>@bIwK1{sBf8H+%n&@xFK5Y*<y;NjiI;Gm#qzrGj0H@gdk=Ei?9 z)T~Ps{@WfiHDwa6c)7oViZU7QiN4LeD^5nJ7lI$)>kG7Ie`FB*mQliiO+-XLWz`&H z8%P03Y3Wy#c&x0f02FKj0tq)_1Ox;&3$1=F{yR_*?$cJ+=}!+HJtDiwq>LIG(s`ee zfw{iAz`;>cQsUy`V&mb7`HRJ3WBs<LRz8m@qUd3v$Y}^U`2#aE2HGV+)a5iLs#<v? zXM3F&6i_%3W*`~s{*OQ4@J87_DW8o;>JJ_~ZldPZv*}Bq{dlI^3-BZ*mHrO^`_8Fp zX=!C;W%>P#wzjsS-mB`NXX_a#XeD7O$?3)jLdJKKvcA5)N(#R(sjW>O92X@ORnHtT zJ0Cwkas?IZeZ2JqY!DwOG&GcwnHes<7>Ex7x_o4#nZy*|-hMyy>Uer{Yb(m4x?ey1 zka{Z~W?_-wKG#$La<RI)8YfpWlA4ODHu%`3B7%;VHu~3(Qp`fY(z^Q_Iw4gL4`JF1 zpigHrg5ClC{ub{3is6?Gj$^rf!y;W@2i8|sBD1rz6$#z6FE?ieBTo0(DVStl(%v(f z>8u;0e3&6z%p&FRnei3{)3VZCG9o6$P=-_<-PJv~m9VCX$&?Yb=yLD;IN+Wa%iXxP zlF;gnI}k_VhK}bGWsmXlBCfV!7lKOQ06$J|mmk{O+uLnTl!r~l$HlRg>g9ag+B^>K z)N^yIVv7NEs(4jgQbKCW$H$jHR0ZpLx-20nIZ>hH`sLd|GAl1TJ9|+<L1g{(^mOX- zvQ1_9Dh7iFkt#m~BXNBC^a(Fy%xV)rPo-*hcGmyKKb2RXZen<72s*Kc92-lXn4Em9 z=(*Ug4Q3+fLM+NI{@q>R#Kpx8J7*cPaw<KEbai#D14PcG6h6OUs3i98?OPRv91`}K z&yEnChY#5^hO1cucIT0?tl?V6__U;i;U}N-a9>VF7L6Bba)PD0+}pT3JK_@-)^K*_ zkN6Ydi~vt1m)Fp#??=Ro?cmwjOfKkbv|Z0)M@Zh#{IPP-PUCdh6fMr>QQl4Ot6LC0 zOKWRp9UUD$O9KN+hp}9RH8Bg+FZYG4EU4RTJ)w9e*{@%}K90ujpeM?W2%8Qt14chc zr#8PNevK-IOwP>2jgIPV4dmtKQWP7;$HWjdG&Ozf=wRO2*=dQmQH0*TdvJZRXQT^z zC^0unLr2FUFONuu16GE0T8S3ZP*FuSG)Pu>Jb6Mpng@X6O-^W7Zd~i7Qu<?M@GWOl z{cFn5^IxSO<O76naGJi7u-*^!nqjq2`)=L=V7M5vr!ctwqW$`O`y+jwVL1(P8b+r5 z>SPZ8ke-f?C<&84?zDe~&Epgjf_G^~Mn<0YiG<^(qF>*Fu;c^s28{d7NfPKEOZe%^ zE#oIoRw`>cq2DDt0iLZ|yl_7|jI6rX)zxWfYa>C12U!LO%l%iTNsI(QKR7*jK2G)& z&MZ#{f?nbV#v&#r?*5u7LAai#!`)yZq>;#goa52WdR<n=2gW83YFA*aA20Fjx=N=% zs51YoE@TVbV5uH$OIzD3jLe4-hu<@G_#ip7baX!MzBLwzwx|Z)rvHsd=ir>59CPQz zuKYh?0s=B!0}HUQQV@f}XPP}(4ULS>q~U-{Ej>G;eXyfQJMm||ad!sM=z>)b7nh#8 z=L0cZB?TN35~)<2`*u%*F#Yhbsb33M-T?uc4h|bfoHjTu!N&ssjuU7|rM`F_Tbd3b zgiSo|k8F<Kz;u`IFRI0`zt(mR4z`DDXkrK?_7TtaQ6zSCH3}reKWhb4`*g%J{6~$| z`n7?TRU$|qmtd#!Kff5HJ(vXU-4k>FDT!<tE;lMYZ?x=F_Sz7P1rBFpEKj7ia?<5d zrg$-|6A)MI&RjDFr1mK3n6R4S((|)OfwZB5>k4+Y%E^yyZHU&Gwtzj`o9iq5!|CyH z<n0fXQDaIsOwu0n%7%ti;eQHQ*(D`uK&1DNkE3;p0e{8l6n}ijPeV!w=1NXTAaz^n z4D+IQ;v7|rrFeF<F%rdJ9IzO4>z?VSqTZrOm&ayio3#<H_&G_EZ6S{7<T3n4C2Zp2 z-}yT^I62>DXS?}S@;YK9abu&Sk7ad8?q|X-50WDUwZu2y++1Rnq;9Fk_$=yz#g>=H zZ{6BH4t85;o!s5<q>3lFXHu0<^x0#nlYAMy%cu|rB_jeq(yFMa5R;L;PELjx3(x{S zUf$bF1`%65o`zu&M*x4ava+YkuOKmQ6E~aVg=WA9H+Gq6Xb>r0{FXqz_*&C-{c|^X zD;IbV5<}^o;%Gd5e==`6z(-Mn5}MN47@4Fk3v(irI{z|o=9epwh+6ZFANooU08MKo zF*#2!{hs}Nuyk|XJa!ajPeo1$Aa^t({eDIR4vlVUk!c%-pirnf=N~*<BZg*XaT1QB zY(hfhN;hX?L2Y`asCa5#0~k!iDMo;XR9#)Ydv5M-d3kxe0tW{ksD3Dfq!@^sJr;Vp zZmv9CP(3Lrl$W~!U9U<@c|9c%Bd5c#>+dxa=#32rkpDW$2L}gI+bylFa_&PsAdoek z=K}U^fJgj)&#V~GYnL0ZHg{bKaj%-f!e|Q$3Q(;FuZ(!!GtW+p0=BKPY)-l#{AbW( z_VhHcRM+29{v0t_7kFT6E1p?)sF}>d4a{iR6fjNP@&UU=TVTd|;KuP@u3@o8*hf6n znNsdqBP~j&%lInHX*zv*&FO<;!GD}8JI{62HmyB=HaM%7{vi2O$P4gSx{9MrLmEAz zPEl?wpq1<7;=&;&CT4<Jmr1g7bi8fLZf6tERB2j6XElI)b&L6vNX9Vw><CNvcipt> zoc4*HWIWT!=I(BPXV}F)nl~@b<sB;yuoVQHou2InL{l}eWbIU*$NJBJvjO}8=z{2B zx?sW9d`C#e9`t=$+G8vzHKp8UUIODntkGdFo;lff2!+S!Y}7WXiwX-<bar;`9~`Ve zz$dom>TrGdbOYhZXSr8x0%4Y$*mD+2MRRTP5CT&J=m1d8YKDdm7gZvYES0&r`QY%d z&ro23YC-oq?uTXDqEI+Rb!yYMg$4N4#a|;&S5RWvq2E12sNOv(S_f=u1FFx!gFbq~ zpzbkoAt9l`v9ST5S%dnrAegH=%}f2j1wRI*L3%nJIQGDh5KM8#Q&krifkuhKai?CO zPU9031k317Yd=3fVsi5J;r5fCu3kPqsOa1D2BxOBs!eW3t48MMvvF~AKl@&!ZES@d z%_(f9K0ZE<3SZCq&ow10OXoPXva+(8N5D+2W*UP@R?>L!qJFZ=)YbJ~5{trCO3fmZ zoG<Yab_|<pB$I?{4?4;>seBA7lBOmppd+1M=0ZvnBdO@=HHBtyUT#m<U{?FyTpjwH z{5GF&3rHSX%>p&y)9^EUq?%_LLlckh{}ca1u)h=Y6LVKpd^Q#q7A_?v*f%bY*OZ0h zT0iIJv|U{**<hI|yt)ej1AAP2d~t_gJQn0sBvL6=Tie>SpFijP;<rs#UQ@$sZEYPT z4(c~9F|oJ*Buqn2PVV<5H?eHkc>_w<X<7S|m%DrQ6BsygE~WvJt(kvQ0+g-&)5?;n z^<3vh3*dUHbN93(5Z|U+rNACozj>DD=POf=0Y?^FOma0rK=xA5toEg(l$3?gg&-*d z4haPX&s?)7udc4Hf6a81NhLRMf$ZMi-gpr%jahDUP1Ue#V)mkniUgqA*;-swLwV{P z?0|@OeN^J3ufYhjt$vltT5>r_MMXtsLVIy9aZ><uyx5xHZxgdD0~;fH6<L3**{!Ik zxOTELXM6?xI28+vV*Zk$q2arH*fk2GM9IpUhqdemeFxyfa((^%Nrx0G@raZ|!@|%N zPbvq|sl3{1YOid(hE*LMdA|Tjnr#n~h)+m>HYkCUc5!t@1E8TnP~)tGo3}*7#ozs2 zXv>&y4>H<UYZj%%?fUcmVNpj%jzGjp4<V@R#rbgY^-Uyqem1@V4F}6B&pV(K0qn~= zz;G#P_e@ENy3RS5zkYp$e6@LcfHASWlf*<C9c9%X`1a6IJW~rOXUgr<a^d+M0Ao$o zu-Bj>McM`LJz*Q&0221kQo42AN;_9A9Jc|!ss*eV*vC6}?nHtY0|Nt<1=3V&rVASz zX+bY$-6z3a_sHxrEj`_~=QRm~gag^0zHkQ0NpcnJ%E`@5U00V}*t*ruhqYv{>xKm( z?b)9J3y&9j-mhG2NK)TcQBh$d%1|5f*TV%`3cSC5*FRAV4_Cy)!wa<3*4ECu*&C`C zD97gi0M%Aiy3oCQYGGjto#$Kk67Nb=h_vtH-B(axPt*BVKLeCbOig`kXrM8zc?xv& zV?V|3ox(}^PYZ1UMomaJJx5W7l)OBV5BSY%#G!x2H7D}Z?a6u>y-vHP_iZvI9FrEp zQjG=DCYhlimH5quo>*Hea6McX1j%!HdTJom@iaLW!g?~70jd+wk^+4a+{{P)w`au5 zCSjoKw2Z~x(%0A5=^hywxw;+;2P(*^kw{sUAkIJ*c)DM}R*7Cz*#GmVh~R90MGb+# zoSmN=LMUD#ZT_xQ84KK@p&<|v5do8J`pPvlG~`!T!|5|Y&w`sY#QRV0+?=JC5<nJF z<5lvTH*ak0mR?kM?13%`)o&^}hFlBkEau-f1IrYp{}pU+ZPj`%DI)Bd!c@WB16~%? z7UjmISw$VrE-t|p2pb88vAcSp|A=G=pXvPef1xA!|JM?|!CyMycQV|oIRdR=9PI~s K>Yr3?qW%ZABJ|Aw literal 0 HcmV?d00001 From db5441eba1d4c3bee11dac239a685e3c0630cda5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:40:38 +0000 Subject: [PATCH 090/243] =?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 d3f80fac9..8b338f60d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* 🔧 Update sponsors, LambdaTest changes to TestMu AI. PR [#14734](https://github.com/fastapi/fastapi/pull/14734) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/cache from 4 to 5. PR [#14511](https://github.com/fastapi/fastapi/pull/14511) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/upload-artifact from 5 to 6. PR [#14525](https://github.com/fastapi/fastapi/pull/14525) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/download-artifact from 6 to 7. PR [#14526](https://github.com/fastapi/fastapi/pull/14526) by [@dependabot[bot]](https://github.com/apps/dependabot). From 463a3a24d7aee142bede2bcdd0ef542646b1fe3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 19 Jan 2026 12:55:32 -0800 Subject: [PATCH 091/243] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors:=20rem?= =?UTF-8?q?ove=20Requestly=20(#14735)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index c0bdb15f6..f8085b452 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -68,6 +68,3 @@ bronze: - url: https://www.testmu.ai/?utm_source=fastapi&utm_medium=partner&utm_campaign=sponsor&utm_term=opensource&utm_content=webpage title: TestMu AI. The Native AI-Agentic Cloud Platform to Supercharge Quality Engineering. img: https://fastapi.tiangolo.com/img/sponsors/testmu.png - - url: https://requestly.com/fastapi - title: All-in-one platform to Test, Mock and Intercept APIs. Built for speed, privacy and offline support. - img: https://fastapi.tiangolo.com/img/sponsors/requestly.png From ad6b2901a6ab9350c55f261b06dfd1dc363e1fde Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:56:10 +0000 Subject: [PATCH 092/243] =?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 8b338f60d..4cc8bf018 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* 🔧 Update sponsors: remove Requestly. PR [#14735](https://github.com/fastapi/fastapi/pull/14735) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, LambdaTest changes to TestMu AI. PR [#14734](https://github.com/fastapi/fastapi/pull/14734) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/cache from 4 to 5. PR [#14511](https://github.com/fastapi/fastapi/pull/14511) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/upload-artifact from 5 to 6. PR [#14525](https://github.com/fastapi/fastapi/pull/14525) by [@dependabot[bot]](https://github.com/apps/dependabot). From 6afb15c518e7c151c02639b3704424477997da87 Mon Sep 17 00:00:00 2001 From: Kader Miyanyedi <48386782+Kadermiyanyedi@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:34:03 +0300 Subject: [PATCH 093/243] =?UTF-8?q?=F0=9F=8C=90=20Improve=20LLM=20prompt?= =?UTF-8?q?=20for=20Turkish=20translations=20(#14728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/llm-prompt.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/tr/llm-prompt.md b/docs/tr/llm-prompt.md index 297b0a0e6..2ba922ec5 100644 --- a/docs/tr/llm-prompt.md +++ b/docs/tr/llm-prompt.md @@ -4,10 +4,16 @@ Translate to Turkish (Türkçe). Language code: tr. +### Core principle + +Don't translate word-by-word. Rewrite naturally in Turkish as if writing the doc from scratch. Preserve meaning, but prioritize fluency over literal accuracy. + ### Grammar and tone - Use instructional Turkish, consistent with existing Turkish docs. -- Use imperative/guide language when appropriate (e.g. “açalım”, “gidin”, “kopyalayalım”). +- Use imperative/guide language (e.g. "açalım", "gidin", "kopyalayalım", "bir bakalım"). +- Avoid filler words and overly long sentences. +- Ensure sentences make sense in Turkish context — adjust structure, conjunctions, and verb forms as needed for natural flow (e.g. use "Ancak" instead of "Ve" when connecting contrasting sentences, use "-maktadır/-mektedir" for formal statements). ### Headings @@ -15,13 +21,23 @@ Language code: tr. ### Quotes -- Alıntı stili mevcut Türkçe dokümanlarla tutarlı tutun (genellikle metin içinde ASCII tırnak işaretleri kullanılır). -- Satır içi kod, kod blokları, URL'ler veya dosya yolları içindeki tırnak işaretlerini asla değiştirmeyin. +- Keep quote style consistent with existing Turkish docs (typically ASCII quotes in text). +- Never modify quotes inside inline code, code blocks, URLs, or file paths. ### Ellipsis -- Üç nokta (...) stili mevcut Türkçe dokümanlarla tutarlı tutun. -- Kod, URL veya CLI örneklerindeki `...` ifadesini asla değiştirmeyin. +- Keep ellipsis style (`...`) consistent with existing Turkish docs. +- Never modify `...` in code, URLs, or CLI examples. + +### Consistency + +- Use the same translation for the same term throughout the document. +- If you translate a concept one way, keep it consistent across all occurrences. + +### Links and references + +- Never modify link syntax like `{.internal-link target=_blank}`. +- Keep markdown link structure intact: `[text](url){.internal-link}`. ### Preferred translations / glossary From 7443bc7a4684b3d1754c0ab59bb9e878bac96a6b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:34:36 +0000 Subject: [PATCH 094/243] =?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 4cc8bf018..8d9790039 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Translations +* 🌐 Improve LLM prompt for Turkish translations. PR [#14728](https://github.com/fastapi/fastapi/pull/14728) by [@Kadermiyanyedi](https://github.com/Kadermiyanyedi). * 🌐 Update portuguese llm-prompt.md. PR [#14702](https://github.com/fastapi/fastapi/pull/14702) by [@ceb10n](https://github.com/ceb10n). * 🌐 Update LLM prompt instructions file for French. PR [#14618](https://github.com/fastapi/fastapi/pull/14618) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for ko (add-missing). PR [#14699](https://github.com/fastapi/fastapi/pull/14699) by [@tiangolo](https://github.com/tiangolo). From 0ab68a762f5fbf86777a45afc8df7059e07a0a7d Mon Sep 17 00:00:00 2001 From: "hy.lee" <rurouni24@gmail.com> Date: Wed, 21 Jan 2026 05:37:04 +0900 Subject: [PATCH 095/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20Korean=20LLM=20?= =?UTF-8?q?prompt=20(#14740)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/llm-prompt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ko/llm-prompt.md b/docs/ko/llm-prompt.md index df807c949..533160eab 100644 --- a/docs/ko/llm-prompt.md +++ b/docs/ko/llm-prompt.md @@ -8,6 +8,7 @@ Language code: ko. - Use polite, instructional Korean (e.g. 합니다/하세요 style). - Keep the tone consistent with the existing Korean FastAPI docs. +- Do not translate “You” literally as “당신”. Use “여러분” where appropriate, or omit the subject if it sounds more natural in Korean. ### Headings From 7faa7089d665ad9e47a73d4a55457a23ba5cf643 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:37:28 +0000 Subject: [PATCH 096/243] =?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 8d9790039..7c5460f1d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Translations +* 🌐 Update Korean LLM prompt. PR [#14740](https://github.com/fastapi/fastapi/pull/14740) by [@hard-coders](https://github.com/hard-coders). * 🌐 Improve LLM prompt for Turkish translations. PR [#14728](https://github.com/fastapi/fastapi/pull/14728) by [@Kadermiyanyedi](https://github.com/Kadermiyanyedi). * 🌐 Update portuguese llm-prompt.md. PR [#14702](https://github.com/fastapi/fastapi/pull/14702) by [@ceb10n](https://github.com/ceb10n). * 🌐 Update LLM prompt instructions file for French. PR [#14618](https://github.com/fastapi/fastapi/pull/14618) by [@tiangolo](https://github.com/tiangolo). From 2d459e4845e8de9e080039d8f3ab2f1fd2e774a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 20 Jan 2026 12:40:17 -0800 Subject: [PATCH 097/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20pt=20(update-outdated)=20(#14724)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- docs/pt/docs/_llm-test.md | 48 +++++------ .../path-operation-advanced-configuration.md | 64 ++++---------- docs/pt/docs/advanced/settings.md | 44 ---------- ...migrate-from-pydantic-v1-to-pydantic-v2.md | 22 ++--- .../docs/how-to/separate-openapi-schemas.md | 8 +- docs/pt/docs/index.md | 86 ++++++++++--------- docs/pt/docs/tutorial/bigger-applications.md | 84 +++++++++--------- docs/pt/docs/tutorial/body-updates.md | 42 +++------ docs/pt/docs/tutorial/body.md | 18 ++-- docs/pt/docs/tutorial/extra-models.md | 26 ++---- .../tutorial/query-params-str-validations.md | 34 +++----- docs/pt/docs/tutorial/response-model.md | 14 --- docs/pt/docs/tutorial/schema-extra-example.md | 36 ++------ 13 files changed, 188 insertions(+), 338 deletions(-) diff --git a/docs/pt/docs/_llm-test.md b/docs/pt/docs/_llm-test.md index 3da5e8a71..b59292f47 100644 --- a/docs/pt/docs/_llm-test.md +++ b/docs/pt/docs/_llm-test.md @@ -1,8 +1,8 @@ # Arquivo de teste de LLM { #llm-test-file } -Este documento testa se o <abbr title="Large Language Model – Modelo de Linguagem de Grande Porte">LLM</abbr>, que traduz a documentação, entende o `general_prompt` em `scripts/translate.py` e o prompt específico do idioma em `docs/{language code}/llm-prompt.md`. O prompt específico do idioma é anexado ao `general_prompt`. +Este documento testa se o <abbr title="Large Language Model - Modelo de Linguagem de Grande Porte">LLM</abbr>, que traduz a documentação, entende o `general_prompt` em `scripts/translate.py` e o prompt específico do idioma em `docs/{language code}/llm-prompt.md`. O prompt específico do idioma é anexado ao `general_prompt`. -Os testes adicionados aqui serão vistos por todos os autores dos prompts específicos de idioma. +Os testes adicionados aqui serão vistos por todos os designers dos prompts específicos de idioma. Use da seguinte forma: @@ -23,7 +23,7 @@ Este é um trecho de código: `foo`. E este é outro trecho de código: `bar`. E //// -//// tab | Informações +//// tab | Informação O conteúdo dos trechos de código deve ser deixado como está. @@ -45,9 +45,9 @@ O LLM provavelmente vai traduzir isso errado. O interessante é apenas se ele ma //// -//// tab | Informações +//// tab | Informação -O autor do prompt pode escolher se deseja converter aspas neutras em aspas tipográficas. Também é aceitável deixá-las como estão. +O designer do prompt pode escolher se quer converter aspas neutras em aspas tipográficas. Também é aceitável deixá-las como estão. Veja, por exemplo, a seção `### Quotes` em `docs/de/llm-prompt.md`. @@ -67,7 +67,7 @@ Pesado: `Yesterday, my friend wrote: "If you spell incorrectly correctly, you ha //// -//// tab | Informações +//// tab | Informação ... No entanto, as aspas dentro de trechos de código devem permanecer como estão. @@ -95,24 +95,24 @@ $ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid ...e outro exemplo de código de console... ```console -// Crie um diretório "Code" +// Criar um diretório "Code" $ mkdir code -// Entre nesse diretório +// Mudar para esse diretório $ cd code ``` ...e um exemplo de código Python... ```Python -wont_work() # Isto não vai funcionar 😱 -works(foo="bar") # Isto funciona 🎉 +wont_work() # This won't work 😱 +works(foo="bar") # This works 🎉 ``` ...e é isso. //// -//// tab | Informações +//// tab | Informação O código em blocos de código não deve ser modificado, com exceção dos comentários. @@ -154,7 +154,7 @@ Algum texto //// -//// tab | Informações +//// tab | Informação Abas e blocos `Info`/`Note`/`Warning`/etc. devem ter a tradução do seu título adicionada após uma barra vertical (`|`). @@ -181,7 +181,7 @@ O texto do link deve ser traduzido, o endereço do link deve apontar para a trad //// -//// tab | Informações +//// tab | Informação Os links devem ser traduzidos, mas seus endereços devem permanecer inalterados. Uma exceção são links absolutos para páginas da documentação do FastAPI. Nesse caso, devem apontar para a tradução. @@ -197,10 +197,10 @@ Aqui estão algumas coisas envolvidas em elementos HTML "abbr" (algumas são inv ### O abbr fornece uma frase completa { #the-abbr-gives-a-full-phrase } -* <abbr title="Getting Things Done – Fazer as Coisas">GTD</abbr> -* <abbr title="menos que"><code>lt</code></abbr> -* <abbr title="XML Web Token – Token Web XML">XWT</abbr> -* <abbr title="Parallel Server Gateway Interface – Interface de Gateway de Servidor Paralelo">PSGI</abbr> +* <abbr title="Getting Things Done">GTD</abbr> +* <abbr title="less than - menos que"><code>lt</code></abbr> +* <abbr title="XML Web Token">XWT</abbr> +* <abbr title="Parallel Server Gateway Interface - Interface de Gateway de Servidor Paralelo">PSGI</abbr> ### O abbr fornece uma explicação { #the-abbr-gives-an-explanation } @@ -209,12 +209,12 @@ Aqui estão algumas coisas envolvidas em elementos HTML "abbr" (algumas são inv ### O abbr fornece uma frase completa e uma explicação { #the-abbr-gives-a-full-phrase-and-an-explanation } -* <abbr title="Mozilla Developer Network – Rede de Desenvolvedores da Mozilla: documentação para desenvolvedores, escrita pelo pessoal do Firefox">MDN</abbr> -* <abbr title="Input/Output – Entrada/Saída: leitura ou escrita em disco, comunicações de rede.">I/O</abbr>. +* <abbr title="Mozilla Developer Network: documentação para desenvolvedores, escrita pelo pessoal do Firefox">MDN</abbr> +* <abbr title="Input/Output: leitura ou escrita em disco, comunicações de rede.">I/O</abbr>. //// -//// tab | Informações +//// tab | Informação Os atributos "title" dos elementos "abbr" são traduzidos seguindo algumas instruções específicas. @@ -228,7 +228,7 @@ Veja a seção `### HTML abbr elements` no prompt geral em `scripts/translate.py //// tab | Teste -### Desenvolver uma aplicação web - um tutorial { #develop-a-webapp-a-tutorial } +### Desenvolver uma webapp - um tutorial { #develop-a-webapp-a-tutorial } Olá. @@ -242,7 +242,7 @@ Olá novamente. //// -//// tab | Informações +//// tab | Informação A única regra rígida para títulos é que o LLM deixe a parte do hash dentro de chaves inalterada, o que garante que os links não quebrem. @@ -494,9 +494,9 @@ Para algumas instruções específicas do idioma, veja, por exemplo, a seção ` //// -//// tab | Informações +//// tab | Informação -Esta é uma lista não completa e não normativa de termos (principalmente) técnicos vistos na documentação. Pode ser útil para o autor do prompt descobrir para quais termos o LLM precisa de uma ajudinha. Por exemplo, quando ele continua revertendo uma boa tradução para uma tradução subótima. Ou quando tem problemas para conjugar/declinar um termo no seu idioma. +Esta é uma lista não completa e não normativa de termos (principalmente) técnicos vistos na documentação. Pode ser útil para o designer do prompt descobrir para quais termos o LLM precisa de uma ajudinha. Por exemplo, quando ele continua revertendo uma boa tradução para uma tradução subótima. Ou quando tem problemas para conjugar/declinar um termo no seu idioma. Veja, por exemplo, a seção `### List of English terms and their preferred German translations` em `docs/de/llm-prompt.md`. diff --git a/docs/pt/docs/advanced/path-operation-advanced-configuration.md b/docs/pt/docs/advanced/path-operation-advanced-configuration.md index e1c3e5ab8..b3af116a2 100644 --- a/docs/pt/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/pt/docs/advanced/path-operation-advanced-configuration.md @@ -10,7 +10,7 @@ Se você não é um "especialista" no OpenAPI, você provavelmente não precisa Você pode definir o `operationId` do OpenAPI que será utilizado na sua *operação de rota* com o parâmetro `operation_id`. -Você precisa ter certeza que ele é único para cada operação. +Você deveria ter certeza que ele é único para cada operação. {* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} @@ -18,13 +18,13 @@ Você precisa ter certeza que ele é único para cada operação. Se você quiser utilizar o nome das funções da sua API como `operationId`s, você pode iterar sobre todos esses nomes e sobrescrever o `operation_id` em cada *operação de rota* utilizando o `APIRoute.name` dela. -Você deve fazer isso depois de adicionar todas as suas *operações de rota*. +Você deveria fazer isso depois de adicionar todas as suas *operações de rota*. {* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *} /// tip | Dica -Se você chamar `app.openapi()` manualmente, os `operationId`s devem ser atualizados antes dessa chamada. +Se você chamar `app.openapi()` manualmente, você deveria atualizar os `operationId`s antes dessa chamada. /// @@ -44,11 +44,11 @@ Para excluir uma *operação de rota* do esquema OpenAPI gerado (e por consequê ## Descrição avançada a partir de docstring { #advanced-description-from-docstring } -Você pode limitar as linhas utilizadas a partir de uma docstring de uma *função de operação de rota* para o OpenAPI. +Você pode limitar as linhas utilizadas a partir da docstring de uma *função de operação de rota* para o OpenAPI. -Adicionar um `\f` (um caractere de escape para alimentação de formulário) faz com que o **FastAPI** restrinja a saída utilizada pelo OpenAPI até esse ponto. +Adicionar um `\f` (um caractere de escape para "form feed") faz com que o **FastAPI** trunque a saída usada para o OpenAPI até esse ponto. -Ele não será mostrado na documentação, mas outras ferramentas (como o Sphinx) serão capazes de utilizar o resto do texto. +Ele não será mostrado na documentação, mas outras ferramentas (como o Sphinx) serão capazes de utilizar o resto. {* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} @@ -131,70 +131,38 @@ E se você olhar o esquema OpenAPI resultante (na rota `/openapi.json` da sua AP ### Esquema de *operação de rota* do OpenAPI personalizado { #custom-openapi-path-operation-schema } -O dicionário em `openapi_extra` vai ter todos os seus níveis mesclados dentro do esquema OpenAPI gerado automaticamente para a *operação de rota*. +O dicionário em `openapi_extra` vai ser mesclado profundamente com o esquema OpenAPI gerado automaticamente para a *operação de rota*. -Então, você pode adicionar dados extras para o esquema gerado automaticamente. +Então, você pode adicionar dados extras ao esquema gerado automaticamente. -Por exemplo, você poderia optar por ler e validar a requisição com seu próprio código, sem utilizar funcionalidades automatizadas do FastAPI com o Pydantic, mas você ainda pode quere definir a requisição no esquema OpenAPI. +Por exemplo, você poderia decidir ler e validar a requisição com seu próprio código, sem usar as funcionalidades automáticas do FastAPI com o Pydantic, mas ainda assim querer definir a requisição no esquema OpenAPI. Você pode fazer isso com `openapi_extra`: {* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} -Nesse exemplo, nós não declaramos nenhum modelo do Pydantic. Na verdade, o corpo da requisição não está nem mesmo <abbr title="convertido de um formato plano, como bytes, para objetos Python">analisado</abbr> como JSON, ele é lido diretamente como `bytes` e a função `magic_data_reader()` seria a responsável por analisar ele de alguma forma. +Nesse exemplo, nós não declaramos nenhum modelo do Pydantic. Na verdade, o corpo da requisição não está nem mesmo <abbr title="converted from some plain format, like bytes, into Python objects - convertido de algum formato simples, como bytes, em objetos Python">analisado</abbr> como JSON, ele é lido diretamente como `bytes`, e a função `magic_data_reader()` seria a responsável por analisar ele de alguma forma. De toda forma, nós podemos declarar o esquema esperado para o corpo da requisição. ### Tipo de conteúdo do OpenAPI personalizado { #custom-openapi-content-type } -Utilizando esse mesmo truque, você pode utilizar um modelo Pydantic para definir o JSON Schema que é então incluído na seção do esquema personalizado do OpenAPI na *operação de rota*. +Utilizando esse mesmo truque, você pode usar um modelo Pydantic para definir o JSON Schema que é então incluído na seção do esquema personalizado do OpenAPI na *operação de rota*. -E você pode fazer isso até mesmo quando os dados da requisição não seguem o formato JSON. +E você pode fazer isso até mesmo quando o tipo de dados na requisição não é JSON. -Por exemplo, nesta aplicação nós não usamos a funcionalidade integrada ao FastAPI de extrair o JSON Schema dos modelos Pydantic nem a validação automática do JSON. Na verdade, estamos declarando o tipo do conteúdo da requisição como YAML, em vez de JSON: - -//// tab | Pydantic v2 +Por exemplo, nesta aplicação nós não usamos a funcionalidade integrada ao FastAPI de extrair o JSON Schema dos modelos Pydantic nem a validação automática para JSON. Na verdade, estamos declarando o tipo de conteúdo da requisição como YAML, em vez de JSON: {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} -//// +Entretanto, mesmo que não utilizemos a funcionalidade integrada por padrão, ainda estamos usando um modelo Pydantic para gerar um JSON Schema manualmente para os dados que queremos receber em YAML. -//// tab | Pydantic v1 +Então utilizamos a requisição diretamente e extraímos o corpo como `bytes`. Isso significa que o FastAPI não vai sequer tentar analisar o payload da requisição como JSON. -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *} - -//// - -/// info | Informação - -Na versão 1 do Pydantic, o método para obter o JSON Schema de um modelo é `Item.schema()`, na versão 2 do Pydantic, o método é `Item.model_json_schema()`. - -/// - -Entretanto, mesmo que não utilizemos a funcionalidade integrada por padrão, ainda estamos usando um modelo Pydantic para gerar um JSON Schema manualmente para os dados que queremos receber no formato YAML. - -Então utilizamos a requisição diretamente, e extraímos o corpo como `bytes`. Isso significa que o FastAPI não vai sequer tentar analisar o corpo da requisição como JSON. - -E então no nosso código, nós analisamos o conteúdo YAML diretamente, e estamos utilizando o mesmo modelo Pydantic para validar o conteúdo YAML: - -//// tab | Pydantic v2 +E então no nosso código, nós analisamos o conteúdo YAML diretamente e, em seguida, estamos usando novamente o mesmo modelo Pydantic para validar o conteúdo YAML: {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *} - -//// - -/// info | Informação - -Na versão 1 do Pydantic, o método para analisar e validar um objeto era `Item.parse_obj()`, na versão 2 do Pydantic, o método é chamado de `Item.model_validate()`. - -/// - /// tip | Dica Aqui reutilizamos o mesmo modelo do Pydantic. diff --git a/docs/pt/docs/advanced/settings.md b/docs/pt/docs/advanced/settings.md index 6f5b7feae..28411269b 100644 --- a/docs/pt/docs/advanced/settings.md +++ b/docs/pt/docs/advanced/settings.md @@ -46,12 +46,6 @@ $ pip install "fastapi[all]" </div> -/// info | Informação - -No Pydantic v1 ele vinha incluído no pacote principal. Agora é distribuído como um pacote independente para que você possa optar por instalá-lo ou não, caso não precise dessa funcionalidade. - -/// - ### Criar o objeto `Settings` { #create-the-settings-object } Importe `BaseSettings` do Pydantic e crie uma subclasse, muito parecido com um modelo do Pydantic. @@ -60,24 +54,8 @@ Da mesma forma que com modelos do Pydantic, você declara atributos de classe co Você pode usar as mesmas funcionalidades e ferramentas de validação que usa em modelos do Pydantic, como diferentes tipos de dados e validações adicionais com `Field()`. -//// tab | Pydantic v2 - {* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} -//// - -//// tab | Pydantic v1 - -/// info | Informação - -No Pydantic v1 você importaria `BaseSettings` diretamente de `pydantic` em vez de `pydantic_settings`. - -/// - -{* ../../docs_src/settings/tutorial001_pv1_py39.py hl[2,5:8,11] *} - -//// - /// tip | Dica Se você quer algo rápido para copiar e colar, não use este exemplo, use o último abaixo. @@ -215,8 +193,6 @@ APP_NAME="ChimichangApp" E então atualizar seu `config.py` com: -//// tab | Pydantic v2 - {* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip | Dica @@ -225,26 +201,6 @@ O atributo `model_config` é usado apenas para configuração do Pydantic. Você /// -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *} - -/// tip | Dica - -A classe `Config` é usada apenas para configuração do Pydantic. Você pode ler mais em <a href="https://docs.pydantic.dev/1.10/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>. - -/// - -//// - -/// info | Informação - -Na versão 1 do Pydantic a configuração era feita em uma classe interna `Config`, na versão 2 do Pydantic é feita em um atributo `model_config`. Esse atributo recebe um `dict`, e para ter autocompletar e erros inline você pode importar e usar `SettingsConfigDict` para definir esse `dict`. - -/// - Aqui definimos a configuração `env_file` dentro da sua classe `Settings` do Pydantic e definimos o valor como o nome do arquivo dotenv que queremos usar. ### Criando o `Settings` apenas uma vez com `lru_cache` { #creating-the-settings-only-once-with-lru-cache } diff --git a/docs/pt/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/pt/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md index 2a2659a03..0995e1028 100644 --- a/docs/pt/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md +++ b/docs/pt/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -2,21 +2,23 @@ Se você tem uma aplicação FastAPI antiga, pode estar usando o Pydantic versão 1. -O FastAPI tem suporte ao Pydantic v1 ou v2 desde a versão 0.100.0. +O FastAPI versão 0.100.0 tinha suporte ao Pydantic v1 ou v2. Ele usaria aquele que você tivesse instalado. -Se você tiver o Pydantic v2 instalado, ele será utilizado. Se, em vez disso, tiver o Pydantic v1, será ele que será utilizado. +O FastAPI versão 0.119.0 introduziu suporte parcial ao Pydantic v1 a partir de dentro do Pydantic v2 (como `pydantic.v1`), para facilitar a migração para o v2. -O Pydantic v1 está agora descontinuado e o suporte a ele será removido nas próximas versões do FastAPI, você deveria migrar para o Pydantic v2. Assim, você terá as funcionalidades, melhorias e correções mais recentes. +O FastAPI 0.126.0 removeu o suporte ao Pydantic v1, enquanto ainda oferece suporte a `pydantic.v1` por mais algum tempo. /// warning | Atenção -Além disso, a equipe do Pydantic interrompeu o suporte ao Pydantic v1 para as versões mais recentes do Python, a partir do **Python 3.14**. +A equipe do Pydantic interrompeu o suporte ao Pydantic v1 para as versões mais recentes do Python, a partir do **Python 3.14**. + +Isso inclui `pydantic.v1`, que não é mais suportado no Python 3.14 e superiores. Se quiser usar as funcionalidades mais recentes do Python, você precisará garantir que usa o Pydantic v2. /// -Se você tem uma aplicação FastAPI antiga com Pydantic v1, aqui vou mostrar como migrá-la para o Pydantic v2 e as **novas funcionalidades no FastAPI 0.119.0** para ajudar em uma migração gradual. +Se você tem uma aplicação FastAPI antiga com Pydantic v1, aqui vou mostrar como migrá-la para o Pydantic v2, e as **funcionalidades no FastAPI 0.119.0** para ajudar em uma migração gradual. ## Guia oficial { #official-guide } @@ -44,7 +46,7 @@ Depois disso, você pode rodar os testes e verificar se tudo funciona. Se funcio ## Pydantic v1 no v2 { #pydantic-v1-in-v2 } -O Pydantic v2 inclui tudo do Pydantic v1 como um submódulo `pydantic.v1`. +O Pydantic v2 inclui tudo do Pydantic v1 como um submódulo `pydantic.v1`. Mas isso não é mais suportado em versões acima do Python 3.13. Isso significa que você pode instalar a versão mais recente do Pydantic v2 e importar e usar os componentes antigos do Pydantic v1 a partir desse submódulo, como se tivesse o Pydantic v1 antigo instalado. @@ -66,7 +68,7 @@ Tenha em mente que, como a equipe do Pydantic não oferece mais suporte ao Pydan ### Pydantic v1 e v2 na mesma aplicação { #pydantic-v1-and-v2-on-the-same-app } -Não é suportado pelo Pydantic ter um modelo do Pydantic v2 com campos próprios definidos como modelos do Pydantic v1, ou vice-versa. +Não é **suportado** pelo Pydantic ter um modelo do Pydantic v2 com campos próprios definidos como modelos do Pydantic v1, ou vice-versa. ```mermaid graph TB @@ -86,7 +88,7 @@ graph TB style V2Field fill:#f9fff3 ``` -...but, you can have separated models using Pydantic v1 and v2 in the same app. +...mas, você pode ter modelos separados usando Pydantic v1 e v2 na mesma aplicação. ```mermaid graph TB @@ -106,7 +108,7 @@ graph TB style V2Field fill:#f9fff3 ``` -Em alguns casos, é até possível ter modelos Pydantic v1 e v2 na mesma operação de rota na sua aplicação FastAPI: +Em alguns casos, é até possível ter modelos Pydantic v1 e v2 na mesma **operação de rota** na sua aplicação FastAPI: {* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} @@ -122,7 +124,7 @@ Se você precisar usar algumas das ferramentas específicas do FastAPI para par /// tip | Dica -Primeiro tente com o `bump-pydantic`; se seus testes passarem e isso funcionar, então você concluiu tudo com um único comando. ✨ +Primeiro tente com o `bump-pydantic`, se seus testes passarem e isso funcionar, então você concluiu tudo com um único comando. ✨ /// diff --git a/docs/pt/docs/how-to/separate-openapi-schemas.md b/docs/pt/docs/how-to/separate-openapi-schemas.md index 8855934fd..f757025a0 100644 --- a/docs/pt/docs/how-to/separate-openapi-schemas.md +++ b/docs/pt/docs/how-to/separate-openapi-schemas.md @@ -1,8 +1,8 @@ # Esquemas OpenAPI Separados para Entrada e Saída ou Não { #separate-openapi-schemas-for-input-and-output-or-not } -Ao usar **Pydantic v2**, o OpenAPI gerado é um pouco mais exato e **correto** do que antes. 😎 +Desde que o **Pydantic v2** foi lançado, o OpenAPI gerado é um pouco mais exato e **correto** do que antes. 😎 -Inclusive, em alguns casos, ele terá até **dois JSON Schemas** no OpenAPI para o mesmo modelo Pydantic, para entrada e saída, dependendo se eles possuem **valores padrão**. +De fato, em alguns casos, ele terá até **dois JSON Schemas** no OpenAPI para o mesmo modelo Pydantic, para entrada e saída, dependendo se eles possuem **valores padrão**. Vamos ver como isso funciona e como alterar se for necessário. @@ -95,10 +95,8 @@ O suporte para `separate_input_output_schemas` foi adicionado no FastAPI `0.102. ### Mesmo Esquema para Modelos de Entrada e Saída na Documentação { #same-schema-for-input-and-output-models-in-docs } -E agora haverá um único esquema para entrada e saída para o modelo, apenas `Item`, e `description` **não será obrigatório**: +E agora haverá um único esquema para entrada e saída para o modelo, apenas `Item`, e ele terá `description` como **não obrigatório**: <div class="screenshot"> <img src="/img/tutorial/separate-openapi-schemas/image05.png"> </div> - -Esse é o mesmo comportamento do Pydantic v1. 🤓 diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index 0428c3a79..4e3be586d 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -40,8 +40,8 @@ Os recursos chave são: * **Rápido**: alta performance, equivalente a **NodeJS** e **Go** (graças ao Starlette e Pydantic). [Um dos frameworks mais rápidos disponíveis](#performance). * **Rápido para codar**: Aumenta a velocidade para desenvolver recursos entre 200% a 300%. * * **Poucos bugs**: Reduz cerca de 40% de erros induzidos por humanos (desenvolvedores). * -* **Intuitivo**: Grande suporte a _IDEs_. <abbr title="também conhecido como autocompletar, preenchimento automático, IntelliSense">Preenchimento automático</abbr> em todos os lugares. Menos tempo debugando. -* **Fácil**: Projetado para ser fácil de aprender e usar. Menos tempo lendo documentação. +* **Intuitivo**: Grande suporte a editores. <abbr title="também conhecido como auto-complete, autocompletion, IntelliSense">Completação</abbr> em todos os lugares. Menos tempo debugando. +* **Fácil**: Projetado para ser fácil de aprender e usar. Menos tempo lendo docs. * **Enxuto**: Minimize duplicação de código. Múltiplas funcionalidades para cada declaração de parâmetro. Menos bugs. * **Robusto**: Tenha código pronto para produção. E com documentação interativa automática. * **Baseado em padrões**: Baseado em (e totalmente compatível com) os padrões abertos para APIs: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (anteriormente conhecido como Swagger) e <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. @@ -73,7 +73,7 @@ Os recursos chave são: ## Opiniões { #opinions } -"*[...] Estou usando **FastAPI** muito esses dias. [...] Estou na verdade planejando utilizar ele em todos os times de **serviços _Machine Learning_ na Microsoft**. Alguns deles estão sendo integrados no _core_ do produto **Windows** e alguns produtos **Office**.*" +"_[...] Estou usando **FastAPI** muito esses dias. [...] Estou na verdade planejando utilizar ele em todos os times de **serviços ML na Microsoft**. Alguns deles estão sendo integrados no _core_ do produto **Windows** e alguns produtos **Office**._" <div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/fastapi/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div> @@ -91,39 +91,45 @@ Os recursos chave são: --- -"*Estou extremamente entusiasmado com o **FastAPI**. É tão divertido!*" +"_Estou muito entusiasmado com o **FastAPI**. É tão divertido!_" -<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> podcaster</strong> <a href="https://x.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div> +<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> apresentador do podcast</strong> <a href="https://x.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div> --- -"*Honestamente, o que você construiu parece super sólido e rebuscado. De muitas formas, eu queria que o **Hug** fosse assim - é realmente inspirador ver alguém que construiu ele.*" +"_Honestamente, o que você construiu parece super sólido e refinado. De muitas formas, é o que eu queria que o **Hug** fosse - é realmente inspirador ver alguém construir isso._" <div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong>criador do<a href="https://github.com/hugapi/hug" target="_blank">Hug</a></strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> --- -"*Se você está procurando aprender um **_framework_ moderno** para construir aplicações _REST_, dê uma olhada no **FastAPI** [...] É rápido, fácil de usar e fácil de aprender [...]*" +"_Se você está procurando aprender um **framework moderno** para construir APIs REST, dê uma olhada no **FastAPI** [...] É rápido, fácil de usar e fácil de aprender [...]_" -"*Nós trocamos nossas **APIs** por **FastAPI** [...] Acredito que vocês gostarão dele [...]*" +"_Nós trocamos nossas **APIs** por **FastAPI** [...] Acredito que você gostará dele [...]_" <div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong>fundadores da <a href="https://explosion.ai" target="_blank">Explosion AI</a> - criadores da <a href="https://spacy.io" target="_blank">spaCy</a></strong> <a href="https://x.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://x.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div> --- -"_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._" +"_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**, e se tornou um **componente chave** para a nossa estratégia de desenvolvimento API first, impulsionando diversas automações e serviços, como o nosso Virtual TAC Engineer._" <div style="text-align: right; margin-right: 10%;">Deon Pillsbury - <strong>Cisco</strong> <a href="https://www.linkedin.com/posts/deonpillsbury_cisco-cx-python-activity-6963242628536487936-trAp/" target="_blank"><small>(ref)</small></a></div> --- +## Mini documentário do FastAPI { #fastapi-mini-documentary } + +Há um <a href="https://www.youtube.com/watch?v=mpR8ngthqiE" class="external-link" target="_blank">mini documentário do FastAPI</a> lançado no fim de 2025, você pode assisti-lo online: + +<a href="https://www.youtube.com/watch?v=mpR8ngthqiE" target="_blank"><img src="https://fastapi.tiangolo.com/img/fastapi-documentary.jpg" alt="FastAPI Mini Documentary"></a> + ## **Typer**, o FastAPI das interfaces de linhas de comando { #typer-the-fastapi-of-clis } <a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> -Se você estiver construindo uma aplicação <abbr title="Command Line Interface – Interface de Linha de Comando">CLI</abbr> para ser utilizada em um terminal ao invés de uma aplicação web, dê uma olhada no <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>. +Se você estiver construindo uma aplicação <abbr title="Command Line Interface - Interface de Linha de Comando">CLI</abbr> para ser utilizada no terminal ao invés de uma API web, dê uma olhada no <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>. -**Typer** é o irmão menor do FastAPI. E seu propósito é ser o **FastAPI das _CLIs_**. ⌨️ 🚀 +**Typer** é o irmão menor do FastAPI. E seu propósito é ser o **FastAPI das CLIs**. ⌨️ 🚀 ## Requisitos { #requirements } @@ -255,10 +261,10 @@ Você verá a resposta JSON como: Você acabou de criar uma API que: -* Recebe requisições HTTP nas _rotas_ `/` e `/items/{item_id}`. -* Ambas _rotas_ fazem <em>operações</em> `GET` (também conhecido como _métodos_ HTTP). -* A _rota_ `/items/{item_id}` tem um _parâmetro de rota_ `item_id` que deve ser um `int`. -* A _rota_ `/items/{item_id}` tem um _parâmetro query_ `q` `str` opcional. +* Recebe requisições HTTP nos _paths_ `/` e `/items/{item_id}`. +* Ambos _paths_ fazem <em>operações</em> `GET` (também conhecido como _métodos_ HTTP). +* O _path_ `/items/{item_id}` tem um _parâmetro de path_ `item_id` que deve ser um `int`. +* O _path_ `/items/{item_id}` tem um _parâmetro query_ `q` `str` opcional. ### Documentação Interativa da API { #interactive-api-docs } @@ -278,7 +284,7 @@ Você verá a documentação automática alternativa (fornecida por <a href="htt ## Evoluindo o Exemplo { #example-upgrade } -Agora modifique o arquivo `main.py` para receber um corpo para uma requisição `PUT`. +Agora modifique o arquivo `main.py` para receber um corpo de uma requisição `PUT`. Declare o corpo utilizando tipos padrão Python, graças ao Pydantic. @@ -334,7 +340,7 @@ Agora vá para <a href="http://127.0.0.1:8000/docs" class="external-link" target E agora, vá para <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. -* A documentação alternativa também irá refletir o novo parâmetro da _query_ e o corpo: +* A documentação alternativa também irá refletir o novo parâmetro query e o corpo: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) @@ -368,15 +374,15 @@ item: Item * Validação de dados: * Erros automáticos e claros quando o dado é inválido. * Validação até para objetos JSON profundamente aninhados. -* <abbr title="também conhecido como: serialização, parsing, marshalling">Conversão</abbr> de dados de entrada: vindo da rede para dados e tipos Python. Consegue ler: +* <abbr title="também conhecido como: serialization, parsing, marshalling">Conversão</abbr> de dados de entrada: vindo da rede para dados e tipos Python. Consegue ler: * JSON. - * Parâmetros de rota. - * Parâmetros de _query_ . - * _Cookies_. + * Parâmetros de path. + * Parâmetros query. + * Cookies. * Cabeçalhos. * Formulários. * Arquivos. -* <abbr title="também conhecido como: serialização, parsing, marshalling">Conversão</abbr> de dados de saída de tipos e dados Python para dados de rede (como JSON): +* <abbr title="também conhecido como: serialization, parsing, marshalling">Conversão</abbr> de dados de saída: convertendo de tipos e dados Python para dados de rede (como JSON): * Converte tipos Python (`str`, `int`, `float`, `bool`, `list` etc). * Objetos `datetime`. * Objetos `UUID`. @@ -390,17 +396,17 @@ item: Item Voltando ao código do exemplo anterior, **FastAPI** irá: -* Validar que existe um `item_id` na rota para requisições `GET` e `PUT`. +* Validar que existe um `item_id` no path para requisições `GET` e `PUT`. * Validar que `item_id` é do tipo `int` para requisições `GET` e `PUT`. - * Se não é validado, o cliente verá um útil, claro erro. -* Verificar se existe um parâmetro de _query_ opcional nomeado como `q` (como em `http://127.0.0.1:8000/items/foo?q=somequery`) para requisições `GET`. + * Se não for, o cliente verá um erro útil e claro. +* Verificar se existe um parâmetro query opcional nomeado como `q` (como em `http://127.0.0.1:8000/items/foo?q=somequery`) para requisições `GET`. * Como o parâmetro `q` é declarado com `= None`, ele é opcional. - * Sem o `None` ele poderia ser obrigatório (como o corpo no caso de `PUT`). + * Sem o `None` ele seria obrigatório (como o corpo no caso de `PUT`). * Para requisições `PUT` para `/items/{item_id}`, lerá o corpo como JSON: * Verifica que tem um atributo obrigatório `name` que deve ser `str`. - * Verifica que tem um atributo obrigatório `price` que deve ser `float`. - * Verifica que tem an atributo opcional `is_offer`, que deve ser `bool`, se presente. - * Tudo isso também funciona para objetos JSON profundamente aninhados. + * Verifica que tem um atributo obrigatório `price` que tem que ser um `float`. + * Verifica que tem um atributo opcional `is_offer`, que deve ser um `bool`, se presente. + * Tudo isso também funcionaria para objetos JSON profundamente aninhados. * Converter de e para JSON automaticamente. * Documentar tudo com OpenAPI, que poderá ser usado por: * Sistemas de documentação interativos. @@ -409,7 +415,7 @@ Voltando ao código do exemplo anterior, **FastAPI** irá: --- -Nós apenas arranhamos a superfície, mas você já tem idéia de como tudo funciona. +Nós apenas arranhamos a superfície, mas você já tem ideia de como tudo funciona. Experimente mudar a seguinte linha: @@ -437,22 +443,22 @@ Para um exemplo mais completo incluindo mais recursos, veja <a href="https://fas **Alerta de Spoiler**: o tutorial - guia do usuário inclui: -* Declaração de **parâmetetros** de diferentes lugares como: **cabeçalhos**, **cookies**, **campos de formulários** e **arquivos**. -* Como configurar **Limitações de Validação** como `maximum_length` ou `regex`. -* Um poderoso e fácil de usar sistema de **<abbr title="também conhecido como componentes, recursos, fornecedores, serviços, injetáveis">Injeção de Dependência</abbr>**. -* Segurança e autenticação, incluindo suporte para **OAuth2** com autenticação **JWT tokens** e **HTTP Basic**. +* Declaração de **parâmetros** de diferentes lugares como: **cabeçalhos**, **cookies**, **campos de formulários** e **arquivos**. +* Como configurar **limitações de validação** como `maximum_length` ou `regex`. +* Um poderoso e fácil de usar sistema de **<abbr title="também conhecido como components, resources, providers, services, injectables">Injeção de Dependência</abbr>**. +* Segurança e autenticação, incluindo suporte para **OAuth2** com autenticação com **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 <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a> e outras bibliotecas. * Muitos recursos extras (graças ao Starlette) como: * **WebSockets** - * testes extrememamente fáceis baseados em HTTPX e `pytest` + * testes extremamente fáceis baseados em HTTPX e `pytest` * **CORS** * **Cookie Sessions** * ...e mais. ### Implemente sua aplicação (opcional) { #deploy-your-app-optional } -Você pode opcionalmente implantar sua aplicação FastAPI na <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, inscreva-se na lista de espera se ainda não o fez. 🚀 +Você pode opcionalmente implantar sua aplicação FastAPI na <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, vá e entre na lista de espera se ainda não o fez. 🚀 Se você já tem uma conta na **FastAPI Cloud** (nós convidamos você da lista de espera 😉), pode implantar sua aplicação com um único comando. @@ -506,7 +512,7 @@ Siga os tutoriais do seu provedor de nuvem para implantar aplicações FastAPI c Testes de performance da _Independent TechEmpower_ mostram aplicações **FastAPI** rodando sob Uvicorn como <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">um dos _frameworks_ Python mais rápidos disponíveis</a>, somente atrás de Starlette e Uvicorn (utilizados internamente pelo FastAPI). (*) -Para entender mais sobre performance, veja a seção <a href="https://fastapi.tiangolo.com/pt/benchmarks/" class="internal-link" target="_blank">Comparações</a>. +Para entender mais sobre isso, veja a seção <a href="https://fastapi.tiangolo.com/pt/benchmarks/" class="internal-link" target="_blank">Comparações</a>. ## Dependências { #dependencies } @@ -514,7 +520,7 @@ O FastAPI depende do Pydantic e do Starlette. ### Dependências `standard` { #standard-dependencies } -Quando você instala o FastAPI com `pip install "fastapi[standard]"`, ele vêm com o grupo `standard` (padrão) de dependências opcionais: +Quando você instala o FastAPI com `pip install "fastapi[standard]"`, ele vem com o grupo `standard` de dependências opcionais: Utilizado pelo Pydantic: @@ -524,7 +530,7 @@ Utilizado pelo Starlette: * <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - Obrigatório caso você queira utilizar o `TestClient`. * <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - Obrigatório se você quer utilizar a configuração padrão de templates. -* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - Obrigatório se você deseja suporte a <abbr title="convertendo a string que vem de uma requisição HTTP em dados Python">"parsing"</abbr> de formulário, com `request.form()`. +* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - Obrigatório se você deseja suporte a <abbr title="converting the string that comes from an HTTP request into Python data - convertendo a string que vem de uma requisição HTTP em dados Python">"parsing"</abbr> de formulário, com `request.form()`. Utilizado pelo FastAPI: @@ -547,7 +553,7 @@ Existem algumas dependências adicionais que você pode querer instalar. Dependências opcionais adicionais do Pydantic: * <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - para gerenciamento de configurações. -* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - tipos extras para serem utilizados com o Pydantic. +* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - para tipos extras a serem utilizados com o Pydantic. Dependências opcionais adicionais do FastAPI: diff --git a/docs/pt/docs/tutorial/bigger-applications.md b/docs/pt/docs/tutorial/bigger-applications.md index 9dec7b196..87bd13375 100644 --- a/docs/pt/docs/tutorial/bigger-applications.md +++ b/docs/pt/docs/tutorial/bigger-applications.md @@ -31,7 +31,7 @@ Digamos que você tenha uma estrutura de arquivos como esta: /// tip | Dica -Existem vários arquivos `__init__.py` presentes em cada diretório ou subdiretório. +Existem vários arquivos `__init__.py`: um em cada diretório ou subdiretório. Isso permite a importação de código de um arquivo para outro. @@ -43,32 +43,32 @@ from app.routers import items /// -* O diretório `app` contém todo o código da aplicação. Ele possui um arquivo `app/__init__.py` vazio, o que o torna um "pacote Python" (uma coleção de "módulos Python"): `app`. -* Dentro dele, o arquivo `app/main.py` está localizado em um pacote Python (diretório com `__init__.py`). Portanto, ele é um "módulo" desse pacote: `app.main`. -* Existem também um arquivo `app/dependencies.py`, assim como o `app/main.py`, ele é um "módulo": `app.dependencies`. +* O diretório `app` contém tudo. E possui um arquivo vazio `app/__init__.py`, então ele é um "pacote Python" (uma coleção de "módulos Python"): `app`. +* Ele contém um arquivo `app/main.py`. Como está dentro de um pacote Python (um diretório com um arquivo `__init__.py`), ele é um "módulo" desse pacote: `app.main`. +* Existe também um arquivo `app/dependencies.py`, assim como `app/main.py`, ele é um "módulo": `app.dependencies`. * Há um subdiretório `app/routers/` com outro arquivo `__init__.py`, então ele é um "subpacote Python": `app.routers`. -* O arquivo `app/routers/items.py` está dentro de um pacote, `app/routers/`, portanto, é um "submódulo": `app.routers.items`. -* O mesmo com `app/routers/users.py`, ele é outro submódulo: `app.routers.users`. -* Há também um subdiretório `app/internal/` com outro arquivo `__init__.py`, então ele é outro "subpacote Python":`app.internal`. +* O arquivo `app/routers/items.py` está dentro de um pacote, `app/routers/`, portanto é um submódulo: `app.routers.items`. +* O mesmo com `app/routers/users.py`, ele é outro submódulo: `app.routers.users`. +* Há também um subdiretório `app/internal/` com outro arquivo `__init__.py`, então ele é outro "subpacote Python": `app.internal`. * E o arquivo `app/internal/admin.py` é outro submódulo: `app.internal.admin`. <img src="/img/tutorial/bigger-applications/package.drawio.svg"> A mesma estrutura de arquivos com comentários: -``` +```bash . -├── app # "app" é um pacote Python -│   ├── __init__.py # este arquivo torna "app" um "pacote Python" -│   ├── main.py # "main" módulo, e.g. import app.main -│   ├── dependencies.py # "dependencies" módulo, e.g. import app.dependencies -│   └── routers # "routers" é um "subpacote Python" -│   │ ├── __init__.py # torna "routers" um "subpacote Python" -│   │ ├── items.py # "items" submódulo, e.g. import app.routers.items -│   │ └── users.py # "users" submódulo, e.g. import app.routers.users -│   └── internal # "internal" é um "subpacote Python" -│   ├── __init__.py # torna "internal" um "subpacote Python" -│   └── admin.py # "admin" submódulo, e.g. import app.internal.admin +├── app # "app" is a Python package +│   ├── __init__.py # this file makes "app" a "Python package" +│   ├── main.py # "main" module, e.g. import app.main +│   ├── dependencies.py # "dependencies" module, e.g. import app.dependencies +│   └── routers # "routers" is a "Python subpackage" +│   │ ├── __init__.py # makes "routers" a "Python subpackage" +│   │ ├── items.py # "items" submodule, e.g. import app.routers.items +│   │ └── users.py # "users" submodule, e.g. import app.routers.users +│   └── internal # "internal" is a "Python subpackage" +│   ├── __init__.py # makes "internal" a "Python subpackage" +│   └── admin.py # "admin" submodule, e.g. import app.internal.admin ``` ## `APIRouter` { #apirouter } @@ -79,11 +79,11 @@ Você quer manter as *operações de rota* relacionadas aos seus usuários separ Mas ele ainda faz parte da mesma aplicação/web API **FastAPI** (faz parte do mesmo "pacote Python"). -Você pode criar as *operações de rotas* para esse módulo usando o `APIRouter`. +Você pode criar as *operações de rota* para esse módulo usando o `APIRouter`. ### Importe `APIRouter` { #import-apirouter } -você o importa e cria uma "instância" da mesma maneira que faria com a classe `FastAPI`: +Você o importa e cria uma "instância" da mesma maneira que faria com a classe `FastAPI`: {* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} @@ -91,7 +91,7 @@ você o importa e cria uma "instância" da mesma maneira que faria com a classe E então você o utiliza para declarar suas *operações de rota*. -Utilize-o da mesma maneira que utilizaria a classe `FastAPI`: +Utilize-o da mesma maneira que utilizaria a classe `FastAPI`: {* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} @@ -151,7 +151,7 @@ Então, em vez de adicionar tudo isso a cada *operação de rota*, podemos adici {* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} -Como o caminho de cada *operação de rota* deve começar com `/`, como em: +Como o path de cada *operação de rota* tem que começar com `/`, como em: ```Python hl_lines="1" @router.get("/{item_id}") @@ -163,9 +163,9 @@ async def read_item(item_id: str): Então, o prefixo neste caso é `/items`. -Também podemos adicionar uma lista de `tags` e `responses` extras que serão aplicadas a todas as *operações de rota* incluídas neste roteador. +Também podemos adicionar uma list de `tags` e `responses` extras que serão aplicadas a todas as *operações de rota* incluídas neste router. -E podemos adicionar uma lista de `dependencies` que serão adicionadas a todas as *operações de rota* no roteador e serão executadas/resolvidas para cada request feita a elas. +E podemos adicionar uma list de `dependencies` que serão adicionadas a todas as *operações de rota* no router e serão executadas/resolvidas para cada request feita a elas. /// tip | Dica @@ -173,7 +173,7 @@ Observe que, assim como [dependências em *decoradores de operação de rota*](d /// -O resultado final é que os caminhos dos itens agora são: +O resultado final é que os paths dos itens agora são: * `/items/` * `/items/{item_id}` @@ -183,9 +183,9 @@ O resultado final é que os caminhos dos itens agora são: * Elas serão marcadas com uma lista de tags que contêm uma única string `"items"`. * Essas "tags" são especialmente úteis para os sistemas de documentação interativa automática (usando OpenAPI). * Todas elas incluirão as `responses` predefinidas. -* Todas essas *operações de rota* terão a lista de `dependencies` avaliada/executada antes delas. +* Todas essas *operações de rota* terão a list de `dependencies` avaliada/executada antes delas. * Se você também declarar dependências em uma *operação de rota* específica, **elas também serão executadas**. - * As dependências do roteador são executadas primeiro, depois as [`dependencies` no decorador](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank} e, em seguida, as dependências de parâmetros normais. + * As dependências do router são executadas primeiro, depois as [`dependencies` no decorador](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank} e, em seguida, as dependências de parâmetros normais. * Você também pode adicionar [dependências de `Segurança` com `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}. /// tip | Dica @@ -246,7 +246,7 @@ from ..dependencies import get_token_header significa: -* Começando no mesmo pacote em que este módulo (o arquivo `app/routers/items.py`) reside (o diretório `app/routers/`)... +* Começando no mesmo pacote em que este módulo (o arquivo `app/routers/items.py`) vive (o diretório `app/routers/`)... * vá para o pacote pai (o diretório `app/`)... * e lá, encontre o módulo `dependencies` (o arquivo em `app/dependencies.py`)... * e dele, importe a função `get_token_header`. @@ -283,9 +283,9 @@ Mas ainda podemos adicionar _mais_ `tags` que serão aplicadas a uma *operação /// tip | Dica -Esta última operação de caminho terá a combinação de tags: `["items", "custom"]`. +Esta última operação de rota terá a combinação de tags: `["items", "custom"]`. -E também terá ambas as respostas na documentação, uma para `404` e uma para `403`. +E também terá ambas as responses na documentação, uma para `404` e uma para `403`. /// @@ -325,7 +325,7 @@ from .routers import items, users significa: -* Começando no mesmo pacote em que este módulo (o arquivo `app/main.py`) reside (o diretório `app/`)... +* Começando no mesmo pacote em que este módulo (o arquivo `app/main.py`) vive (o diretório `app/`)... * procure o subpacote `routers` (o diretório em `app/routers/`)... * e dele, importe o submódulo `items` (o arquivo em `app/routers/items.py`) e `users` (o arquivo em `app/routers/users.py`)... @@ -376,7 +376,7 @@ Então, para poder usar ambos no mesmo arquivo, importamos os submódulos direta {* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *} -### Inclua os `APIRouter`s para `usuários` e `itens` { #include-the-apirouters-for-users-and-items } +### Inclua os `APIRouter`s para `users` e `items` { #include-the-apirouters-for-users-and-items } Agora, vamos incluir os `router`s dos submódulos `users` e `items`: @@ -392,7 +392,7 @@ E `items.router` contém o `APIRouter` dentro do arquivo `app/routers/items.py`. Com `app.include_router()` podemos adicionar cada `APIRouter` ao aplicativo principal `FastAPI`. -Ele incluirá todas as rotas daquele roteador como parte dele. +Ele incluirá todas as rotas daquele router como parte dele. /// note | Detalhes Técnicos @@ -404,7 +404,7 @@ Então, nos bastidores, ele realmente funcionará como se tudo fosse o mesmo apl /// check | Verifique -Você não precisa se preocupar com desempenho ao incluir roteadores. +Você não precisa se preocupar com desempenho ao incluir routers. Isso levará microssegundos e só acontecerá na inicialização. @@ -453,7 +453,7 @@ e funcionará corretamente, junto com todas as outras *operações de rota* adic /// note | Detalhes Técnicos Avançados -**Observação**: este é um detalhe muito técnico que você provavelmente pode **simplesmente pular**. +**Nota**: este é um detalhe muito técnico que você provavelmente pode **simplesmente pular**. --- @@ -479,15 +479,15 @@ $ fastapi dev app/main.py </div> -E abra os documentos em <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. +E abra a documentação em <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. -Você verá a documentação automática da API, incluindo os caminhos de todos os submódulos, usando os caminhos (e prefixos) corretos e as tags corretas: +Você verá a documentação automática da API, incluindo os paths de todos os submódulos, usando os paths (e prefixos) corretos e as tags corretas: <img src="/img/tutorial/bigger-applications/image01.png"> -## Inclua o mesmo roteador várias vezes com `prefix` diferentes { #include-the-same-router-multiple-times-with-different-prefix } +## Inclua o mesmo router várias vezes com `prefix` diferentes { #include-the-same-router-multiple-times-with-different-prefix } -Você também pode usar `.include_router()` várias vezes com o *mesmo* roteador usando prefixos diferentes. +Você também pode usar `.include_router()` várias vezes com o *mesmo* router usando prefixos diferentes. Isso pode ser útil, por exemplo, para expor a mesma API sob prefixos diferentes, por exemplo, `/api/v1` e `/api/latest`. @@ -495,10 +495,10 @@ Esse é um uso avançado que você pode não precisar, mas está lá caso precis ## Inclua um `APIRouter` em outro { #include-an-apirouter-in-another } -Da mesma forma que você pode incluir um `APIRouter` em um aplicativo `FastAPI`, você pode incluir um `APIRouter` em outro `APIRouter` usando: +Da mesma forma que você pode incluir um `APIRouter` em uma aplicação `FastAPI`, você pode incluir um `APIRouter` em outro `APIRouter` usando: ```Python router.include_router(other_router) ``` -Certifique-se de fazer isso antes de incluir `router` no aplicativo `FastAPI`, para que as *operações de rota* de `other_router` também sejam incluídas. +Certifique-se de fazer isso antes de incluir `router` na aplicação `FastAPI`, para que as *operações de rota* de `other_router` também sejam incluídas. diff --git a/docs/pt/docs/tutorial/body-updates.md b/docs/pt/docs/tutorial/body-updates.md index 67bf68492..95f89c8d2 100644 --- a/docs/pt/docs/tutorial/body-updates.md +++ b/docs/pt/docs/tutorial/body-updates.md @@ -1,6 +1,6 @@ # Corpo - Atualizações { #body-updates } -## Atualização de dados existentes com `PUT` { #update-replacing-with-put } +## Atualização substituindo com `PUT` { #update-replacing-with-put } Para atualizar um item, você pode usar a operação <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT" class="external-link" target="_blank">HTTP `PUT`</a>. @@ -22,13 +22,13 @@ Isso significa que, se você quiser atualizar o item `bar` usando `PUT` com um c } ``` -Como ele não inclui o atributo já armazenado `"tax": 20.2`, o modelo de entrada assumiria o valor padrão de `"tax": 10.5`. +como ele não inclui o atributo já armazenado `"tax": 20.2`, o modelo de entrada assumiria o valor padrão de `"tax": 10.5`. E os dados seriam salvos com esse "novo" `tax` de `10.5`. ## Atualizações parciais com `PATCH` { #partial-updates-with-patch } -Você também pode usar a operação <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH" class="external-link" target="_blank">HTTP `PATCH`</a> para atualizar parcialmente os dados. +Você também pode usar a operação <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH" class="external-link" target="_blank">HTTP `PATCH`</a> para atualizar dados *parcialmente*. Isso significa que você pode enviar apenas os dados que deseja atualizar, deixando o restante intacto. @@ -40,25 +40,17 @@ E muitas equipes usam apenas `PUT`, mesmo para atualizações parciais. Você é **livre** para usá-los como preferir, **FastAPI** não impõe restrições. -Mas este guia te dá uma ideia de como eles são destinados a serem usados. +Mas este guia mostra, mais ou menos, como eles são destinados a serem usados. /// ### Usando o parâmetro `exclude_unset` do Pydantic { #using-pydantics-exclude-unset-parameter } -Se você quiser receber atualizações parciais, é muito útil usar o parâmetro `exclude_unset` no método `.model_dump()` do modelo do Pydantic. +Se você quiser receber atualizações parciais, é muito útil usar o parâmetro `exclude_unset` no `.model_dump()` do modelo do Pydantic. Como `item.model_dump(exclude_unset=True)`. -/// info | Informação - -No Pydantic v1, o método que era chamado `.dict()` e foi descontinuado (mas ainda suportado) no Pydantic v2. Agora, deve-se usar o método `.model_dump()`. - -Os exemplos aqui usam `.dict()` para compatibilidade com o Pydantic v1, mas você deve usar `.model_dump()` a partir do Pydantic v2. - -/// - -Isso gera um `dict` com apenas os dados definidos ao criar o modelo `item`, excluindo os valores padrão. +Isso geraria um `dict` com apenas os dados que foram definidos ao criar o modelo `item`, excluindo os valores padrão. Então, você pode usar isso para gerar um `dict` com apenas os dados definidos (enviados na solicitação), omitindo valores padrão: @@ -68,31 +60,23 @@ Então, você pode usar isso para gerar um `dict` com apenas os dados definidos Agora, você pode criar uma cópia do modelo existente usando `.model_copy()`, e passar o parâmetro `update` com um `dict` contendo os dados para atualizar. -/// info | Informação - -No Pydantic v1, o método era chamado `.copy()`, ele foi descontinuado (mas ainda suportado) no Pydantic v2, e renomeado para `.model_copy()`. - -Os exemplos aqui usam `.copy()` para compatibilidade com o Pydantic v1, mas você deve usar `.model_copy()` com o Pydantic v2. - -/// - Como `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} ### Recapitulando as atualizações parciais { #partial-updates-recap } -Resumindo, para aplicar atualizações parciais você pode: +Resumindo, para aplicar atualizações parciais você deveria: * (Opcionalmente) usar `PATCH` em vez de `PUT`. * Recuperar os dados armazenados. * Colocar esses dados em um modelo do Pydantic. * Gerar um `dict` sem valores padrão a partir do modelo de entrada (usando `exclude_unset`). - * Dessa forma, você pode atualizar apenas os valores definidos pelo usuário, em vez de substituir os valores já armazenados com valores padrão em seu modelo. + * Dessa forma, você pode atualizar apenas os valores realmente definidos pelo usuário, em vez de substituir valores já armazenados por valores padrão do modelo. * Criar uma cópia do modelo armazenado, atualizando seus atributos com as atualizações parciais recebidas (usando o parâmetro `update`). -* Converter o modelo copiado em algo que possa ser armazenado no seu banco de dados (por exemplo, usando o `jsonable_encoder`). - * Isso é comparável ao uso do método `.model_dump()`, mas garante (e converte) os valores para tipos de dados que possam ser convertidos em JSON, por exemplo, `datetime` para `str`. -* Salvar os dados no seu banco de dados. +* Converter o modelo copiado em algo que possa ser armazenado no seu BD (por exemplo, usando o `jsonable_encoder`). + * Isso é comparável a usar o método `.model_dump()` do modelo novamente, mas garante (e converte) os valores para tipos de dados que possam ser convertidos em JSON, por exemplo, `datetime` para `str`. +* Salvar os dados no seu BD. * Retornar o modelo atualizado. {* ../../docs_src/body_updates/tutorial002_py310.py hl[28:35] *} @@ -109,8 +93,8 @@ Mas o exemplo aqui usa `PATCH` porque foi criado para esses casos de uso. Observe que o modelo de entrada ainda é validado. -Portanto, se você quiser receber atualizações parciais que possam omitir todos os atributos, precisará ter um modelo com todos os atributos marcados como opcionais (com valores padrão ou `None`). +Portanto, se você quiser receber atualizações parciais que possam omitir todos os atributos, você precisa ter um modelo com todos os atributos marcados como opcionais (com valores padrão ou `None`). -Para distinguir os modelos com todos os valores opcionais para **atualizações** e modelos com valores obrigatórios para **criação**, você pode usar as ideias descritas em [Modelos Adicionais](extra-models.md){.internal-link target=_blank}. +Para distinguir entre os modelos com todos os valores opcionais para **atualizações** e modelos com valores obrigatórios para **criação**, você pode usar as ideias descritas em [Modelos Adicionais](extra-models.md){.internal-link target=_blank}. /// diff --git a/docs/pt/docs/tutorial/body.md b/docs/pt/docs/tutorial/body.md index 1330f4458..669334439 100644 --- a/docs/pt/docs/tutorial/body.md +++ b/docs/pt/docs/tutorial/body.md @@ -10,11 +10,11 @@ Para declarar um corpo da **requisição**, você utiliza os modelos do <a href= /// info | Informação -Para enviar dados, você deve usar um dos: `POST` (o mais comum), `PUT`, `DELETE` ou `PATCH`. +Para enviar dados, você deveria usar um dos: `POST` (o mais comum), `PUT`, `DELETE` ou `PATCH`. Enviar um corpo em uma requisição `GET` não tem um comportamento definido nas especificações, porém é suportado pelo FastAPI, apenas para casos de uso bem complexos/extremos. -Como é desencorajado, a documentação interativa com Swagger UI não irá mostrar a documentação para o corpo da requisição para um `GET`, e proxies que intermediarem podem não suportar o corpo da requisição. +Como é desencorajado, a documentação interativa com Swagger UI não irá mostrar a documentação para o corpo da requisição ao usar `GET`, e proxies intermediários podem não suportá-lo. /// @@ -32,7 +32,8 @@ Utilize os tipos Python padrão para todos os atributos: {* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} -Assim como quando declaramos parâmetros de consulta, quando um atributo do modelo possui um valor padrão, ele se torna opcional. Caso contrário, se torna obrigatório. Use `None` para torná-lo opcional. + +Assim como quando declaramos parâmetros de consulta, quando um atributo do modelo possui um valor padrão, ele não é obrigatório. Caso contrário, é obrigatório. Use `None` para torná-lo apenas opcional. Por exemplo, o modelo acima declara um JSON "`object`" (ou `dict` no Python) como esse: @@ -66,7 +67,7 @@ Para adicioná-lo à sua *operação de rota*, declare-o da mesma maneira que vo Apenas com essa declaração de tipos do Python, o **FastAPI** irá: -* Ler o corpo da requisição como um JSON. +* Ler o corpo da requisição como JSON. * Converter os tipos correspondentes (se necessário). * Validar os dados. * Se algum dado for inválido, irá retornar um erro bem claro, indicando exatamente onde e o que estava incorreto. @@ -127,14 +128,6 @@ Dentro da função, você pode acessar todos os atributos do objeto do modelo di {* ../../docs_src/body/tutorial002_py310.py *} -/// info | Informação - -No Pydantic v1 o método se chamava `.dict()`, ele foi descontinuado (mas ainda é suportado) no Pydantic v2, e renomeado para `.model_dump()`. - -Os exemplos aqui usam `.dict()` para compatibilidade com o Pydantic v1, mas você deve usar `.model_dump()` se puder usar o Pydantic v2. - -/// - ## Corpo da requisição + parâmetros de rota { #request-body-path-parameters } Você pode declarar parâmetros de rota e corpo da requisição ao mesmo tempo. @@ -143,6 +136,7 @@ O **FastAPI** irá reconhecer que os parâmetros da função que combinam com pa {* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} + ## Corpo da requisição + parâmetros de rota + parâmetros de consulta { #request-body-path-query-parameters } Você também pode declarar parâmetros de **corpo**, **rota** e **consulta**, ao mesmo tempo. diff --git a/docs/pt/docs/tutorial/extra-models.md b/docs/pt/docs/tutorial/extra-models.md index c0d22df57..24eafce01 100644 --- a/docs/pt/docs/tutorial/extra-models.md +++ b/docs/pt/docs/tutorial/extra-models.md @@ -22,21 +22,13 @@ Aqui está uma ideia geral de como os modelos poderiam parecer com seus campos d {* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} -/// info | Informação +### Sobre `**user_in.model_dump()` { #about-user-in-model-dump } -No Pydantic v1 o método se chamava `.dict()`, ele foi descontinuado (mas ainda é suportado) no Pydantic v2 e renomeado para `.model_dump()`. - -Os exemplos aqui usam `.dict()` por compatibilidade com o Pydantic v1, mas você deve usar `.model_dump()` se puder usar o Pydantic v2. - -/// - -### Sobre `**user_in.dict()` { #about-user-in-dict } - -#### O `.dict()` do Pydantic { #pydantics-dict } +#### O `.model_dump()` do Pydantic { #pydantics-model-dump } `user_in` é um modelo Pydantic da classe `UserIn`. -Os modelos Pydantic possuem um método `.dict()` que retorna um `dict` com os dados do modelo. +Os modelos Pydantic possuem um método `.model_dump()` que retorna um `dict` com os dados do modelo. Então, se criarmos um objeto Pydantic `user_in` como: @@ -47,7 +39,7 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com e depois chamarmos: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() ``` agora temos um `dict` com os dados na variável `user_dict` (é um `dict` em vez de um objeto de modelo Pydantic). @@ -103,20 +95,20 @@ UserInDB( #### Um modelo Pydantic a partir do conteúdo de outro { #a-pydantic-model-from-the-contents-of-another } -Como no exemplo acima, obtivemos o `user_dict` a partir do `user_in.dict()`, este código: +Como no exemplo acima, obtivemos o `user_dict` a partir do `user_in.model_dump()`, este código: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() UserInDB(**user_dict) ``` seria equivalente a: ```Python -UserInDB(**user_in.dict()) +UserInDB(**user_in.model_dump()) ``` -...porque `user_in.dict()` é um `dict`, e depois fazemos o Python "desembrulhá-lo" passando-o para `UserInDB` precedido por `**`. +...porque `user_in.model_dump()` é um `dict`, e depois fazemos o Python "desembrulhá-lo" passando-o para `UserInDB` precedido por `**`. Então, obtemos um modelo Pydantic a partir dos dados em outro modelo Pydantic. @@ -125,7 +117,7 @@ Então, obtemos um modelo Pydantic a partir dos dados em outro modelo Pydantic. E, então, adicionando o argumento de palavra-chave extra `hashed_password=hashed_password`, como em: ```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) ``` ...acaba sendo como: diff --git a/docs/pt/docs/tutorial/query-params-str-validations.md b/docs/pt/docs/tutorial/query-params-str-validations.md index 5ec1b1b55..c93a941e5 100644 --- a/docs/pt/docs/tutorial/query-params-str-validations.md +++ b/docs/pt/docs/tutorial/query-params-str-validations.md @@ -33,7 +33,7 @@ Para isso, primeiro importe: O FastAPI adicionou suporte a `Annotated` (e passou a recomendá-lo) na versão 0.95.0. -Se você tiver uma versão mais antiga, terá erros ao tentar usar `Annotated`. +Se você tiver uma versão mais antiga, teria erros ao tentar usar `Annotated`. Certifique-se de [Atualizar a versão do FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} para pelo menos 0.95.1 antes de usar `Annotated`. @@ -109,7 +109,7 @@ Agora o FastAPI vai: ## Alternativa (antiga): `Query` como valor padrão { #alternative-old-query-as-the-default-value } -Versões anteriores do FastAPI (antes de <abbr title="antes de 2023-03">0.95.0</abbr>) exigiam que você usasse `Query` como valor padrão do seu parâmetro, em vez de colocá-lo em `Annotated`. É muito provável que você veja código assim por aí, então vou te explicar. +Versões anteriores do FastAPI (antes de <abbr title="before 2023-03 - antes de 2023-03">0.95.0</abbr>) exigiam que você usasse `Query` como valor padrão do seu parâmetro, em vez de colocá-lo em `Annotated`, há uma grande chance de você ver código usando isso por aí, então vou explicar. /// tip | Dica @@ -192,7 +192,7 @@ Você também pode adicionar um parâmetro `min_length`: ## Adicione expressões regulares { #add-regular-expressions } -Você pode definir um `pattern` de <abbr title="Uma expressão regular, regex ou regexp é uma sequência de caracteres que define um padrão de busca para strings.">expressão regular</abbr> que o parâmetro deve corresponder: +Você pode definir um `pattern` de <abbr title="A regular expression, regex or regexp is a sequence of characters that define a search pattern for strings. - Uma expressão regular, regex ou regexp é uma sequência de caracteres que define um padrão de busca para strings.">expressão regular</abbr> que o parâmetro deve corresponder: {* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} @@ -206,20 +206,6 @@ Se você se sentir perdido com essas ideias de **"expressão regular"**, não se Agora você sabe que, sempre que precisar delas, pode usá-las no **FastAPI**. -### Pydantic v1 `regex` em vez de `pattern` { #pydantic-v1-regex-instead-of-pattern } - -Antes da versão 2 do Pydantic e antes do FastAPI 0.100.0, o parâmetro se chamava `regex` em vez de `pattern`, mas agora está descontinuado. - -Você ainda pode ver algum código usando isso: - -//// tab | Pydantic v1 - -{* ../../docs_src/query_params_str_validations/tutorial004_regex_an_py310.py hl[11] *} - -//// - -Mas saiba que isso está descontinuado e deve ser atualizado para usar o novo parâmetro `pattern`. 🤓 - ## Valores padrão { #default-values } Você pode, claro, usar valores padrão diferentes de `None`. @@ -280,7 +266,7 @@ Então, com uma URL como: http://localhost:8000/items/?q=foo&q=bar ``` -você receberá os múltiplos valores do *parâmetro de consulta* `q` (`foo` e `bar`) em uma `list` Python dentro da sua *função de operação de rota*, no *parâmetro da função* `q`. +você receberia os múltiplos valores dos *parâmetros de consulta* `q` (`foo` e `bar`) em uma `list` Python dentro da sua *função de operação de rota*, no *parâmetro da função* `q`. Assim, a resposta para essa URL seria: @@ -350,7 +336,7 @@ Essas informações serão incluídas no OpenAPI gerado e usadas pelas interface Tenha em mente que ferramentas diferentes podem ter níveis diferentes de suporte ao OpenAPI. -Algumas delas podem ainda não mostrar todas as informações extras declaradas, embora na maioria dos casos o recurso ausente já esteja planejado para desenvolvimento. +Algumas delas podem ainda não mostrar todas as informações extras declaradas, embora na maioria dos casos a funcionalidade ausente já esteja planejada para desenvolvimento. /// @@ -386,7 +372,7 @@ Então você pode declarar um `alias`, e esse alias será usado para encontrar o Agora digamos que você não gosta mais desse parâmetro. -Você tem que deixá-lo por um tempo, pois há clientes usando-o, mas quer que a documentação mostre claramente que ele está <abbr title="obsoleto, recomenda-se não usá-lo">descontinuado</abbr>. +Você tem que deixá-lo por um tempo, pois há clientes usando-o, mas quer que a documentação mostre claramente que ele está <abbr title="obsolete, recommended not to use it - obsoleto, recomenda-se não usá-lo">deprecated</abbr>. Então passe o parâmetro `deprecated=True` para `Query`: @@ -416,7 +402,7 @@ O Pydantic também tem <a href="https://docs.pydantic.dev/latest/concepts/valida /// -Por exemplo, este validador personalizado verifica se o ID do item começa com `isbn-` para um número de livro <abbr title="ISBN significa Número Padrão Internacional de Livro">ISBN</abbr> ou com `imdb-` para um ID de URL de filme <abbr title="IMDB (Internet Movie Database) é um site com informações sobre filmes">IMDB</abbr>: +Por exemplo, este validador personalizado verifica se o ID do item começa com `isbn-` para um número de livro <abbr title="ISBN means International Standard Book Number - ISBN significa Número Padrão Internacional de Livro">ISBN</abbr> ou com `imdb-` para um ID de URL de filme <abbr title="IMDB (Internet Movie Database) is a website with information about movies - IMDB (Internet Movie Database) é um site com informações sobre filmes">IMDB</abbr>: {* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} @@ -428,7 +414,7 @@ Isso está disponível com a versão 2 do Pydantic ou superior. 😎 /// tip | Dica -Se você precisar fazer qualquer tipo de validação que exija comunicação com algum **componente externo**, como um banco de dados ou outra API, você deve usar **Dependências do FastAPI** em vez disso; você aprenderá sobre elas mais adiante. +Se você precisar fazer qualquer tipo de validação que exija comunicação com algum **componente externo**, como um banco de dados ou outra API, você deveria usar **Dependências do FastAPI** em vez disso; você aprenderá sobre elas mais adiante. Esses validadores personalizados são para coisas que podem ser verificadas **apenas** com os **mesmos dados** fornecidos na requisição. @@ -440,7 +426,7 @@ O ponto importante é apenas usar **`AfterValidator` com uma função dentro de --- -Mas se você está curioso sobre este exemplo específico e ainda entretido, aqui vão alguns detalhes extras. +Mas se você estiver curioso sobre este exemplo de código específico e ainda entretido, aqui vão alguns detalhes extras. #### String com `value.startswith()` { #string-with-value-startswith } @@ -450,7 +436,7 @@ Percebeu? Uma string usando `value.startswith()` pode receber uma tupla, e verif #### Um item aleatório { #a-random-item } -Com `data.items()` obtemos um <abbr title="Algo que podemos iterar com um laço for, como uma list, set, etc.">objeto iterável</abbr> com tuplas contendo a chave e o valor de cada item do dicionário. +Com `data.items()` obtemos um <abbr title="Something we can iterate on with a for loop, like a list, set, etc. - Algo que podemos iterar com um laço for, como uma list, set, etc.">objeto iterável</abbr> com tuplas contendo a chave e o valor de cada item do dicionário. Convertimos esse objeto iterável em uma `list` adequada com `list(data.items())`. diff --git a/docs/pt/docs/tutorial/response-model.md b/docs/pt/docs/tutorial/response-model.md index dc66bb46c..8a7a71248 100644 --- a/docs/pt/docs/tutorial/response-model.md +++ b/docs/pt/docs/tutorial/response-model.md @@ -252,20 +252,6 @@ Então, se você enviar uma solicitação para essa *operação de rota* para o /// info | Informação -No Pydantic v1, o método era chamado `.dict()`, ele foi descontinuado (mas ainda suportado) no Pydantic v2 e renomeado para `.model_dump()`. - -Os exemplos aqui usam `.dict()` para compatibilidade com Pydantic v1, mas você deve usar `.model_dump()` em vez disso se puder usar Pydantic v2. - -/// - -/// info | Informação - -O FastAPI usa `.dict()` do modelo Pydantic com <a href="https://docs.pydantic.dev/1.10/usage/exporting_models/#modeldict" class="external-link" target="_blank">seu parâmetro `exclude_unset`</a> para chegar a isso. - -/// - -/// info | Informação - Você também pode usar: * `response_model_exclude_defaults=True` diff --git a/docs/pt/docs/tutorial/schema-extra-example.md b/docs/pt/docs/tutorial/schema-extra-example.md index bddd320cd..2d62ffd85 100644 --- a/docs/pt/docs/tutorial/schema-extra-example.md +++ b/docs/pt/docs/tutorial/schema-extra-example.md @@ -8,39 +8,17 @@ Aqui estão várias maneiras de fazer isso. Você pode declarar `examples` para um modelo Pydantic que serão adicionados ao JSON Schema gerado. -//// tab | Pydantic v2 - {* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *} - -//// - Essas informações extras serão adicionadas como estão ao **JSON Schema** de saída para esse modelo e serão usadas na documentação da API. -//// tab | Pydantic v2 - -Na versão 2 do Pydantic, você usaria o atributo `model_config`, que recebe um `dict`, conforme descrito na <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">documentação do Pydantic: Configuration</a>. +Você pode usar o atributo `model_config`, que recebe um `dict`, conforme descrito na <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">documentação do Pydantic: Configuration</a>. Você pode definir `"json_schema_extra"` com um `dict` contendo quaisquer dados adicionais que você queira que apareçam no JSON Schema gerado, incluindo `examples`. -//// - -//// tab | Pydantic v1 - -Na versão 1 do Pydantic, você usaria uma classe interna `Config` e `schema_extra`, conforme descrito na <a href="https://docs.pydantic.dev/1.10/usage/schema/#schema-customization" class="external-link" target="_blank">documentação do Pydantic: Schema customization</a>. - -Você pode definir `schema_extra` com um `dict` contendo quaisquer dados adicionais que você queira que apareçam no JSON Schema gerado, incluindo `examples`. - -//// - /// tip | Dica -Você pode usar a mesma técnica para estender o JSON Schema e adicionar suas próprias informações extras personalizadas. +Você poderia usar a mesma técnica para estender o JSON Schema e adicionar suas próprias informações extras personalizadas. Por exemplo, você poderia usá-la para adicionar metadados para uma interface de usuário de front-end, etc. @@ -50,7 +28,7 @@ Por exemplo, você poderia usá-la para adicionar metadados para uma interface d O OpenAPI 3.1.0 (usado desde o FastAPI 0.99.0) adicionou suporte a `examples`, que faz parte do padrão **JSON Schema**. -Antes disso, ele suportava apenas a palavra‑chave `example` com um único exemplo. Isso ainda é suportado pelo OpenAPI 3.1.0, mas é descontinuado e não faz parte do padrão JSON Schema. Portanto, é recomendado migrar de `example` para `examples`. 🤓 +Antes disso, ele suportava apenas a palavra‑chave `example` com um único exemplo. Isso ainda é suportado pelo OpenAPI 3.1.0, mas é descontinuado e não faz parte do padrão JSON Schema. Portanto, você é incentivado a migrar de `example` para `examples`. 🤓 Você pode ler mais no final desta página. @@ -102,7 +80,7 @@ No entanto, <abbr title="2023-08-26">no momento em que isto foi escrito</abbr>, Antes do **JSON Schema** suportar `examples`, o OpenAPI já tinha suporte para um campo diferente também chamado `examples`. -Esse `examples` específico do OpenAPI vai em outra seção da especificação. Ele fica nos **detalhes de cada função de operação de rota**, não dentro de cada JSON Schema. +Esse `examples` **específico do OpenAPI** vai em outra seção da especificação OpenAPI. Ele fica nos **detalhes de cada *operação de rota***, não dentro de cada JSON Schema. E o Swagger UI tem suportado esse campo `examples` particular há algum tempo. Então, você pode usá-lo para **mostrar** diferentes **exemplos na UI da documentação**. @@ -189,9 +167,9 @@ Depois, o JSON Schema adicionou um campo <a href="https://json-schema.org/draft/ E então o novo OpenAPI 3.1.0 passou a se basear na versão mais recente (JSON Schema 2020-12), que incluiu esse novo campo `examples`. -Agora, esse novo campo `examples` tem precedência sobre o antigo (e customizado) campo único `example`, que agora está descontinuado. +E agora esse novo campo `examples` tem precedência sobre o antigo campo único (e customizado) `example`, que agora está descontinuado. -Esse novo campo `examples` no JSON Schema é **apenas uma `list`** de exemplos, não um `dict` com metadados extras como nos outros lugares do OpenAPI (descritos acima). +Esse novo campo `examples` no JSON Schema é **apenas uma `list`** de exemplos, não um dict com metadados extras como nos outros lugares do OpenAPI (descritos acima). /// info | Informação @@ -213,7 +191,7 @@ Mas agora que o FastAPI 0.99.0 e superiores usam o OpenAPI 3.1.0, que usa o JSON ### Swagger UI e `examples` específicos do OpenAPI { #swagger-ui-and-openapi-specific-examples } -Como o Swagger UI não suportava vários exemplos no JSON Schema (em 2023-08-26), os usuários não tinham uma forma de mostrar vários exemplos na documentação. +Agora, como o Swagger UI não suportava vários exemplos no JSON Schema (em 2023-08-26), os usuários não tinham uma forma de mostrar vários exemplos na documentação. Para resolver isso, o FastAPI `0.103.0` **adicionou suporte** para declarar o mesmo antigo campo **específico do OpenAPI** `examples` com o novo parâmetro `openapi_examples`. 🤓 From 50a78bf84091df3f1620d986e36ab4ee3aa1a2d2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:40:39 +0000 Subject: [PATCH 098/243] =?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 7c5460f1d..33926e3af 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Translations +* 🌐 Update translations for pt (update-outdated). PR [#14724](https://github.com/fastapi/fastapi/pull/14724) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update Korean LLM prompt. PR [#14740](https://github.com/fastapi/fastapi/pull/14740) by [@hard-coders](https://github.com/hard-coders). * 🌐 Improve LLM prompt for Turkish translations. PR [#14728](https://github.com/fastapi/fastapi/pull/14728) by [@Kadermiyanyedi](https://github.com/Kadermiyanyedi). * 🌐 Update portuguese llm-prompt.md. PR [#14702](https://github.com/fastapi/fastapi/pull/14702) by [@ceb10n](https://github.com/ceb10n). From 2eb978b87a2227801cc9abaeddd5e27d941af868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 20 Jan 2026 15:03:07 -0800 Subject: [PATCH 099/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20ru=20(update-outdated)=20(#14693)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🌐 Update translations for ru (update-outdated) * 🎨 Auto format * Apply suggestions from code review * Apply suggestions from code review 2 * Apply suggestions from code review 3 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- docs/ru/docs/_llm-test.md | 18 +- .../path-operation-advanced-configuration.md | 56 +--- docs/ru/docs/advanced/settings.md | 44 --- ...migrate-from-pydantic-v1-to-pydantic-v2.md | 28 +- .../docs/how-to/separate-openapi-schemas.md | 16 +- docs/ru/docs/index.md | 40 +-- docs/ru/docs/tutorial/bigger-applications.md | 289 +++++++++--------- docs/ru/docs/tutorial/body-updates.md | 34 +-- docs/ru/docs/tutorial/body.md | 22 +- docs/ru/docs/tutorial/extra-models.md | 62 ++-- .../tutorial/query-params-str-validations.md | 30 +- docs/ru/docs/tutorial/response-model.md | 72 ++--- docs/ru/docs/tutorial/schema-extra-example.md | 30 +- 13 files changed, 297 insertions(+), 444 deletions(-) diff --git a/docs/ru/docs/_llm-test.md b/docs/ru/docs/_llm-test.md index 9a15f6bb2..6a0272f3a 100644 --- a/docs/ru/docs/_llm-test.md +++ b/docs/ru/docs/_llm-test.md @@ -1,8 +1,8 @@ # Тестовый файл LLM { #llm-test-file } -Этот документ проверяет, понимает ли <abbr title="Large Language Model – Большая языковая модель">LLM</abbr>, переводящая документацию, `general_prompt` в `scripts/translate.py` и языковой специфичный промпт в `docs/{language code}/llm-prompt.md`. Языковой специфичный промпт добавляется к `general_prompt`. +Этот документ проверяет, понимает ли <abbr title="Large Language Model - Большая языковая модель">LLM</abbr>, переводящая документацию, `general_prompt` в `scripts/translate.py` и языковой специфичный промпт в `docs/{language code}/llm-prompt.md`. Языковой специфичный промпт добавляется к `general_prompt`. -Тесты, добавленные здесь, увидят все создатели языковых промптов. +Тесты, добавленные здесь, увидят все создатели языковых специфичных промптов. Использование: @@ -11,7 +11,7 @@ * Проверьте, всё ли в порядке в переводе. * При необходимости улучшите ваш языковой специфичный промпт, общий промпт или английский документ. * Затем вручную исправьте оставшиеся проблемы в переводе, чтобы он был хорошим. -* Переведите заново, имея хороший перевод на месте. Идеальным результатом будет ситуация, когда LLM больше не вносит изменений в перевод. Это означает, что общий промпт и ваш языковой специфичный промпт максимально хороши (иногда он будет делать несколько, казалось бы, случайных изменений, причина в том, что <a href="https://doublespeak.chat/#/handbook#deterministic-output" class="external-link" target="_blank">LLM — недетерминированные алгоритмы</a>). +* Переведите заново, имея хороший перевод на месте. Идеальным результатом будет ситуация, когда LLM больше не вносит изменений в перевод. Это означает, что общий промпт и ваш языковой специфичный промпт настолько хороши, насколько это возможно (иногда он будет делать несколько, казалось бы, случайных изменений, причина в том, что <a href="https://doublespeak.chat/#/handbook#deterministic-output" class="external-link" target="_blank">LLM — недетерминированные алгоритмы</a>). Тесты: @@ -197,10 +197,10 @@ works(foo="bar") # Это работает 🎉 ### abbr даёт полную расшифровку { #the-abbr-gives-a-full-phrase } -* <abbr title="Getting Things Done – Как привести дела в порядок">GTD</abbr> -* <abbr title="less than – меньше чем"><code>lt</code></abbr> -* <abbr title="XML Web Token – XML веб‑токен">XWT</abbr> -* <abbr title="Parallel Server Gateway Interface – Параллельный серверный интерфейс шлюза">PSGI</abbr> +* <abbr title="Getting Things Done - Как привести дела в порядок">GTD</abbr> +* <abbr title="less than - меньше чем"><code>lt</code></abbr> +* <abbr title="XML Web Token - XML веб‑токен">XWT</abbr> +* <abbr title="Parallel Server Gateway Interface - Параллельный серверный интерфейс шлюза">PSGI</abbr> ### abbr даёт объяснение { #the-abbr-gives-an-explanation } @@ -209,8 +209,8 @@ works(foo="bar") # Это работает 🎉 ### abbr даёт полную расшифровку и объяснение { #the-abbr-gives-a-full-phrase-and-an-explanation } -* <abbr title="Mozilla Developer Network – Сеть разработчиков Mozilla: документация для разработчиков, созданная командой Firefox">MDN</abbr> -* <abbr title="Input/Output – Ввод/Вывод: чтение или запись на диск, сетевое взаимодействие.">I/O</abbr>. +* <abbr title="Mozilla Developer Network - Сеть разработчиков Mozilla: документация для разработчиков, созданная командой Firefox">MDN</abbr> +* <abbr title="Input/Output - Ввод/Вывод: чтение или запись на диск, сетевое взаимодействие.">I/O</abbr>. //// diff --git a/docs/ru/docs/advanced/path-operation-advanced-configuration.md b/docs/ru/docs/advanced/path-operation-advanced-configuration.md index eaf9ad052..86d3a5b63 100644 --- a/docs/ru/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/ru/docs/advanced/path-operation-advanced-configuration.md @@ -14,7 +14,7 @@ {* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} -### Использование имени функции-обработчика пути как operationId { #using-the-path-operation-function-name-as-the-operationid } +### Использование имени *функции-обработчика пути* как operationId { #using-the-path-operation-function-name-as-the-operationid } Если вы хотите использовать имена функций ваших API в качестве `operationId`, вы можете пройти по всем из них и переопределить `operation_id` каждой *операции пути* с помощью их `APIRoute.name`. @@ -38,7 +38,7 @@ ## Исключить из OpenAPI { #exclude-from-openapi } -Чтобы исключить *операцию пути* из генерируемой схемы OpenAPI (а значит, и из автоматической документации), используйте параметр `include_in_schema` и установите его в `False`: +Чтобы исключить *операцию пути* из генерируемой схемы OpenAPI (а значит, и из автоматических систем документации), используйте параметр `include_in_schema` и установите его в `False`: {* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *} @@ -48,7 +48,7 @@ Добавление `\f` (экранированного символа «form feed») заставит **FastAPI** обрезать текст, используемый для OpenAPI, в этой точке. -Эта часть не попадёт в документацию, но другие инструменты (например, Sphinx) смогут использовать остальное. +Это не отобразится в документации, но другие инструменты (например, Sphinx) смогут использовать остальное. {* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} @@ -56,7 +56,7 @@ Вы, вероятно, уже видели, как объявлять `response_model` и `status_code` для *операции пути*. -Это определяет метаданные об основном ответе *операции пути*. +Это определяет метаданные об основном HTTP-ответе *операции пути*. Также можно объявлять дополнительные ответы с их моделями, статус-кодами и т.д. @@ -76,7 +76,7 @@ Там есть `tags`, `parameters`, `requestBody`, `responses` и т.д. -Эта спецификация OpenAPI, специфичная для *операции пути*, обычно генерируется автоматически **FastAPI**, но вы также можете её расширить. +Эта специфичная для *операции пути* схема OpenAPI обычно генерируется автоматически **FastAPI**, но вы также можете её расширить. /// tip | Совет @@ -129,13 +129,13 @@ } ``` -### Пользовательская схема OpenAPI для операции пути { #custom-openapi-path-operation-schema } +### Пользовательская схема OpenAPI для *операции пути* { #custom-openapi-path-operation-schema } -Словарь в `openapi_extra` будет объединён с автоматически сгенерированной схемой OpenAPI для *операции пути*. +Словарь в `openapi_extra` будет глубоко объединён с автоматически сгенерированной схемой OpenAPI для *операции пути*. Таким образом, вы можете добавить дополнительные данные к автоматически сгенерированной схеме. -Например, вы можете решить читать и валидировать запрос своим кодом, не используя автоматические возможности FastAPI и Pydantic, но при этом захотите описать запрос в схеме OpenAPI. +Например, вы можете решить читать и валидировать HTTP-запрос своим кодом, не используя автоматические возможности FastAPI и Pydantic, но при этом захотите описать HTTP-запрос в схеме OpenAPI. Это можно сделать с помощью `openapi_extra`: @@ -149,52 +149,20 @@ Используя тот же приём, вы можете воспользоваться Pydantic-моделью, чтобы определить JSON Schema, которая затем будет включена в пользовательский раздел схемы OpenAPI для *операции пути*. -И вы можете сделать это, даже если тип данных в запросе — не JSON. +И вы можете сделать это, даже если тип данных в HTTP-запросе — не JSON. -Например, в этом приложении мы не используем встроенную функциональность FastAPI для извлечения JSON Schema из моделей Pydantic, равно как и автоматическую валидацию JSON. Мы объявляем тип содержимого запроса как YAML, а не JSON: - -//// tab | Pydantic v2 +Например, в этом приложении мы не используем встроенную функциональность FastAPI для извлечения JSON Schema из моделей Pydantic, равно как и автоматическую валидацию JSON. Мы объявляем тип содержимого HTTP-запроса как YAML, а не JSON: {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *} - -//// - -/// info | Информация - -В Pydantic версии 1 метод для получения JSON Schema модели назывался `Item.schema()`, в Pydantic версии 2 метод называется `Item.model_json_schema()`. - -/// - Тем не менее, хотя мы не используем встроенную функциональность по умолчанию, мы всё равно используем Pydantic-модель, чтобы вручную сгенерировать JSON Schema для данных, которые мы хотим получить в YAML. -Затем мы работаем с запросом напрямую и извлекаем тело как `bytes`. Это означает, что FastAPI даже не попытается распарсить полезную нагрузку запроса как JSON. +Затем мы работаем с HTTP-запросом напрямую и извлекаем тело как `bytes`. Это означает, что FastAPI даже не попытается распарсить полезную нагрузку HTTP-запроса как JSON. -А затем в нашем коде мы напрямую парсим этот YAML и снова используем ту же Pydantic-модель для валидации YAML-содержимого: - -//// tab | Pydantic v2 +А затем в нашем коде мы напрямую парсим это содержимое YAML и снова используем ту же Pydantic-модель, чтобы валидировать YAML-содержимое: {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *} - -//// - -/// info | Информация - -В Pydantic версии 1 метод для парсинга и валидации объекта назывался `Item.parse_obj()`, в Pydantic версии 2 метод называется `Item.model_validate()`. - -/// - /// tip | Совет Здесь мы переиспользуем ту же Pydantic-модель. diff --git a/docs/ru/docs/advanced/settings.md b/docs/ru/docs/advanced/settings.md index b96ee44a3..8408faebf 100644 --- a/docs/ru/docs/advanced/settings.md +++ b/docs/ru/docs/advanced/settings.md @@ -46,12 +46,6 @@ $ pip install "fastapi[all]" </div> -/// info | Информация - -В Pydantic v1 он входил в основной пакет. Теперь он распространяется как отдельный пакет, чтобы вы могли установить его только при необходимости. - -/// - ### Создание объекта `Settings` { #create-the-settings-object } Импортируйте `BaseSettings` из Pydantic и создайте подкласс, очень похожий на Pydantic‑модель. @@ -60,24 +54,8 @@ $ pip install "fastapi[all]" Вы можете использовать все те же возможности валидации и инструменты, что и для Pydantic‑моделей, например разные типы данных и дополнительную валидацию через `Field()`. -//// tab | Pydantic v2 - {* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} -//// - -//// tab | Pydantic v1 - -/// info | Информация - -В Pydantic v1 вы бы импортировали `BaseSettings` напрямую из `pydantic`, а не из `pydantic_settings`. - -/// - -{* ../../docs_src/settings/tutorial001_pv1_py39.py hl[2,5:8,11] *} - -//// - /// tip | Совет Если вам нужно что-то быстро скопировать и вставить, не используйте этот пример — воспользуйтесь последним ниже. @@ -215,8 +193,6 @@ APP_NAME="ChimichangApp" Затем обновите ваш `config.py` так: -//// tab | Pydantic v2 - {* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip | Совет @@ -225,26 +201,6 @@ APP_NAME="ChimichangApp" /// -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *} - -/// tip | Совет - -Класс `Config` используется только для конфигурации Pydantic. Подробнее см. <a href="https://docs.pydantic.dev/1.10/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>. - -/// - -//// - -/// info | Информация - -В Pydantic версии 1 конфигурация задавалась во внутреннем классе `Config`, в Pydantic версии 2 — в атрибуте `model_config`. Этот атрибут принимает `dict`, и чтобы получить автозавершение и ошибки «на лету», вы можете импортировать и использовать `SettingsConfigDict` для описания этого `dict`. - -/// - Здесь мы задаем параметр конфигурации `env_file` внутри вашего класса Pydantic `Settings` и устанавливаем значение равным имени файла dotenv, который хотим использовать. ### Создание `Settings` только один раз с помощью `lru_cache` { #creating-the-settings-only-once-with-lru-cache } diff --git a/docs/ru/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/ru/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md index 95481bc66..2b47c08f6 100644 --- a/docs/ru/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md +++ b/docs/ru/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -2,21 +2,23 @@ Если у вас старое приложение FastAPI, возможно, вы используете Pydantic версии 1. -FastAPI поддерживает и Pydantic v1, и v2 начиная с версии 0.100.0. +FastAPI версии 0.100.0 поддерживал либо Pydantic v1, либо v2. Он использовал ту версию, которая была установлена. -Если у вас был установлен Pydantic v2, использовался он. Если вместо этого был установлен Pydantic v1 — использовался он. +FastAPI версии 0.119.0 добавил частичную поддержку Pydantic v1 изнутри Pydantic v2 (как `pydantic.v1`), чтобы упростить миграцию на v2. -Сейчас Pydantic v1 объявлен устаревшим, и поддержка его будет удалена в следующих версиях FastAPI, поэтому вам следует **перейти на Pydantic v2**. Так вы получите последние возможности, улучшения и исправления. +FastAPI 0.126.0 убрал поддержку Pydantic v1, при этом ещё некоторое время продолжал поддерживать `pydantic.v1`. /// warning | Предупреждение -Кроме того, команда Pydantic прекратила поддержку Pydantic v1 для последних версий Python, начиная с **Python 3.14**. +Команда Pydantic прекратила поддержку Pydantic v1 для последних версий Python, начиная с **Python 3.14**. + +Это включает `pydantic.v1`, который больше не поддерживается в Python 3.14 и выше. Если вы хотите использовать последние возможности Python, вам нужно убедиться, что вы используете Pydantic v2. /// -Если у вас старое приложение FastAPI с Pydantic v1, здесь я покажу, как мигрировать на Pydantic v2, и **новые возможности в FastAPI 0.119.0**, которые помогут выполнить постепенную миграцию. +Если у вас старое приложение FastAPI с Pydantic v1, здесь я покажу, как мигрировать на Pydantic v2, и **возможности FastAPI 0.119.0**, которые помогут выполнить постепенную миграцию. ## Официальное руководство { #official-guide } @@ -38,13 +40,13 @@ FastAPI поддерживает и Pydantic v1, и v2 начиная с вер Вы можете использовать <a href="https://github.com/pydantic/bump-pydantic" class="external-link" target="_blank">`bump-pydantic`</a> от той же команды Pydantic. -Этот инструмент поможет автоматически внести большую часть необходимых изменений в код. +Этот инструмент поможет автоматически изменить большую часть кода, который нужно изменить. -После этого запустите тесты и проверьте, что всё работает. Если да — на этом всё. 😎 +После этого вы можете запустить тесты и проверить, что всё работает. Если да — на этом всё. 😎 ## Pydantic v1 в v2 { #pydantic-v1-in-v2 } -Pydantic v2 включает всё из Pydantic v1 как подмодуль `pydantic.v1`. +Pydantic v2 включает всё из Pydantic v1 как подмодуль `pydantic.v1`. Но это больше не поддерживается в версиях Python выше 3.13. Это означает, что вы можете установить последнюю версию Pydantic v2 и импортировать и использовать старые компоненты Pydantic v1 из этого подмодуля так, как если бы у вас был установлен старый Pydantic v1. @@ -52,7 +54,7 @@ Pydantic v2 включает всё из Pydantic v1 как подмодуль ` ### Поддержка FastAPI для Pydantic v1 внутри v2 { #fastapi-support-for-pydantic-v1-in-v2 } -Начиная с FastAPI 0.119.0, есть также частичная поддержка Pydantic v1 в составе Pydantic v2, чтобы упростить миграцию на v2. +Начиная с FastAPI 0.119.0, есть также частичная поддержка Pydantic v1 изнутри Pydantic v2, чтобы упростить миграцию на v2. Таким образом, вы можете обновить Pydantic до последней версии 2 и сменить импорты на подмодуль `pydantic.v1` — во многих случаях всё просто заработает. @@ -106,7 +108,7 @@ graph TB style V2Field fill:#f9fff3 ``` -В некоторых случаях можно использовать и модели Pydantic v1, и v2 в одной и той же операции пути (обработчике пути) вашего приложения FastAPI: +В некоторых случаях можно использовать и модели Pydantic v1, и v2 в одной и той же **операции пути** (обработчике пути) вашего приложения FastAPI: {* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} @@ -122,12 +124,12 @@ graph TB /// tip | Совет -Сначала попробуйте `bump-pydantic`. Если тесты проходят и всё работает, вы справились одной командой. ✨ +Сначала попробуйте `bump-pydantic`: если тесты проходят и всё работает, вы справились одной командой. ✨ /// Если `bump-pydantic` не подходит для вашего случая, вы можете использовать поддержку одновременной работы моделей Pydantic v1 и v2 в одном приложении, чтобы мигрировать на Pydantic v2 постепенно. -Сначала обновите Pydantic до последней 2-й версии и измените импорты так, чтобы все ваши модели использовали `pydantic.v1`. +Сначала вы можете обновить Pydantic до последней 2-й версии и изменить импорты так, чтобы все ваши модели использовали `pydantic.v1`. -Затем начните мигрировать ваши модели с Pydantic v1 на v2 группами, поэтапно. 🚶 +Затем вы можете начать мигрировать ваши модели с Pydantic v1 на v2 группами, поэтапно. 🚶 diff --git a/docs/ru/docs/how-to/separate-openapi-schemas.md b/docs/ru/docs/how-to/separate-openapi-schemas.md index 5b1214016..8f6c83e7e 100644 --- a/docs/ru/docs/how-to/separate-openapi-schemas.md +++ b/docs/ru/docs/how-to/separate-openapi-schemas.md @@ -2,7 +2,7 @@ При использовании **Pydantic v2** сгенерированный OpenAPI становится чуть более точным и **корректным**, чем раньше. 😎 -На самом деле, в некоторых случаях в OpenAPI будет даже **две JSON схемы** для одной и той же Pydantic‑модели: для входа и для выхода — в зависимости от наличия **значений по умолчанию**. +На самом деле, в некоторых случаях в OpenAPI будет даже **две JSON-схемы** для одной и той же Pydantic‑модели: для входа и для выхода — в зависимости от наличия **значений по умолчанию**. Посмотрим, как это работает, и как это изменить при необходимости. @@ -34,7 +34,7 @@ {* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py hl[19] *} -…то, поскольку у `description` есть значение по умолчанию, даже если вы **ничего не вернёте** для этого поля, оно всё равно будет иметь это **значение по умолчанию**. +…то, поскольку у `description` есть значение по умолчанию, если вы **ничего не вернёте** для этого поля, оно всё равно будет иметь это **значение по умолчанию**. ### Модель для данных ответа { #model-for-output-response-data } @@ -46,13 +46,13 @@ Это означает, что у него **всегда будет какое‑то значение**, просто иногда это значение может быть `None` (или `null` в JSON). -Следовательно, клиентам, использующим ваш API, не нужно проверять наличие этого значения: они могут **исходить из того, что поле всегда присутствует**, а в некоторых случаях имеет значение по умолчанию `None`. +Это означает, что клиентам, использующим ваш API, не нужно проверять, существует ли это значение или нет: они могут **исходить из того, что поле всегда присутствует**, но в некоторых случаях оно будет иметь значение по умолчанию `None`. В OpenAPI это описывается тем, что поле помечается как **обязательное**, поскольку оно всегда присутствует. Из‑за этого JSON Schema для модели может отличаться в зависимости от использования для **входа** или **выхода**: -* для **входа** `description` не будет обязательным +* для **входа** `description` **не будет обязательным** * для **выхода** оно будет **обязательным** (и при этом может быть `None`, или, в терминах JSON, `null`) ### Выходная модель в документации { #model-for-output-in-docs } @@ -81,9 +81,9 @@ Однако бывают случаи, когда вы хотите иметь **одну и ту же схему для входа и выхода**. -Главный сценарий — когда у вас уже есть сгенерированный клиентский код/SDK, и вы пока не хотите обновлять весь этот автогенерируемый код/SDK (рано или поздно вы это сделаете, но не сейчас). +Главный сценарий — когда у вас уже есть сгенерированный клиентский код/SDK, и вы пока не хотите обновлять весь этот автогенерируемый клиентский код/SDK, вероятно, вы захотите сделать это в какой-то момент, но, возможно, не прямо сейчас. -В таком случае вы можете отключить эту функциональность в FastAPI с помощью параметра `separate_input_output_schemas=False`. +В таком случае вы можете отключить эту функциональность в **FastAPI** с помощью параметра `separate_input_output_schemas=False`. /// info | Информация @@ -95,10 +95,8 @@ ### Одна и та же схема для входной и выходной моделей в документации { #same-schema-for-input-and-output-models-in-docs } -Теперь для этой модели будет одна общая схема и для входа, и для выхода — только `Item`, и в ней `description` будет **не обязательным**: +И теперь для модели будет одна общая схема и для входа, и для выхода — только `Item`, и в ней `description` будет **не обязательным**: <div class="screenshot"> <img src="/img/tutorial/separate-openapi-schemas/image05.png"> </div> - -Это то же поведение, что и в Pydantic v1. 🤓 diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index b562cbe5b..02b1c9a28 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -5,10 +5,10 @@ </style> <p align="center"> - <a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> + <a href="https://fastapi.tiangolo.com/ru"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> </p> <p align="center"> - <em>Фреймворк FastAPI: высокая производительность, прост в изучении, быстрый в разработке, готов к продакшн</em> + <em>Фреймворк FastAPI: высокая производительность, прост в изучении, позволяет быстро писать код, готов к продакшн</em> </p> <p align="center"> <a href="https://github.com/fastapi/fastapi/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank"> @@ -40,7 +40,7 @@ FastAPI — это современный, быстрый (высокопрои * **Скорость**: Очень высокая производительность, на уровне **NodeJS** и **Go** (благодаря Starlette и Pydantic). [Один из самых быстрых доступных фреймворков Python](#performance). * **Быстрота разработки**: Увеличьте скорость разработки фич примерно на 200–300%. * * **Меньше ошибок**: Сократите примерно на 40% количество ошибок, вызванных человеком (разработчиком). * -* **Интуитивность**: Отличная поддержка редактора кода. <abbr title="также известное как: автодополнение, IntelliSense">Автозавершение</abbr> везде. Меньше времени на отладку. +* **Интуитивность**: Отличная поддержка редактора кода. <abbr title="также известное как: автодополнение, автозавершение, IntelliSense">Автозавершение</abbr> везде. Меньше времени на отладку. * **Простота**: Разработан так, чтобы его было легко использовать и осваивать. Меньше времени на чтение документации. * **Краткость**: Минимизируйте дублирование кода. Несколько возможностей из каждого объявления параметров. Меньше ошибок. * **Надежность**: Получите код, готовый к продакшн. С автоматической интерактивной документацией. @@ -117,6 +117,12 @@ FastAPI — это современный, быстрый (высокопрои --- +## Мини-документальный фильм о FastAPI { #fastapi-mini-documentary } + +В конце 2025 года вышел <a href="https://www.youtube.com/watch?v=mpR8ngthqiE" class="external-link" target="_blank">мини-документальный фильм о FastAPI</a>, вы можете посмотреть его онлайн: + +<a href="https://www.youtube.com/watch?v=mpR8ngthqiE" target="_blank"><img src="https://fastapi.tiangolo.com/img/fastapi-documentary.jpg" alt="FastAPI Mini Documentary"></a> + ## **Typer**, FastAPI для CLI { #typer-the-fastapi-of-clis } <a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> @@ -257,7 +263,7 @@ INFO: Application startup complete. * Получает HTTP-запросы по _путям_ `/` и `/items/{item_id}`. * Оба _пути_ используют `GET` <em>операции</em> (также известные как HTTP _методы_). -* _Путь_ `/items/{item_id}` имеет _параметр пути_ `item_id`, который должен быть `int`. +* _Путь_ `/items/{item_id}` имеет _path-параметр_ `item_id`, который должен быть `int`. * _Путь_ `/items/{item_id}` имеет необязательный `str` _параметр запроса_ `q`. ### Интерактивная документация API { #interactive-api-docs } @@ -278,9 +284,9 @@ INFO: Application startup complete. ## Пример обновления { #example-upgrade } -Теперь измените файл `main.py`, чтобы принимать тело запроса из `PUT` запроса. +Теперь измените файл `main.py`, чтобы принимать тело запроса из `PUT` HTTP-запроса. -Объявите тело, используя стандартные типы Python, спасибо Pydantic. +Объявите тело запроса, используя стандартные типы Python, спасибо Pydantic. ```Python hl_lines="4 9-12 25-27" from typing import Union @@ -318,7 +324,7 @@ def update_item(item_id: int, item: Item): Перейдите на <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. -* Интерактивная документация API будет автоматически обновлена, включая новое тело: +* Интерактивная документация API будет автоматически обновлена, включая новое тело запроса: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) @@ -334,13 +340,13 @@ def update_item(item_id: int, item: Item): Теперь откройте <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. -* Альтернативная документация также отразит новый параметр запроса и тело: +* Альтернативная документация также отразит новый параметр запроса и тело запроса: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) ### Подведём итоги { #recap } -Итак, вы объявляете **один раз** типы параметров, тела запроса и т.д. как параметры функции. +Итак, вы объявляете **один раз** типы параметров, тело запроса и т.д. как параметры функции. Вы делаете это с помощью стандартных современных типов Python. @@ -390,13 +396,13 @@ item: Item Возвращаясь к предыдущему примеру кода, **FastAPI** будет: -* Валидировать наличие `item_id` в пути для `GET` и `PUT` запросов. -* Валидировать, что `item_id` имеет тип `int` для `GET` и `PUT` запросов. +* Валидировать наличие `item_id` в пути для `GET` и `PUT` HTTP-запросов. +* Валидировать, что `item_id` имеет тип `int` для `GET` и `PUT` HTTP-запросов. * Если это не так, клиент увидит полезную понятную ошибку. -* Проверять, есть ли необязательный параметр запроса с именем `q` (например, `http://127.0.0.1:8000/items/foo?q=somequery`) для `GET` запросов. +* Проверять, есть ли необязательный параметр запроса с именем `q` (например, `http://127.0.0.1:8000/items/foo?q=somequery`) для `GET` HTTP-запросов. * Поскольку параметр `q` объявлен с `= None`, он необязателен. * Без `None` он был бы обязательным (как тело запроса в случае с `PUT`). -* Для `PUT` запросов к `/items/{item_id}` читать тело запроса как JSON: +* Для `PUT` HTTP-запросов к `/items/{item_id}` читать тело запроса как JSON: * Проверять, что есть обязательный атрибут `name`, который должен быть `str`. * Проверять, что есть обязательный атрибут `price`, который должен быть `float`. * Проверять, что есть необязательный атрибут `is_offer`, который должен быть `bool`, если он присутствует. @@ -435,11 +441,11 @@ item: Item Более полный пример с дополнительными возможностями см. в <a href="https://fastapi.tiangolo.com/ru/tutorial/">Учебник - Руководство пользователя</a>. -**Осторожно, спойлер**: учебник - руководство включает: +**Осторожно, спойлер**: учебник - руководство пользователя включает: * Объявление **параметров** из других источников: **HTTP-заголовки**, **cookies**, **поля формы** и **файлы**. * Как задать **ограничения валидации** вроде `maximum_length` или `regex`. -* Очень мощную и простую в использовании систему **<abbr title="также известная как: компоненты, ресурсы, провайдеры, сервисы, инъекции">внедрения зависимостей</abbr>**. +* Очень мощную и простую в использовании систему **<abbr title="также известна как: компоненты, ресурсы, провайдеры, сервисы, инъекции">внедрения зависимостей</abbr>**. * Безопасность и аутентификацию, включая поддержку **OAuth2** с **JWT токенами** и **HTTP Basic** аутентификацию. * Более продвинутые (но столь же простые) приёмы объявления **глубоко вложенных JSON-моделей** (спасибо Pydantic). * Интеграцию **GraphQL** с <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a> и другими библиотеками. @@ -524,11 +530,11 @@ FastAPI зависит от Pydantic и Starlette. * <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> — обязателен, если вы хотите использовать `TestClient`. * <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> — обязателен, если вы хотите использовать конфигурацию шаблонов по умолчанию. -* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> — обязателен, если вы хотите поддерживать <abbr title="преобразование строки, полученной из HTTP-запроса, в данные Python">«парсинг»</abbr> форм через `request.form()`. +* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - обязателен, если вы хотите поддерживать <abbr title="преобразование строки, полученной из HTTP-запроса, в данные Python">«парсинг»</abbr> форм через `request.form()`. Используется FastAPI: -* <a href="https://www.uvicorn.dev" target="_blank"><code>uvicorn</code></a> — сервер, который загружает и обслуживает ваше приложение. Включает `uvicorn[standard]`, содержащий некоторые зависимости (например, `uvloop`), нужные для высокой производительности. +* <a href="https://www.uvicorn.dev" target="_blank"><code>uvicorn</code></a> — сервер, который загружает и «отдаёт» ваше приложение. Включает `uvicorn[standard]`, содержащий некоторые зависимости (например, `uvloop`), нужные для высокой производительности. * `fastapi-cli[standard]` — чтобы предоставить команду `fastapi`. * Включает `fastapi-cloud-cli`, который позволяет развернуть ваше приложение FastAPI в <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>. diff --git a/docs/ru/docs/tutorial/bigger-applications.md b/docs/ru/docs/tutorial/bigger-applications.md index 5e5d6ada9..76304523c 100644 --- a/docs/ru/docs/tutorial/bigger-applications.md +++ b/docs/ru/docs/tutorial/bigger-applications.md @@ -1,4 +1,4 @@ -# Большие приложения, в которых много файлов { #bigger-applications-multiple-files } +# Большие приложения — несколько файлов { #bigger-applications-multiple-files } При построении приложения или веб-API нам редко удается поместить всё в один файл. @@ -31,7 +31,7 @@ /// tip | Подсказка -Обратите внимание, что в каждом каталоге и подкаталоге имеется файл `__init__.py` +Есть несколько файлов `__init__.py`: по одному в каждом каталоге или подкаталоге. Это как раз то, что позволяет импортировать код из одного файла в другой. @@ -43,61 +43,63 @@ from app.routers import items /// -* Всё помещается в каталоге `app`. В нём также находится пустой файл `app/__init__.py`. Таким образом, `app` является "Python-пакетом" (коллекцией модулей Python). -* Он содержит файл `app/main.py`. Данный файл является частью пакета (т.е. находится внутри каталога, содержащего файл `__init__.py`), и, соответственно, он является модулем пакета: `app.main`. +* Всё помещается в каталоге `app`. В нём также находится пустой файл `app/__init__.py`. Таким образом, `app` является "Python-пакетом" (коллекцией "Python-модулей"): `app`. +* Он содержит файл `app/main.py`. Данный файл является частью Python-пакета (т.е. находится внутри каталога, содержащего файл `__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/routers/`, содержащий `__init__.py`. Он является Python-подпакетом: `app.routers`. +* Файл `app/routers/items.py` находится внутри пакета `app/routers/`. Таким образом, он является подмодулем: `app.routers.items`. +* Точно так же `app/routers/users.py` является ещё одним подмодулем: `app.routers.users`. +* Подкаталог `app/internal/`, содержащий файл `__init__.py`, является ещё одним Python-подпакетом: `app.internal`. +* А файл `app/internal/admin.py` является ещё одним подмодулем: `app.internal.admin`. <img src="/img/tutorial/bigger-applications/package.drawio.svg"> Та же самая файловая структура приложения, но с комментариями: -``` +```bash . ├── 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 +│   └── 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` { #apirouter } -Давайте предположим, что для работы с пользователями используется отдельный файл (суб-модуль) `/app/routers/users.py`. +Давайте предположим, что для работы с пользователями используется отдельный файл (подмодуль) `/app/routers/users.py`. -Для лучшей организации приложения, вы хотите отделить операции пути, связанные с пользователями, от остального кода. +Вы хотите отделить *операции пути*, связанные с пользователями, от остального кода, чтобы сохранить порядок. -Но так, чтобы эти операции по-прежнему оставались частью **FastAPI** приложения/веб-API (частью одного пакета) +Но это всё равно часть того же приложения/веб-API на **FastAPI** (часть того же «Python-пакета»). -С помощью `APIRouter` вы можете создать *операции пути* (*эндпоинты*) для данного модуля. +С помощью `APIRouter` вы можете создать *операции пути* для этого модуля. ### Импорт `APIRouter` { #import-apirouter } -Точно также, как и в случае с классом `FastAPI`, вам нужно импортировать и создать объект класса `APIRouter`. +Точно так же, как и в случае с классом `FastAPI`, вам нужно импортировать и создать его «экземпляр»: {* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} -### Создание *эндпоинтов* с помощью `APIRouter` { #path-operations-with-apirouter } +### *Операции пути* с `APIRouter` { #path-operations-with-apirouter } -В дальнейшем используйте `APIRouter` для объявления *эндпоинтов*, точно также, как вы используете класс `FastAPI`: +И затем вы используете его, чтобы объявить ваши *операции пути*. + +Используйте его так же, как вы использовали бы класс `FastAPI`: {* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} -Вы можете думать об `APIRouter` как об "уменьшенной версии" класса FastAPI`. +Вы можете думать об `APIRouter` как об «мини-классе `FastAPI`». -`APIRouter` поддерживает все те же самые опции. +Поддерживаются все те же опции. -`APIRouter` поддерживает все те же самые параметры, такие как `parameters`, `responses`, `dependencies`, `tags`, и т. д. +Все те же `parameters`, `responses`, `dependencies`, `tags` и т.д. /// tip | Подсказка @@ -105,21 +107,21 @@ from app.routers import items /// -Мы собираемся подключить данный `APIRouter` к нашему основному приложению на `FastAPI`, но сначала давайте проверим зависимости и создадим ещё один модуль с `APIRouter`. +Мы собираемся подключить данный `APIRouter` к нашему основному приложению на `FastAPI`, но сначала давайте проверим зависимости и ещё один `APIRouter`. ## Зависимости { #dependencies } -Нам понадобятся некоторые зависимости, которые мы будем использовать в разных местах нашего приложения. +Мы видим, что нам понадобятся некоторые зависимости, которые будут использоваться в нескольких местах приложения. -Мы поместим их в отдельный модуль `dependencies` (`app/dependencies.py`). +Поэтому мы поместим их в отдельный модуль `dependencies` (`app/dependencies.py`). -Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомизированный `X-Token` из заголовка: +Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомный HTTP-заголовок `X-Token`: {* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} /// tip | Подсказка -Для простоты мы воспользовались неким воображаемым заголовоком. +Для простоты мы воспользовались выдуманным заголовком. В реальных случаях для получения наилучших результатов используйте интегрированные [утилиты безопасности](security/index.md){.internal-link target=_blank}. @@ -127,30 +129,29 @@ from app.routers import items ## Ещё один модуль с `APIRouter` { #another-module-with-apirouter } -Давайте также предположим, что у вас есть *эндпоинты*, отвечающие за обработку "items", и они находятся в модуле `app/routers/items.py`. +Давайте также предположим, что у вас есть эндпоинты, отвечающие за обработку «items» в вашем приложении, и они находятся в модуле `app/routers/items.py`. -У вас определены следующие *операции пути* (*эндпоинты*): +У вас определены *операции пути* для: * `/items/` * `/items/{item_id}` -Тут всё точно также, как и в ситуации с `app/routers/users.py`. +Тут всё та же структура, как и в случае с `app/routers/users.py`. -Но теперь мы хотим поступить немного умнее и слегка упростить код. +Но мы хотим поступить умнее и слегка упростить код. -Мы знаем, что все *эндпоинты* данного модуля имеют некоторые общие свойства: +Мы знаем, что все *операции пути* этого модуля имеют одинаковые: -* Префикс пути: `/items`. -* Теги: (один единственный тег: `items`). -* Дополнительные ответы (responses) -* Зависимости: использование созданной нами зависимости `X-token` +* `prefix` пути: `/items`. +* `tags`: (один единственный тег: `items`). +* Дополнительные `responses`. +* `dependencies`: всем им нужна та зависимость `X-Token`, которую мы создали. -Таким образом, вместо того чтобы добавлять все эти свойства в функцию каждого отдельного *эндпоинта*, -мы добавим их в `APIRouter`. +Таким образом, вместо того чтобы добавлять всё это в каждую *операцию пути*, мы можем добавить это в `APIRouter`. {* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} -Так как каждый *эндпоинт* начинается с символа `/`: +Так как путь каждой *операции пути* должен начинаться с `/`, как здесь: ```Python hl_lines="1" @router.get("/{item_id}") @@ -162,73 +163,74 @@ async def read_item(item_id: str): В нашем случае префиксом является `/items`. -Мы также можем добавить в наш маршрутизатор (router) список `тегов` (`tags`) и дополнительных `ответов` (`responses`), которые являются общими для каждого *эндпоинта*. +Мы также можем добавить список `tags` и дополнительные `responses`, которые будут применяться ко всем *операциям пути*, включённым в этот маршрутизатор. -И ещё мы можем добавить в наш маршрутизатор список `зависимостей`, которые должны вызываться при каждом обращении к *эндпоинтам*. +И ещё мы можем добавить список `dependencies`, которые будут добавлены ко всем *операциям пути* в маршрутизаторе и будут выполняться/разрешаться для каждого HTTP-запроса к ним. /// tip | Подсказка -Обратите внимание, что также, как и в случае с зависимостями в декораторах *эндпоинтов* ([зависимости в декораторах операций пути](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), никакого значения в *функцию эндпоинта* передано не будет. +Обратите внимание, что так же, как и в случае с [зависимостями в декораторах *операций пути*](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, никакое значение не будет передано в вашу *функцию-обработчик пути*. /// -В результате мы получим следующие эндпоинты: +В результате пути для items теперь такие: * `/items/` * `/items/{item_id}` ...как мы и планировали. -* Они будут помечены тегами из заданного списка, в нашем случае это `"items"`. - * Эти теги особенно полезны для системы автоматической интерактивной документации (с использованием OpenAPI). -* Каждый из них будет включать предопределенные ответы `responses`. -* Каждый *эндпоинт* будет иметь список зависимостей (`dependencies`), исполняемых перед вызовом *эндпоинта*. - * Если вы определили зависимости в самой операции пути, **то она также будет выполнена**. - * Сначала выполняются зависимости маршрутизатора, затем вызываются [зависимости в декораторе](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, и, наконец, обычные параметрические зависимости. - * Вы также можете добавить [зависимости `Security` с `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}. +* Они будут помечены списком тегов, содержащим одну строку `"items"`. + * Эти «теги» особенно полезны для систем автоматической интерактивной документации (с использованием OpenAPI). +* Все они будут включать предопределённые `responses`. +* Все эти *операции пути* будут иметь список `dependencies`, вычисляемых/выполняемых перед ними. + * Если вы также объявите зависимости в конкретной *операции пути*, **они тоже будут выполнены**. + * Сначала выполняются зависимости маршрутизатора, затем [`dependencies` в декораторе](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, и затем обычные параметрические зависимости. + * Вы также можете добавить [`Security`-зависимости с `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}. /// tip | Подсказка -Например, с помощью зависимостей в `APIRouter` мы можем потребовать аутентификации для доступа ко всей группе *эндпоинтов*, не указывая зависимости для каждой отдельной функции *эндпоинта*. +Например, с помощью зависимостей в `APIRouter` мы можем потребовать аутентификации для доступа ко всей группе *операций пути*. Даже если зависимости не добавляются по отдельности к каждой из них. /// /// check | Заметка -Параметры `prefix`, `tags`, `responses` и `dependencies` относятся к функционалу **FastAPI**, помогающему избежать дублирования кода. +Параметры `prefix`, `tags`, `responses` и `dependencies` — это (как и во многих других случаях) просто возможность **FastAPI**, помогающая избежать дублирования кода. /// ### Импорт зависимостей { #import-the-dependencies } -Наш код находится в модуле `app.routers.items` (файл `app/routers/items.py`). +Этот код находится в модуле `app.routers.items`, в файле `app/routers/items.py`. -И нам нужно вызвать функцию зависимости из модуля `app.dependencies` (файл `app/dependencies.py`). +И нам нужно получить функцию зависимости из модуля `app.dependencies`, файла `app/dependencies.py`. -Мы используем операцию относительного импорта `..` для импорта зависимости: +Поэтому мы используем относительный импорт с `..` для зависимостей: {* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *} -#### Как работает относительный импорт? { #how-relative-imports-work } +#### Как работает относительный импорт { #how-relative-imports-work } /// tip | Подсказка -Если вы прекрасно знаете, как работает импорт в Python, то переходите к следующему разделу. +Если вы прекрасно знаете, как работает импорт, переходите к следующему разделу ниже. /// -Одна точка `.`, как в данном примере: +Одна точка `.`, как здесь: ```Python from .dependencies import get_token_header ``` + означает: -* Начните с пакета, в котором находится данный модуль (файл `app/routers/items.py` расположен в каталоге `app/routers/`)... -* ... найдите модуль `dependencies` (файл `app/routers/dependencies.py`)... -* ... и импортируйте из него функцию `get_token_header`. +* Начать в том же пакете, в котором находится этот модуль (файл `app/routers/items.py`) (каталог `app/routers/`)... +* найти модуль `dependencies` (воображаемый файл `app/routers/dependencies.py`)... +* и импортировать из него функцию `get_token_header`. -К сожалению, такого файла не существует, и наши зависимости находятся в файле `app/dependencies.py`. +Но такого файла не существует, наши зависимости находятся в файле `app/dependencies.py`. Вспомните, как выглядит файловая структура нашего приложения: @@ -236,7 +238,7 @@ from .dependencies import get_token_header --- -Две точки `..`, как в данном примере: +Две точки `..`, как здесь: ```Python from ..dependencies import get_token_header @@ -244,12 +246,12 @@ from ..dependencies import get_token_header означают: -* Начните с пакета, в котором находится данный модуль (файл `app/routers/items.py` находится в каталоге `app/routers/`)... -* ... перейдите в родительский пакет (каталог `app/`)... -* ... найдите в нём модуль `dependencies` (файл `app/dependencies.py`)... -* ... и импортируйте из него функцию `get_token_header`. +* Начать в том же пакете, в котором находится этот модуль (файл `app/routers/items.py`) (каталог `app/routers/`)... +* перейти в родительский пакет (каталог `app/`)... +* и там найти модуль `dependencies` (файл `app/dependencies.py`)... +* и импортировать из него функцию `get_token_header`. -Это работает верно! 🎉 +Это работает корректно! 🎉 --- @@ -261,29 +263,29 @@ from ...dependencies import get_token_header то это бы означало: -* Начните с пакета, в котором находится данный модуль (файл `app/routers/items.py` находится в каталоге `app/routers/`)... -* ... перейдите в родительский пакет (каталог `app/`)... -* ... затем перейдите в родительский пакет текущего пакета (такого пакета не существует, `app` находится на самом верхнем уровне 😱)... -* ... найдите в нём модуль `dependencies` (файл `app/dependencies.py`)... -* ... и импортируйте из него функцию `get_token_header`. +* Начать в том же пакете, в котором находится этот модуль (файл `app/routers/items.py`) расположен в (каталоге `app/routers/`)... +* перейти в родительский пакет (каталог `app/`)... +* затем перейти в родительский пакет этого пакета (родительского пакета нет, `app` — верхний уровень 😱)... +* и там найти модуль `dependencies` (файл `app/dependencies.py`)... +* и импортировать из него функцию `get_token_header`. -Это будет относиться к некоторому пакету, находящемуся на один уровень выше чем `app/` и содержащему свой собственный файл `__init__.py`. Но ничего такого у нас нет. Поэтому это приведет к ошибке в нашем примере. 🚨 +Это ссылалось бы на какой-то пакет выше `app/`, со своим файлом `__init__.py` и т.п. Но у нас такого нет. Поэтому это вызвало бы ошибку в нашем примере. 🚨 -Теперь вы знаете, как работает импорт в Python, и сможете использовать относительное импортирование в своих собственных приложениях любого уровня сложности. 🤓 +Но теперь вы знаете, как это работает, так что можете использовать относительные импорты в своих приложениях, независимо от того, насколько они сложные. 🤓 -### Добавление пользовательских тегов (`tags`), ответов (`responses`) и зависимостей (`dependencies`) { #add-some-custom-tags-responses-and-dependencies } +### Добавление пользовательских `tags`, `responses` и `dependencies` { #add-some-custom-tags-responses-and-dependencies } -Мы не будем добавлять префикс `/items` и список тегов `tags=["items"]` для каждого *эндпоинта*, т.к. мы уже их добавили с помощью `APIRouter`. +Мы не добавляем префикс `/items` и `tags=["items"]` к каждой *операции пути*, потому что мы добавили их в `APIRouter`. -Но помимо этого мы можем добавить новые теги для каждого отдельного *эндпоинта*, а также некоторые дополнительные ответы (`responses`), характерные для данного *эндпоинта*: +Но мы всё равно можем добавить _ещё_ `tags`, которые будут применяться к конкретной *операции пути*, а также дополнительные `responses`, специфичные для этой *операции пути*: {* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} /// tip | Подсказка -Последний *эндпоинт* будет иметь следующую комбинацию тегов: `["items", "custom"]`. +Эта последняя операция пути будет иметь комбинацию тегов: `["items", "custom"]`. -А также в его документации будут содержаться оба ответа: один для `404` и другой для `403`. +И в документации у неё будут оба ответа: один для `404` и один для `403`. /// @@ -293,29 +295,29 @@ from ...dependencies import get_token_header Именно сюда вы импортируете и именно здесь вы используете класс `FastAPI`. -Это основной файл вашего приложения, который объединяет всё в одно целое. +Это основной файл вашего приложения, который связывает всё воедино. -И теперь, когда большая часть логики приложения разделена на отдельные модули, основной файл `app/main.py` будет достаточно простым. +И так как большая часть вашей логики теперь будет находиться в отдельных специфичных модулях, основной файл будет довольно простым. ### Импорт `FastAPI` { #import-fastapi } -Вы импортируете и создаете класс `FastAPI` как обычно. +Вы импортируете и создаёте класс `FastAPI` как обычно. -Мы даже можем объявить [глобальные зависимости](dependencies/global-dependencies.md){.internal-link target=_blank}, которые будут объединены с зависимостями для каждого отдельного маршрутизатора: +И мы даже можем объявить [глобальные зависимости](dependencies/global-dependencies.md){.internal-link target=_blank}, которые будут объединены с зависимостями для каждого `APIRouter`: {* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} ### Импорт `APIRouter` { #import-the-apirouter } -Теперь мы импортируем другие суб-модули, содержащие `APIRouter`: +Теперь мы импортируем другие подмодули, содержащие `APIRouter`: {* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *} -Так как файлы `app/routers/users.py` и `app/routers/items.py` являются суб-модулями одного и того же Python-пакета `app`, то мы сможем их импортировать, воспользовавшись операцией относительного импорта `.`. +Так как файлы `app/routers/users.py` и `app/routers/items.py` являются подмодулями, входящими в один и тот же Python-пакет `app`, мы можем использовать одну точку `.` для импорта через «относительные импорты». -### Как работает импорт? { #how-the-importing-works } +### Как работает импорт { #how-the-importing-works } -Данная строка кода: +Этот фрагмент: ```Python from .routers import items, users @@ -323,15 +325,15 @@ from .routers import items, users означает: -* Начните с пакета, в котором содержится данный модуль (файл `app/main.py` содержится в каталоге `app/`)... -* ... найдите суб-пакет `routers` (каталог `app/routers/`)... -* ... и из него импортируйте суб-модули `items` (файл `app/routers/items.py`) и `users` (файл `app/routers/users.py`)... +* Начать в том же пакете, в котором находится этот модуль (файл `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`. +В модуле `items` будет переменная `router` (`items.router`). Это та же самая, которую мы создали в файле `app/routers/items.py`, это объект `APIRouter`. -И затем мы сделаем то же самое для модуля `users`. +И затем мы делаем то же самое для модуля `users`. -Мы также могли бы импортировать и другим методом: +Мы также могли бы импортировать их так: ```Python from app.routers import items, users @@ -339,44 +341,44 @@ from app.routers import items, users /// info | Примечание -Первая версия является примером относительного импорта: +Первая версия — это «относительный импорт»: ```Python from .routers import items, users ``` -Вторая версия является примером абсолютного импорта: +Вторая версия — это «абсолютный импорт»: ```Python from app.routers import items, users ``` -Узнать больше о пакетах и модулях в Python вы можете из <a href="https://docs.python.org/3/tutorial/modules.html" class="external-link" target="_blank">официальной документации Python о модулях</a> +Чтобы узнать больше о Python-пакетах и модулях, прочитайте <a href="https://docs.python.org/3/tutorial/modules.html" class="external-link" target="_blank">официальную документацию Python о модулях</a>. /// -### Избегайте конфликтов имен { #avoid-name-collisions } +### Избегайте конфликтов имён { #avoid-name-collisions } -Вместо того чтобы импортировать только переменную `router`, мы импортируем непосредственно суб-модуль `items`. +Мы импортируем подмодуль `items` напрямую, вместо того чтобы импортировать только его переменную `router`. -Мы делаем это потому, что у нас есть ещё одна переменная `router` в суб-модуле `users`. +Это потому, что у нас также есть другая переменная с именем `router` в подмодуле `users`. -Если бы мы импортировали их одну за другой, как показано в примере: +Если бы мы импортировали их одну за другой, как здесь: ```Python from .routers.items import router from .routers.users import router ``` -то переменная `router` из `users` переписал бы переменную `router` из `items`, и у нас не было бы возможности использовать их одновременно. +то `router` из `users` перезаписал бы `router` из `items`, и мы не смогли бы использовать их одновременно. -Поэтому, для того чтобы использовать обе эти переменные в одном файле, мы импортировали соответствующие суб-модули: +Поэтому, чтобы иметь возможность использовать обе в одном файле, мы импортируем подмодули напрямую: {* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *} -### Подключение маршрутизаторов (`APIRouter`) для `users` и для `items` { #include-the-apirouters-for-users-and-items } +### Подключение `APIRouter` для `users` и `items` { #include-the-apirouters-for-users-and-items } -Давайте подключим маршрутизаторы (`router`) из суб-модулей `users` и `items`: +Теперь давайте подключим `router` из подмодулей `users` и `items`: {* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *} @@ -388,79 +390,78 @@ from .routers.users import router /// -С помощью `app.include_router()` мы можем добавить каждый из маршрутизаторов (`APIRouter`) в основное приложение `FastAPI`. +С помощью `app.include_router()` мы можем добавить каждый `APIRouter` в основное приложение `FastAPI`. -Он подключит все маршруты заданного маршрутизатора к нашему приложению. +Он включит все маршруты этого маршрутизатора как часть приложения. /// note | Технические детали -Фактически, внутри он создаст все *операции пути* для каждой операции пути объявленной в `APIRouter`. +Фактически, внутри он создаст *операцию пути* для каждой *операции пути*, объявленной в `APIRouter`. -И под капотом всё будет работать так, как будто бы мы имеем дело с одним файлом приложения. +Так что под капотом всё будет работать так, как будто всё было одним приложением. /// /// check | Заметка -При подключении маршрутизаторов не стоит беспокоиться о производительности. +При подключении маршрутизаторов не нужно беспокоиться о производительности. -Операция подключения займёт микросекунды и понадобится только при запуске приложения. +Это займёт микросекунды и произойдёт только при старте. -Таким образом, это не повлияет на производительность. ⚡ +Так что это не повлияет на производительность. ⚡ /// -### Подключение `APIRouter` с пользовательскими префиксом (`prefix`), тегами (`tags`), ответами (`responses`), и зависимостями (`dependencies`) { #include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies } +### Подключение `APIRouter` с пользовательскими `prefix`, `tags`, `responses` и `dependencies` { #include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies } Теперь давайте представим, что ваша организация передала вам файл `app/internal/admin.py`. -Он содержит `APIRouter` с некоторыми *эндпоитами* администрирования, которые ваша организация использует для нескольких проектов. +Он содержит `APIRouter` с некоторыми административными *операциями пути*, которые ваша организация использует в нескольких проектах. -В данном примере это сделать очень просто. Но давайте предположим, что поскольку файл используется для нескольких проектов, -то мы не можем модифицировать его, добавляя префиксы (`prefix`), зависимости (`dependencies`), теги (`tags`), и т.д. непосредственно в `APIRouter`: +Для этого примера всё будет очень просто. Но допустим, что поскольку он используется совместно с другими проектами в организации, мы не можем модифицировать его и добавить `prefix`, `dependencies`, `tags` и т.д. непосредственно в `APIRouter`: {* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} -Но, несмотря на это, мы хотим использовать кастомный префикс (`prefix`) для подключенного маршрутизатора (`APIRouter`), в результате чего, каждая *операция пути* будет начинаться с `/admin`. Также мы хотим защитить наш маршрутизатор с помощью зависимостей, созданных для нашего проекта. И ещё мы хотим включить теги (`tags`) и ответы (`responses`). +Но мы всё равно хотим задать пользовательский `prefix` при подключении `APIRouter`, чтобы все его *операции пути* начинались с `/admin`, хотим защитить его с помощью `dependencies`, которые у нас уже есть для этого проекта, и хотим включить `tags` и `responses`. -Мы можем применить все вышеперечисленные настройки, не изменяя начальный `APIRouter`. Нам всего лишь нужно передать нужные параметры в `app.include_router()`. +Мы можем объявить всё это, не изменяя исходный `APIRouter`, передав эти параметры в `app.include_router()`: {* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *} -Таким образом, оригинальный `APIRouter` не будет модифицирован, и мы сможем использовать файл `app/internal/admin.py` сразу в нескольких проектах организации. +Таким образом исходный `APIRouter` не будет модифицирован, и мы сможем использовать файл `app/internal/admin.py` сразу в нескольких проектах организации. -В результате, в нашем приложении каждый *эндпоинт* модуля `admin` будет иметь: +В результате в нашем приложении каждая из *операций пути* из модуля `admin` будет иметь: * Префикс `/admin`. * Тег `admin`. * Зависимость `get_token_header`. * Ответ `418`. 🍵 -Это будет иметь место исключительно для `APIRouter` в нашем приложении, и не затронет любой другой код, использующий его. +Но это повлияет только на этот `APIRouter` в нашем приложении, а не на любой другой код, который его использует. -Например, другие проекты, могут использовать тот же самый `APIRouter` с другими методами аутентификации. +Так что, например, другие проекты могут использовать тот же `APIRouter` с другим методом аутентификации. -### Подключение отдельного *эндпоинта* { #include-a-path-operation } +### Подключение *операции пути* { #include-a-path-operation } -Мы также можем добавить *эндпоинт* непосредственно в основное приложение `FastAPI`. +Мы также можем добавлять *операции пути* напрямую в приложение `FastAPI`. -Здесь мы это делаем ... просто, чтобы показать, что это возможно 🤷: +Здесь мы делаем это... просто чтобы показать, что можем 🤷: {* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *} -и это будет работать корректно вместе с другими *эндпоинтами*, добавленными с помощью `app.include_router()`. +и это будет работать корректно вместе со всеми другими *операциями пути*, добавленными через `app.include_router()`. -/// info | Сложные технические детали +/// info | Очень технические детали -**Примечание**: это сложная техническая деталь, которую, скорее всего, **вы можете пропустить**. +**Примечание**: это очень техническая деталь, которую, вероятно, можно **просто пропустить**. --- -Маршрутизаторы (`APIRouter`) не "монтируются" по-отдельности и не изолируются от остального приложения. +`APIRouter` не «монтируются», они не изолированы от остального приложения. -Это происходит потому, что нужно включить их *эндпоинты* в OpenAPI схему и в интерфейс пользователя. +Это потому, что мы хотим включить их *операции пути* в OpenAPI-схему и пользовательские интерфейсы. -В силу того, что мы не можем их изолировать и "примонтировать" независимо от остальных, *эндпоинты* клонируются (пересоздаются) и не подключаются напрямую. +Так как мы не можем просто изолировать их и «смонтировать» независимо от остального, *операции пути* «клонируются» (пересоздаются), а не включаются напрямую. /// @@ -480,24 +481,24 @@ $ fastapi dev app/main.py Откройте документацию по адресу <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. -Вы увидите автоматическую API документацию. Она включает в себя маршруты из суб-модулей, используя верные маршруты, префиксы и теги: +Вы увидите автоматическую документацию API, включая пути из всех подмодулей, с использованием корректных путей (и префиксов) и корректных тегов: <img src="/img/tutorial/bigger-applications/image01.png"> -## Подключение существующего маршрута через новый префикс (`prefix`) { #include-the-same-router-multiple-times-with-different-prefix } +## Подключение одного и того же маршрутизатора несколько раз с разными `prefix` { #include-the-same-router-multiple-times-with-different-prefix } -Вы можете использовать `.include_router()` несколько раз с одним и тем же маршрутом, применив различные префиксы. +Вы можете использовать `.include_router()` несколько раз с *одним и тем же* маршрутизатором, используя разные префиксы. -Это может быть полезным, если нужно предоставить доступ к одному и тому же API через различные префиксы, например, `/api/v1` и `/api/latest`. +Это может быть полезно, например, чтобы предоставить доступ к одному и тому же API с разными префиксами, например `/api/v1` и `/api/latest`. -Это продвинутый способ, который вам может и не пригодится. Мы приводим его на случай, если вдруг вам это понадобится. +Это продвинутое использование, которое вам может и не понадобиться, но оно есть на случай, если понадобится. -## Включение одного маршрутизатора (`APIRouter`) в другой { #include-an-apirouter-in-another } +## Подключение `APIRouter` в другой `APIRouter` { #include-an-apirouter-in-another } -Точно так же, как вы включаете `APIRouter` в приложение `FastAPI`, вы можете включить `APIRouter` в другой `APIRouter`: +Точно так же, как вы можете подключить `APIRouter` к приложению `FastAPI`, вы можете подключить `APIRouter` к другому `APIRouter`, используя: ```Python router.include_router(other_router) ``` -Удостоверьтесь, что вы сделали это до того, как подключить маршрутизатор (`router`) к вашему `FastAPI` приложению, и *эндпоинты* маршрутизатора `other_router` были также подключены. +Убедитесь, что вы сделали это до подключения `router` к приложению `FastAPI`, чтобы *операции пути* из `other_router` также были подключены. diff --git a/docs/ru/docs/tutorial/body-updates.md b/docs/ru/docs/tutorial/body-updates.md index 73f4e66c7..4a7adb255 100644 --- a/docs/ru/docs/tutorial/body-updates.md +++ b/docs/ru/docs/tutorial/body-updates.md @@ -2,13 +2,13 @@ ## Обновление с заменой при помощи `PUT` { #update-replacing-with-put } -Для полного обновления элемента можно воспользоваться операцией <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT" class="external-link" target="_blank">HTTP `PUT`</a>. +Чтобы обновить элемент, вы можете использовать операцию <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT" class="external-link" target="_blank">HTTP `PUT`</a>. Вы можете использовать `jsonable_encoder`, чтобы преобразовать входные данные в данные, которые можно сохранить как JSON (например, в NoSQL-базе данных). Например, преобразование `datetime` в `str`. {* ../../docs_src/body_updates/tutorial001_py310.py hl[28:33] *} -`PUT` используется для получения данных, которые должны полностью заменить существующие данные. +`PUT` используется для получения данных, которые должны заменить существующие данные. ### Предупреждение о замене { #warning-about-replacing } @@ -24,11 +24,11 @@ поскольку оно не включает уже сохраненный атрибут `"tax": 20.2`, входная модель примет значение по умолчанию `"tax": 10.5`. -И данные будут сохранены с этим "новым" `tax`, равным `10,5`. +И данные будут сохранены с этим «новым» `tax`, равным `10.5`. ## Частичное обновление с помощью `PATCH` { #partial-updates-with-patch } -Также можно использовать <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH" class="external-link" target="_blank">HTTP `PATCH`</a> операцию для *частичного* обновления данных. +Также можно использовать операцию <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH" class="external-link" target="_blank">HTTP `PATCH`</a> для *частичного* обновления данных. Это означает, что можно передавать только те данные, которые необходимо обновить, оставляя остальные нетронутыми. @@ -46,19 +46,13 @@ ### Использование параметра `exclude_unset` в Pydantic { #using-pydantics-exclude-unset-parameter } -Если необходимо выполнить частичное обновление, то очень полезно использовать параметр `exclude_unset` в методе `.model_dump()` модели Pydantic. +Если вы хотите получать частичные обновления, очень полезно использовать параметр `exclude_unset` в `.model_dump()` модели Pydantic. Например, `item.model_dump(exclude_unset=True)`. -/// info | Информация +В результате будет сгенерирован `dict`, содержащий только те данные, которые были заданы при создании модели `item`, без учета значений по умолчанию. -В Pydantic v1 метод назывался `.dict()`, в Pydantic v2 он помечен как устаревший (но все еще поддерживается) и переименован в `.model_dump()`. - -Примеры здесь используют `.dict()` для совместимости с Pydantic v1, но если вы можете использовать Pydantic v2, лучше используйте `.model_dump()`. - -/// - -В результате будет сгенерирован словарь, содержащий только те данные, которые были заданы при создании модели `item`, без учета значений по умолчанию. Затем вы можете использовать это для создания словаря только с теми данными, которые были установлены (отправлены в запросе), опуская значения по умолчанию: +Затем вы можете использовать это для создания `dict` только с теми данными, которые были установлены (отправлены в запросе), опуская значения по умолчанию: {* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} @@ -66,14 +60,6 @@ Теперь можно создать копию существующей модели, используя `.model_copy()`, и передать параметр `update` с `dict`, содержащим данные для обновления. -/// info | Информация - -В Pydantic v1 метод назывался `.copy()`, в Pydantic v2 он помечен как устаревший (но все еще поддерживается) и переименован в `.model_copy()`. - -Примеры здесь используют `.copy()` для совместимости с Pydantic v1, но если вы можете использовать Pydantic v2, лучше используйте `.model_copy()`. - -/// - Например, `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} @@ -84,9 +70,9 @@ * (Опционально) использовать `PATCH` вместо `PUT`. * Извлечь сохранённые данные. -* Поместить эти данные в Pydantic модель. +* Поместить эти данные в Pydantic-модель. * Сгенерировать `dict` без значений по умолчанию из входной модели (с использованием `exclude_unset`). - * Таким образом, можно обновлять только те значения, которые действительно установлены пользователем, вместо того чтобы переопределять значения, уже сохраненные в модели по умолчанию. + * Таким образом, можно обновлять только те значения, которые действительно установлены пользователем, вместо того чтобы переопределять уже сохраненные значения значениями по умолчанию из вашей модели. * Создать копию хранимой модели, обновив ее атрибуты полученными частичными обновлениями (с помощью параметра `update`). * Преобразовать скопированную модель в то, что может быть сохранено в вашей БД (например, с помощью `jsonable_encoder`). * Это сравнимо с повторным использованием метода модели `.model_dump()`, но при этом происходит проверка (и преобразование) значений в типы данных, которые могут быть преобразованы в JSON, например, `datetime` в `str`. @@ -97,7 +83,7 @@ /// tip | Подсказка -Эту же технику можно использовать и для операции HTTP `PUT`. +На самом деле эту же технику можно использовать и для операции HTTP `PUT`. Но в приведенном примере используется `PATCH`, поскольку он был создан именно для таких случаев использования. diff --git a/docs/ru/docs/tutorial/body.md b/docs/ru/docs/tutorial/body.md index b61f3e7a0..537d7ebc9 100644 --- a/docs/ru/docs/tutorial/body.md +++ b/docs/ru/docs/tutorial/body.md @@ -32,9 +32,10 @@ {* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} + Так же, как при объявлении параметров запроса: когда атрибут модели имеет значение по умолчанию, он не обязателен. Иначе он обязателен. Используйте `None`, чтобы сделать его просто необязательным. -Например, модель выше описывает такой JSON "объект" (или Python `dict`): +Например, модель выше описывает такой JSON "`object`" (или Python `dict`): ```JSON { @@ -45,7 +46,7 @@ } ``` -...так как `description` и `tax` являются необязательными (со значением по умолчанию `None`), такой JSON "объект" тоже будет корректным: +...так как `description` и `tax` являются необязательными (со значением по умолчанию `None`), такой JSON "`object`" тоже будет корректным: ```JSON { @@ -73,7 +74,7 @@ * Передаст полученные данные в параметр `item`. * Поскольку внутри функции вы объявили его с типом `Item`, у вас будет поддержка со стороны редактора кода (автозавершение и т. п.) для всех атрибутов и их типов. * Сгенерирует определения <a href="https://json-schema.org" class="external-link" target="_blank">JSON Schema</a> для вашей модели; вы можете использовать их и в других местах, если это имеет смысл для вашего проекта. -* Эти схемы будут частью сгенерированной схемы OpenAPI и будут использоваться автоматической документацией <abbr title="User Interfaces – Пользовательские интерфейсы">UIs</abbr>. +* Эти схемы будут частью сгенерированной схемы OpenAPI и будут использоваться автоматической документацией <abbr title="User Interfaces - Пользовательские интерфейсы">UIs</abbr>. ## Автоматическая документация { #automatic-docs } @@ -127,14 +128,6 @@ JSON Schema ваших моделей будет частью сгенериро {* ../../docs_src/body/tutorial002_py310.py *} -/// info | Информация - -В Pydantic v1 метод назывался `.dict()`, в Pydantic v2 он был помечен как устаревший (но всё ещё поддерживается) и переименован в `.model_dump()`. - -Примеры здесь используют `.dict()` для совместимости с Pydantic v1, но если вы можете использовать Pydantic v2, используйте `.model_dump()`. - -/// - ## Тело запроса + параметры пути { #request-body-path-parameters } Вы можете одновременно объявить параметры пути и тело запроса. @@ -143,6 +136,7 @@ JSON Schema ваших моделей будет частью сгенериро {* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} + ## Тело запроса + параметры пути + параметры запроса { #request-body-path-query-parameters } Вы также можете одновременно объявить параметры **тела**, **пути** и **запроса**. @@ -153,7 +147,7 @@ JSON Schema ваших моделей будет частью сгенериро Параметры функции будут распознаны следующим образом: -* Если параметр также объявлен в **пути**, он будет использоваться как параметр пути. +* Если параметр также объявлен в **пути**, он будет использоваться как path-параметр. * Если параметр имеет **скалярный тип** (например, `int`, `float`, `str`, `bool` и т. п.), он будет интерпретирован как параметр **запроса**. * Если параметр объявлен как тип **модели Pydantic**, он будет интерпретирован как **тело** запроса. @@ -161,7 +155,7 @@ JSON Schema ваших моделей будет частью сгенериро FastAPI понимает, что значение `q` не является обязательным из-за значения по умолчанию `= None`. -Аннотации типов `str | None` (Python 3.10+) или `Union[str, None]` (Python 3.9+) не используются FastAPI для определения обязательности; он узнает, что параметр не обязателен, потому что у него есть значение по умолчанию `= None`. +Аннотации типов `str | None` (Python 3.10+) или `Union` в `Union[str, None]` (Python 3.9+) не используются FastAPI для определения обязательности; он узнает, что параметр не обязателен, потому что у него есть значение по умолчанию `= None`. Но добавление аннотаций типов позволит вашему редактору кода лучше вас поддерживать и обнаруживать ошибки. @@ -169,4 +163,4 @@ FastAPI понимает, что значение `q` не является об ## Без Pydantic { #without-pydantic } -Если вы не хотите использовать модели Pydantic, вы также можете использовать параметры **Body**. См. раздел документации [Тело — Несколько параметров: Единичные значения в теле](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. +Если вы не хотите использовать модели Pydantic, вы также можете использовать параметры **Body**. См. раздел документации [Тело запроса - Несколько параметров: Единичные значения в теле](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. diff --git a/docs/ru/docs/tutorial/extra-models.md b/docs/ru/docs/tutorial/extra-models.md index 2f0ce4e33..03156f2b4 100644 --- a/docs/ru/docs/tutorial/extra-models.md +++ b/docs/ru/docs/tutorial/extra-models.md @@ -22,21 +22,13 @@ {* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} -/// info | Информация +### Про `**user_in.model_dump()` { #about-user-in-model-dump } -В Pydantic v1 метод назывался `.dict()`, в Pydantic v2 он помечен как устаревший (но всё ещё поддерживается) и переименован в `.model_dump()`. +#### `.model_dump()` из Pydantic { #pydantics-model-dump } -В примерах здесь используется `.dict()` для совместимости с Pydantic v1, но если вы используете Pydantic v2, следует использовать `.model_dump()`. +`user_in` — это Pydantic-модель класса `UserIn`. -/// - -### Про `**user_in.dict()` { #about-user-in-dict } - -#### `.dict()` из Pydantic { #pydantics-dict } - -`user_in` - это Pydantic-модель класса `UserIn`. - -У Pydantic-моделей есть метод `.dict()`, который возвращает `dict` с данными модели. +У Pydantic-моделей есть метод `.model_dump()`, который возвращает `dict` с данными модели. Поэтому, если мы создадим Pydantic-объект `user_in` таким способом: @@ -47,10 +39,10 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com и затем вызовем: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() ``` -то теперь у нас есть `dict` с данными модели в переменной `user_dict` (это `dict` вместо объекта Pydantic-модели). +то теперь у нас есть `dict` с данными в переменной `user_dict` (это `dict` вместо объекта Pydantic-модели). И если мы вызовем: @@ -58,7 +50,7 @@ user_dict = user_in.dict() print(user_dict) ``` -мы можем получить `dict` с такими данными: +мы получим Python `dict` с: ```Python { @@ -71,7 +63,7 @@ print(user_dict) #### Распаковка `dict` { #unpacking-a-dict } -Если мы возьмём `dict` наподобие `user_dict` и передадим его в функцию (или класс), используя `**user_dict`, Python распакует его. Он передаст ключи и значения `user_dict` напрямую как аргументы типа ключ-значение. +Если мы возьмём `dict` наподобие `user_dict` и передадим его в функцию (или класс), используя `**user_dict`, Python его "распакует". Он передаст ключи и значения `user_dict` напрямую как аргументы типа ключ-значение. Поэтому, продолжая описанный выше пример с `user_dict`, написание такого кода: @@ -79,7 +71,7 @@ print(user_dict) UserInDB(**user_dict) ``` -Будет работать так же, как примерно такой код: +будет эквивалентно: ```Python UserInDB( @@ -90,7 +82,7 @@ UserInDB( ) ``` -Или, если для большей точности мы напрямую используем `user_dict` с любым потенциальным содержимым, то этот пример будет выглядеть так: +Или, более точно, если использовать `user_dict` напрямую, с любым содержимым, которое он может иметь в будущем: ```Python UserInDB( @@ -101,22 +93,22 @@ UserInDB( ) ``` -#### Pydantic-модель из содержимого другой модели { #a-pydantic-model-from-the-contents-of-another } +#### Pydantic-модель из содержимого другой { #a-pydantic-model-from-the-contents-of-another } -Как в примере выше мы получили `user_dict` из `user_in.dict()`, этот код: +Как в примере выше мы получили `user_dict` из `user_in.model_dump()`, этот код: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() UserInDB(**user_dict) ``` будет равнозначен такому: ```Python -UserInDB(**user_in.dict()) +UserInDB(**user_in.model_dump()) ``` -...потому что `user_in.dict()` - это `dict`, и затем мы указываем, чтобы Python его "распаковал", когда передаём его в `UserInDB` и ставим перед ним `**`. +...потому что `user_in.model_dump()` — это `dict`, и затем мы указываем, чтобы Python его "распаковал", когда передаём его в `UserInDB` с префиксом `**`. Таким образом мы получаем Pydantic-модель на основе данных из другой Pydantic-модели. @@ -125,10 +117,10 @@ UserInDB(**user_in.dict()) И затем, если мы добавим дополнительный именованный аргумент `hashed_password=hashed_password` как здесь: ```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) ``` -... то мы получим что-то подобное: +...то в итоге получится что-то подобное: ```Python UserInDB( @@ -142,13 +134,13 @@ UserInDB( /// warning | Предупреждение -Вспомогательные функции `fake_password_hasher` и `fake_save_user` используются только для демонстрации возможного потока данных и, конечно, не обеспечивают настоящую безопасность. +Вспомогательные дополнительные функции `fake_password_hasher` и `fake_save_user` используются только для демонстрации возможного потока данных и, конечно, не обеспечивают настоящую безопасность. /// ## Сократите дублирование { #reduce-duplication } -Сокращение дублирования кода - это одна из главных идей **FastAPI**. +Сокращение дублирования кода — это одна из главных идей **FastAPI**. Поскольку дублирование кода повышает риск появления багов, проблем с безопасностью, проблем десинхронизации кода (когда вы обновляете код в одном месте, но не обновляете в другом), и т.д. @@ -166,7 +158,7 @@ UserInDB( ## `Union` или `anyOf` { #union-or-anyof } -Вы можете определить ответ как `Union` из двух или более типов. Это означает, что ответ должен соответствовать одному из них. +Вы можете объявить HTTP-ответ как `Union` из двух или более типов. Это означает, что HTTP-ответ может быть любым из них. Он будет определён в OpenAPI как `anyOf`. @@ -174,7 +166,7 @@ UserInDB( /// note | Примечание -При объявлении <a href="https://docs.pydantic.dev/latest/concepts/types/#unions" class="external-link" target="_blank">`Union`</a>, сначала указывайте наиболее детальные типы, затем менее детальные. В примере ниже более детальный `PlaneItem` стоит перед `CarItem` в `Union[PlaneItem, CarItem]`. +При объявлении <a href="https://docs.pydantic.dev/latest/concepts/types/#unions" class="external-link" target="_blank">`Union`</a> сначала указывайте наиболее специфичный тип, затем менее специфичный. В примере ниже более специфичный `PlaneItem` стоит перед `CarItem` в `Union[PlaneItem, CarItem]`. /// @@ -192,19 +184,19 @@ UserInDB( some_variable: PlaneItem | CarItem ``` -Но если мы помещаем его в `response_model=PlaneItem | CarItem` мы получим ошибку, потому что Python попытается произвести **некорректную операцию** между `PlaneItem` и `CarItem` вместо того, чтобы интерпретировать это как аннотацию типа. +Но если мы поместим это в присваивание `response_model=PlaneItem | CarItem`, мы получим ошибку, потому что Python попытается произвести **некорректную операцию** между `PlaneItem` и `CarItem` вместо того, чтобы интерпретировать это как аннотацию типа. ## Список моделей { #list-of-models } -Таким же образом вы можете определять ответы как списки объектов. +Таким же образом вы можете объявлять HTTP-ответы, возвращающие списки объектов. -Для этого используйте `typing.List` из стандартной библиотеки Python (или просто `list` в Python 3.9 и выше): +Для этого используйте стандартный `typing.List` в Python (или просто `list` в Python 3.9 и выше): {* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *} ## Ответ с произвольным `dict` { #response-with-arbitrary-dict } -Вы также можете определить ответ, используя произвольный одноуровневый `dict` и определяя только типы ключей и значений без использования Pydantic-моделей. +Вы также можете объявить HTTP-ответ, используя обычный произвольный `dict`, объявив только тип ключей и значений, без использования Pydantic-модели. Это полезно, если вы заранее не знаете корректных названий полей/атрибутов (которые будут нужны при использовании Pydantic-модели). @@ -214,6 +206,6 @@ some_variable: PlaneItem | CarItem ## Резюме { #recap } -Используйте несколько Pydantic-моделей и свободно применяйте наследование для каждой из них. +Используйте несколько Pydantic-моделей и свободно применяйте наследование для каждого случая. -Вам не обязательно иметь единственную модель данных для каждой сущности, если эта сущность должна иметь возможность быть в разных "состояниях". Как в случае с "сущностью" пользователя, у которого есть состояния с полями `password`, `password_hash` и без пароля. +Вам не обязательно иметь единственную модель данных для каждой сущности, если эта сущность должна иметь возможность быть в разных "состояниях". Как в случае с "сущностью" пользователя, у которого есть состояние, включающее `password`, `password_hash` и отсутствие пароля. diff --git a/docs/ru/docs/tutorial/query-params-str-validations.md b/docs/ru/docs/tutorial/query-params-str-validations.md index 3a4ecc37d..2bc2fb22c 100644 --- a/docs/ru/docs/tutorial/query-params-str-validations.md +++ b/docs/ru/docs/tutorial/query-params-str-validations.md @@ -8,7 +8,7 @@ Query-параметр `q` имеет тип `str | None`, это означает, что он имеет тип `str`, но также может быть `None`. Значение по умолчанию действительно `None`, поэтому FastAPI будет знать, что он не обязателен. -/// note | Технические детали +/// note | Примечание FastAPI поймёт, что значение `q` не обязательно, из‑за значения по умолчанию `= None`. @@ -177,7 +177,7 @@ q: str = Query(default="rick") **Значение по умолчанию** у **параметра функции** — это **настоящее значение по умолчанию**, что более интуитивно для Python. 😌 -Вы можете **вызвать** эту же функцию в **других местах** без FastAPI, и она будет **работать как ожидается**. Если есть **обязательный** параметр (без значения по умолчанию), ваш **редактор кода** сообщит об ошибке, **Python** тоже пожалуется, если вы запустите её без передачи обязательного параметра. +Вы можете **вызвать** эту же функцию в **других местах** без FastAPI, и она будет **работать как ожидается**. Если есть **обязательный** параметр (без значения по умолчанию), ваш **редактор** сообщит об ошибке, **Python** тоже пожалуется, если вы запустите её без передачи обязательного параметра. Если вы не используете `Annotated`, а применяете **(устаревший) стиль со значением по умолчанию**, то при вызове этой функции без FastAPI в **других местах** вам нужно **помнить** о том, что надо передать аргументы, чтобы всё работало корректно, иначе значения будут не такими, как вы ожидаете (например, вместо `str` будет `QueryInfo` или что-то подобное). И ни редактор, ни Python не будут ругаться при самом вызове функции — ошибка проявится лишь при операциях внутри. @@ -191,7 +191,7 @@ q: str = Query(default="rick") ## Регулярные выражения { #add-regular-expressions } -Вы можете определить <abbr title="Регулярное выражение (regex, regexp) — это последовательность символов, задающая шаблон поиска для строк.">регулярное выражение</abbr> `pattern`, которому должен соответствовать параметр: +Вы можете определить <abbr title="Регулярное выражение (regex, regexp) - это последовательность символов, задающая шаблон поиска для строк.">регулярное выражение</abbr> `pattern`, которому должен соответствовать параметр: {* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} @@ -205,20 +205,6 @@ q: str = Query(default="rick") Теперь вы знаете, что когда они понадобятся, вы сможете использовать их в **FastAPI**. -### `regex` из Pydantic v1 вместо `pattern` { #pydantic-v1-regex-instead-of-pattern } - -До Pydantic версии 2 и до FastAPI 0.100.0 этот параметр назывался `regex`, а не `pattern`, но сейчас он устарел. - -Вы всё ещё можете встретить такой код: - -//// tab | Pydantic v1 - -{* ../../docs_src/query_params_str_validations/tutorial004_regex_an_py310.py hl[11] *} - -//// - -Имейте в виду, что это устарело, и код следует обновить на использование нового параметра `pattern`. 🤓 - ## Значения по умолчанию { #default-values } Конечно, можно использовать и другие значения по умолчанию, не только `None`. @@ -279,7 +265,7 @@ q: Annotated[str | None, Query(min_length=3)] = None http://localhost:8000/items/?q=foo&q=bar ``` -вы получите множественные значения query-параметра `q` (`foo` и `bar`) в виде Python-`list` внутри вашей *функции обработки пути*, в *параметре функции* `q`. +вы получите множественные значения *query-параметров* `q` (`foo` и `bar`) в виде Python-`list` внутри вашей *функции-обработчика пути*, в *параметре функции* `q`. Таким образом, ответ на этот URL будет: @@ -331,7 +317,7 @@ http://localhost:8000/items/ {* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} -/// note | Технические детали +/// note | Примечание Имейте в виду, что в этом случае FastAPI не будет проверять содержимое списка. @@ -345,7 +331,7 @@ http://localhost:8000/items/ Эта информация будет включена в сгенерированную OpenAPI-схему и использована интерфейсами документации и внешними инструментами. -/// note | Технические детали +/// note | Примечание Помните, что разные инструменты могут иметь разный уровень поддержки OpenAPI. @@ -415,7 +401,7 @@ http://127.0.0.1:8000/items/?item-query=foobaritems /// -Например, эта кастомная проверка убеждается, что ID элемента начинается с `isbn-` для номера книги <abbr title="ISBN означает International Standard Book Number – Международный стандартный книжный номер">ISBN</abbr> или с `imdb-` для ID URL фильма на <abbr title="IMDB (Internet Movie Database) — веб‑сайт с информацией о фильмах">IMDB</abbr>: +Например, эта кастомная проверка убеждается, что ID элемента начинается с `isbn-` для номера книги <abbr title="ISBN означает International Standard Book Number - Международный стандартный книжный номер">ISBN</abbr> или с `imdb-` для ID URL фильма на <abbr title="IMDB (Internet Movie Database) - веб‑сайт с информацией о фильмах">IMDB</abbr>: {* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} @@ -455,7 +441,7 @@ http://127.0.0.1:8000/items/?item-query=foobaritems Затем с `random.choice()` можно получить **случайное значение** из списка — то есть кортеж вида `(id, name)`. Это будет что‑то вроде `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`. -После этого мы **распаковываем** эти два значения кортежа в переменные `id` и `name`. +После этого мы **присваиваем эти два значения** кортежа переменным `id` и `name`. Так что, если пользователь не передал ID элемента, он всё равно получит случайную рекомендацию. diff --git a/docs/ru/docs/tutorial/response-model.md b/docs/ru/docs/tutorial/response-model.md index 07308c1db..22a811cd5 100644 --- a/docs/ru/docs/tutorial/response-model.md +++ b/docs/ru/docs/tutorial/response-model.md @@ -6,11 +6,11 @@ {* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} -FastAPI будет использовать этот тип ответа для: +FastAPI будет использовать этот возвращаемый тип, чтобы: -* **Валидации** возвращаемых данных. - * Если данные невалидны (например, отсутствует поле), это означает, что код *вашего* приложения работает некорректно и возвращает не то, что должен. В таком случае будет возвращена ошибка сервера вместо неправильных данных. Так вы и ваши клиенты можете быть уверены, что получите ожидаемые данные и ожидаемую структуру. -* Добавления **JSON Schema** для ответа в OpenAPI *операции пути*. +* **Валидировать** возвращаемые данные. + * Если данные невалидны (например, отсутствует поле), это означает, что код *вашего* приложения работает некорректно и возвращает не то, что должен. В таком случае будет возвращена ошибка сервера вместо неправильных данных. Так вы и ваши клиенты можете быть уверены, что получите ожидаемые данные и ожидаемую структуру данных. +* Добавить **JSON Schema** для ответа в OpenAPI *операции пути*. * Это будет использовано **автоматической документацией**. * Это также будет использовано инструментами автоматической генерации клиентского кода. @@ -23,7 +23,7 @@ FastAPI будет использовать этот тип ответа для: Бывают случаи, когда вам нужно или хочется возвращать данные, которые не в точности соответствуют объявленному типу. -Например, вы можете хотеть **возвращать словарь (dict)** или объект из базы данных, но **объявить его как Pydantic-модель**. Тогда Pydantic-модель выполнит документирование данных, валидацию и т.п. для объекта, который вы вернули (например, словаря или объекта из базы данных). +Например, вы можете хотеть **возвращать словарь** или объект из базы данных, но **объявить его как Pydantic-модель**. Тогда Pydantic-модель выполнит документирование данных, валидацию и т.п. для объекта, который вы вернули (например, словаря или объекта из базы данных). Если вы добавите аннотацию возвращаемого типа, инструменты и редакторы кода начнут жаловаться (и будут правы), что функция возвращает тип (например, dict), отличный от объявленного (например, Pydantic-модель). @@ -47,13 +47,13 @@ FastAPI будет использовать этот тип ответа для: `response_model` принимает тот же тип, что вы бы объявили для поля Pydantic-модели, то есть это может быть одна Pydantic-модель, а может быть, например, `list` Pydantic-моделей, как `List[Item]`. -FastAPI будет использовать `response_model` для документации, валидации и т. п., а также для **конвертации и фильтрации выходных данных** к объявленному типу. +FastAPI будет использовать этот `response_model` для документирования, валидации данных и т.п., а также для **конвертации и фильтрации выходных данных** к объявленному типу. /// tip | Совет -Если у вас в редакторе кода, mypy и т. п. включены строгие проверки типов, вы можете объявить возвращаемый тип функции как `Any`. +Если у вас в редакторе кода, mypy и т.п. включены строгие проверки типов, вы можете объявить возвращаемый тип функции как `Any`. -Так вы сообщите редактору, что намеренно возвращаете что угодно. Но FastAPI всё равно выполнит документацию данных, валидацию, фильтрацию и т.д. с помощью `response_model`. +Так вы сообщите редактору, что намеренно возвращаете что угодно. Но FastAPI всё равно выполнит документирование, валидацию, фильтрацию данных и т.д. с помощью `response_model`. /// @@ -61,7 +61,7 @@ FastAPI будет использовать `response_model` для докуме Если вы объявите и возвращаемый тип, и `response_model`, приоритет будет у `response_model`, именно его использует FastAPI. -Так вы можете добавить корректные аннотации типов к своим функциям, даже если фактически возвращаете тип, отличный от модели ответа, чтобы ими пользовались редактор и инструменты вроде mypy. И при этом FastAPI продолжит выполнять валидацию данных, документацию и т.д. с использованием `response_model`. +Так вы можете добавить корректные аннотации типов к своим функциям, даже если фактически возвращаете тип, отличный от модели ответа, чтобы ими пользовались редактор кода и инструменты вроде mypy. И при этом FastAPI продолжит выполнять валидацию данных, документацию и т.д. с использованием `response_model`. Вы также можете указать `response_model=None`, чтобы отключить создание модели ответа для данной *операции пути*. Это может понадобиться, если вы добавляете аннотации типов для вещей, не являющихся валидными полями Pydantic. Пример вы увидите ниже. @@ -75,7 +75,7 @@ FastAPI будет использовать `response_model` для докуме Чтобы использовать `EmailStr`, сначала установите <a href="https://github.com/JoshData/python-email-validator" class="external-link" target="_blank">`email-validator`</a>. -Создайте [виртуальное окружение](../virtual-environments.md){.internal-link target=_blank}, активируйте его и затем установите пакет, например: +Убедитесь, что вы создали [виртуальное окружение](../virtual-environments.md){.internal-link target=_blank}, активировали его, а затем установите пакет, например: ```console $ pip install email-validator @@ -105,7 +105,7 @@ $ pip install "pydantic[email]" /// -## Добавить модель для ответа { #add-an-output-model } +## Добавить выходную модель { #add-an-output-model } Вместо этого мы можем создать входную модель с паролем в открытом виде и выходную модель без него: @@ -123,7 +123,7 @@ $ pip install "pydantic[email]" ### `response_model` или возвращаемый тип { #response-model-or-return-type } -В этом случае, поскольку две модели различаются, если бы мы аннотировали возвращаемый тип функции как `UserOut`, редактор и инструменты пожаловались бы, что мы возвращаем неверный тип, так как это разные классы. +В этом случае, поскольку две модели различаются, если бы мы аннотировали возвращаемый тип функции как `UserOut`, редактор кода и инструменты пожаловались бы, что мы возвращаем неверный тип, так как это разные классы. Поэтому в этом примере мы должны объявить тип ответа в параметре `response_model`. @@ -135,33 +135,33 @@ $ pip install "pydantic[email]" Мы хотим, чтобы FastAPI продолжал **фильтровать** данные с помощью модели ответа. Так что, даже если функция возвращает больше данных, в ответ будут включены только поля, объявленные в модели ответа. -В предыдущем примере, поскольку классы были разными, нам пришлось использовать параметр `response_model`. Но это также означает, что мы теряем поддержку от редактора и инструментов, проверяющих возвращаемый тип функции. +В предыдущем примере, поскольку классы были разными, нам пришлось использовать параметр `response_model`. Но это также означает, что мы теряем поддержку от редактора кода и инструментов, проверяющих возвращаемый тип функции. Однако в большинстве таких случаев нам нужно лишь **отфильтровать/убрать** некоторые данные, как в этом примере. -И в этих случаях мы можем использовать классы и наследование, чтобы воспользоваться **аннотациями типов** функций для лучшей поддержки в редакторе и инструментах и при этом получить **фильтрацию данных** от FastAPI. +И в этих случаях мы можем использовать классы и наследование, чтобы воспользоваться **аннотациями типов** функций для лучшей поддержки в редакторе кода и инструментах и при этом получить **фильтрацию данных** от FastAPI. {* ../../docs_src/response_model/tutorial003_01_py310.py hl[7:10,13:14,18] *} -Так мы получаем поддержку инструментов (редакторы, mypy) — код корректен с точки зрения типов — и одновременно получаем фильтрацию данных от FastAPI. +Так мы получаем поддержку инструментов — редакторов кода и mypy, так как этот код корректен с точки зрения типов — и одновременно получаем фильтрацию данных от FastAPI. Как это работает? Давайте разберёмся. 🤓 ### Аннотации типов и инструменты { #type-annotations-and-tooling } -Сначала посмотрим, как это увидят редакторы, mypy и другие инструменты. +Сначала посмотрим, как это увидят редактор кода, mypy и другие инструменты. -`BaseUser` содержит базовые поля. Затем `UserIn` наследуется от `BaseUser` и добавляет поле `password`, то есть он включает все поля обеих моделей. +`BaseUser` содержит базовые поля. Затем `UserIn` наследуется от `BaseUser` и добавляет поле `password`, то есть он будет включать все поля обеих моделей. Мы аннотируем возвращаемый тип функции как `BaseUser`, но фактически возвращаем экземпляр `UserIn`. -Редактор, mypy и другие инструменты не будут возражать, потому что с точки зрения типов `UserIn` — подкласс `BaseUser`, что означает, что это *валидный* тип везде, где ожидается что-то, являющееся `BaseUser`. +Редактор кода, mypy и другие инструменты не будут возражать, потому что с точки зрения типов `UserIn` — подкласс `BaseUser`, что означает, что это *валидный* тип везде, где ожидается что-то, являющееся `BaseUser`. ### Фильтрация данных FastAPI { #fastapi-data-filtering } -Теперь, для FastAPI: он увидит возвращаемый тип и убедится, что то, что вы возвращаете, включает **только** поля, объявленные в этом типе. +Теперь для FastAPI: он увидит возвращаемый тип и убедится, что то, что вы возвращаете, включает **только** поля, объявленные в этом типе. -FastAPI делает несколько вещей внутри вместе с Pydantic, чтобы гарантировать, что те же правила наследования классов не используются для фильтрации возвращаемых данных, иначе вы могли бы вернуть гораздо больше данных, чем ожидали. +FastAPI делает несколько вещей внутри вместе с Pydantic, чтобы гарантировать, что те же правила наследования классов не используются для фильтрации возвращаемых данных, иначе вы могли бы в итоге вернуть намного больше данных, чем ожидали. Таким образом вы получаете лучшее из обоих миров: аннотации типов с **поддержкой инструментов** и **фильтрацию данных**. @@ -171,17 +171,17 @@ FastAPI делает несколько вещей внутри вместе с <img src="/img/tutorial/response-model/image01.png"> -И обе модели используются в интерактивной документации API: +И обе модели будут использоваться в интерактивной документации API: <img src="/img/tutorial/response-model/image02.png"> ## Другие аннотации возвращаемых типов { #other-return-type-annotations } -Бывают случаи, когда вы возвращаете что-то, что не является валидным полем Pydantic, и аннотируете это в функции только ради поддержки инструментов (редактор, mypy и т. д.). +Бывают случаи, когда вы возвращаете что-то, что не является валидным полем Pydantic, и аннотируете это в функции только ради поддержки инструментов (редактор кода, mypy и т.д.). ### Возврат Response напрямую { #return-a-response-directly } -Самый распространённый случай — [возвращать Response напрямую, как описано далее в разделах для продвинутых](../advanced/response-directly.md){.internal-link target=_blank}. +Самый распространённый случай — [возвращать Response напрямую, как описано далее в разделах документации для продвинутых](../advanced/response-directly.md){.internal-link target=_blank}. {* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} @@ -195,7 +195,7 @@ FastAPI делает несколько вещей внутри вместе с {* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} -Это тоже сработает, так как `RedirectResponse` — подкласс `Response`, и FastAPI автоматически обработает этот случай. +Это тоже сработает, так как `RedirectResponse` — подкласс `Response`, и FastAPI автоматически обработает этот простой случай. ### Некорректные аннотации возвращаемых типов { #invalid-return-type-annotations } @@ -209,15 +209,15 @@ FastAPI делает несколько вещей внутри вместе с ### Отключить модель ответа { #disable-response-model } -Продолжая пример выше, вы можете не хотеть использовать стандартную валидацию данных, документацию, фильтрацию и т.д., выполняемые FastAPI. +Продолжая пример выше, вы можете не хотеть использовать стандартные валидацию данных, документирование, фильтрацию и т.п., выполняемые FastAPI. -Но при этом вы можете хотеть сохранить аннотацию возвращаемого типа в функции, чтобы пользоваться поддержкой инструментов (редакторы, проверки типов вроде mypy). +Но при этом вы можете хотеть сохранить аннотацию возвращаемого типа в функции, чтобы пользоваться поддержкой инструментов вроде редакторов кода и инструментов проверки типов (например, mypy). В этом случае вы можете отключить генерацию модели ответа, установив `response_model=None`: {* ../../docs_src/response_model/tutorial003_05_py310.py hl[7] *} -Так FastAPI пропустит генерацию модели ответа, и вы сможете использовать любые аннотации возвращаемых типов, не влияя на ваше приложение FastAPI. 🤓 +Так FastAPI пропустит генерацию модели ответа, и вы сможете использовать любые аннотации возвращаемых типов, которые вам нужны, без влияния на ваше приложение FastAPI. 🤓 ## Параметры кодирования модели ответа { #response-model-encoding-parameters } @@ -252,20 +252,6 @@ FastAPI делает несколько вещей внутри вместе с /// info | Информация -В Pydantic v1 метод назывался `.dict()`, в Pydantic v2 он был помечен как устаревший (но всё ещё поддерживается) и переименован в `.model_dump()`. - -Примеры здесь используют `.dict()` для совместимости с Pydantic v1, но если вы используете Pydantic v2, применяйте `.model_dump()`. - -/// - -/// info | Информация - -FastAPI использует метод `.dict()` у Pydantic-моделей с <a href="https://docs.pydantic.dev/1.10/usage/exporting_models/#modeldict" class="external-link" target="_blank">параметром `exclude_unset`</a>, чтобы добиться такого поведения. - -/// - -/// info | Информация - Вы также можете использовать: * `response_model_exclude_defaults=True` @@ -312,7 +298,7 @@ FastAPI достаточно умен (на самом деле, это Pydantic Обратите внимание, что значения по умолчанию могут быть любыми, не только `None`. -Это может быть список (`[]`), число с плавающей точкой `10.5` и т. д. +Это может быть список (`[]`), число с плавающей точкой `10.5` и т.д. /// @@ -346,7 +332,7 @@ FastAPI достаточно умен (на самом деле, это Pydantic #### Использование `list` вместо `set` { #using-lists-instead-of-sets } -Если вы забыли использовать `set` и применили `list` или `tuple`, FastAPI всё равно преобразует это в `set`, и всё будет работать корректно: +Если вы забыли использовать `set` и применили `list` или `tuple` вместо него, FastAPI всё равно преобразует это в `set`, и всё будет работать корректно: {* ../../docs_src/response_model/tutorial006_py310.py hl[29,35] *} diff --git a/docs/ru/docs/tutorial/schema-extra-example.md b/docs/ru/docs/tutorial/schema-extra-example.md index 5891f0d12..e4a97c880 100644 --- a/docs/ru/docs/tutorial/schema-extra-example.md +++ b/docs/ru/docs/tutorial/schema-extra-example.md @@ -8,36 +8,14 @@ Вы можете объявить `examples` для модели Pydantic, которые будут добавлены в сгенерированную JSON Schema. -//// tab | Pydantic v2 - {* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *} - -//// - Эта дополнительная информация будет добавлена как есть в выходную **JSON Schema** этой модели и будет использоваться в документации API. -//// tab | Pydantic v2 - -В Pydantic версии 2 вы будете использовать атрибут `model_config`, который принимает `dict`, как описано в <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">Документации Pydantic: Конфигурация</a>. +Вы можете использовать атрибут `model_config`, который принимает `dict`, как описано в <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">Документации Pydantic: Конфигурация</a>. Вы можете задать `"json_schema_extra"` с `dict`, содержащим любые дополнительные данные, которые вы хотите видеть в сгенерированной JSON Schema, включая `examples`. -//// - -//// tab | Pydantic v1 - -В Pydantic версии 1 вы будете использовать внутренний класс `Config` и `schema_extra`, как описано в <a href="https://docs.pydantic.dev/1.10/usage/schema/#schema-customization" class="external-link" target="_blank">Документации Pydantic: Настройка схемы</a>. - -Вы можете задать `schema_extra` со `dict`, содержащим любые дополнительные данные, которые вы хотите видеть в сгенерированной JSON Schema, включая `examples`. - -//// - /// tip | Подсказка Вы можете использовать тот же приём, чтобы расширить JSON Schema и добавить свою собственную дополнительную информацию. @@ -124,7 +102,7 @@ OpenAPI 3.1.0 (используется начиная с FastAPI 0.99.0) доб Ключи `dict` идентифицируют каждый пример, а каждое значение — это ещё один `dict`. -Каждый конкретный пример‑`dict` в `examples` может содержать: +Каждый конкретный пример `dict` в `examples` может содержать: * `summary`: Краткое описание примера. * `description`: Подробное описание, которое может содержать текст в Markdown. @@ -135,7 +113,7 @@ OpenAPI 3.1.0 (используется начиная с FastAPI 0.99.0) доб {* ../../docs_src/schema_extra_example/tutorial005_an_py310.py hl[23:49] *} -### OpenAPI-примеры в UI документации { #openapi-examples-in-the-docs-ui } +### OpenAPI-примеры в UI документации { #openapi-examples-in-the-docs-ui } С `openapi_examples`, добавленным в `Body()`, страница `/docs` будет выглядеть так: @@ -213,7 +191,7 @@ OpenAPI также добавила поля `example` и `examples` в друг ### Swagger UI и специфичные для OpenAPI `examples` { #swagger-ui-and-openapi-specific-examples } -Раньше, поскольку Swagger UI не поддерживал несколько примеров JSON Schema (по состоянию на 2023-08-26), у пользователей не было способа показать несколько примеров в документации. +Теперь, поскольку Swagger UI не поддерживал несколько примеров JSON Schema (по состоянию на 2023-08-26), у пользователей не было способа показать несколько примеров в документации. Чтобы решить это, FastAPI `0.103.0` **добавил поддержку** объявления того же старого, **специфичного для OpenAPI**, поля `examples` с новым параметром `openapi_examples`. 🤓 From e0abd210f64110de77c09909245e9652a4fec78e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:03:31 +0000 Subject: [PATCH 100/243] =?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 33926e3af..f4572ffe8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Translations +* 🌐 Update translations for ru (update-outdated). PR [#14693](https://github.com/fastapi/fastapi/pull/14693) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for pt (update-outdated). PR [#14724](https://github.com/fastapi/fastapi/pull/14724) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update Korean LLM prompt. PR [#14740](https://github.com/fastapi/fastapi/pull/14740) by [@hard-coders](https://github.com/hard-coders). * 🌐 Improve LLM prompt for Turkish translations. PR [#14728](https://github.com/fastapi/fastapi/pull/14728) by [@Kadermiyanyedi](https://github.com/Kadermiyanyedi). From b9b75ba5f1a51c2edcae3df4dffa2b0f1a2353d0 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:07:05 +0300 Subject: [PATCH 101/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20LLM=20prompt=20?= =?UTF-8?q?for=20Russian=20translations=20(#14733)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add some specific translations --- docs/ru/llm-prompt.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/ru/llm-prompt.md b/docs/ru/llm-prompt.md index 6a437bdd1..9131a5d3b 100644 --- a/docs/ru/llm-prompt.md +++ b/docs/ru/llm-prompt.md @@ -90,5 +90,12 @@ For the following technical terms, use these specific translations to ensure con * serve (meaning providing access to something): «отдавать» (or `предоставлять доступ к`) * recap (noun): резюме * utility function: вспомогательная функция +* fast to code: позволяет быстро писать код +* Tutorial - User Guide: Учебник - Руководство пользователя +* submodule: подмодуль +* subpackage: подпакет +* router: роутер +* building, deploying, accessing (when describing features of FastAPI Cloud): созданиe образа, развертывание и доступ +* type checker tool: инструмент проверки типов Do not add whitespace in `т.д.`, `т.п.`. From 6e47171e9cb79a4616014cb9fce5b88912a820ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 07:07:28 +0000 Subject: [PATCH 102/243] =?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 f4572ffe8..62b611f8a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Translations +* 🌐 Update LLM prompt for Russian translations. PR [#14733](https://github.com/fastapi/fastapi/pull/14733) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for ru (update-outdated). PR [#14693](https://github.com/fastapi/fastapi/pull/14693) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for pt (update-outdated). PR [#14724](https://github.com/fastapi/fastapi/pull/14724) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update Korean LLM prompt. PR [#14740](https://github.com/fastapi/fastapi/pull/14740) by [@hard-coders](https://github.com/hard-coders). From 509afeb475e602bd11f2ffa165ccd0ed3d10a19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Thu, 22 Jan 2026 01:27:31 -0800 Subject: [PATCH 103/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20de=20(update-outdated)=20(#14690)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🌐 Update translations for de (update-outdated) * Apply suggestions from code review --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- docs/de/docs/_llm-test.md | 2 +- docs/de/docs/tutorial/bigger-applications.md | 26 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/de/docs/_llm-test.md b/docs/de/docs/_llm-test.md index bc7ce363c..0b95fe3a8 100644 --- a/docs/de/docs/_llm-test.md +++ b/docs/de/docs/_llm-test.md @@ -189,7 +189,7 @@ Siehe Abschnitt `### Links` im allgemeinen Prompt in `scripts/translate.py`. //// -## HTML „abbr“-Elemente { #html-abbr-elements } +## HTML-„abbr“-Elemente { #html-abbr-elements } //// tab | Test diff --git a/docs/de/docs/tutorial/bigger-applications.md b/docs/de/docs/tutorial/bigger-applications.md index 963baf44d..d478d77c2 100644 --- a/docs/de/docs/tutorial/bigger-applications.md +++ b/docs/de/docs/tutorial/bigger-applications.md @@ -56,19 +56,19 @@ from app.routers import items Die gleiche Dateistruktur mit Kommentaren: -``` +```bash . -├── app # „app“ ist ein Python-Package -│   ├── __init__.py # diese Datei macht „app“ zu einem „Python-Package“ -│   ├── main.py # „main“-Modul, z. B. import app.main -│   ├── dependencies.py # „dependencies“-Modul, z. B. import app.dependencies -│   └── routers # „routers“ ist ein „Python-Subpackage“ -│   │ ├── __init__.py # macht „routers“ zu einem „Python-Subpackage“ -│   │ ├── items.py # „items“-Submodul, z. B. import app.routers.items -│   │ └── users.py # „users“-Submodul, z. B. import app.routers.users -│   └── internal # „internal“ ist ein „Python-Subpackage“ -│   ├── __init__.py # macht „internal“ zu einem „Python-Subpackage“ -│   └── admin.py # „admin“-Submodul, z. B. import app.internal.admin +├── app # "app" ist ein Python-Package +│   ├── __init__.py # diese Datei macht "app" zu einem "Python-Package" +│   ├── main.py # "main"-Modul, z. B. import app.main +│   ├── dependencies.py # "dependencies"-Modul, z. B. import app.dependencies +│   └── routers # "routers" ist ein "Python-Subpackage" +│   │ ├── __init__.py # macht "routers" zu einem "Python-Subpackage" +│   │ ├── items.py # "items"-Submodul, z. B. import app.routers.items +│   │ └── users.py # "users"-Submodul, z. B. import app.routers.users +│   └── internal # "internal" ist ein "Python-Subpackage" +│   ├── __init__.py # macht "internal" zu einem "Python-Subpackage" +│   └── admin.py # "admin"-Submodul, z. B. import app.internal.admin ``` ## `APIRouter` { #apirouter } @@ -479,7 +479,7 @@ $ fastapi dev app/main.py </div> -und öffnen Sie die Dokumentation unter <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. +Und öffnen Sie die Dokumentation unter <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. Sie sehen die automatische API-Dokumentation, einschließlich der Pfade aller Submodule, mit den richtigen Pfaden (und Präfixen) und den richtigen Tags: From f1a39cab12c615ba50b21f32159e523baa560424 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:27:58 +0000 Subject: [PATCH 104/243] =?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 62b611f8a..1cd2b887f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Translations +* 🌐 Update translations for de (update-outdated). PR [#14690](https://github.com/fastapi/fastapi/pull/14690) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update LLM prompt for Russian translations. PR [#14733](https://github.com/fastapi/fastapi/pull/14733) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for ru (update-outdated). PR [#14693](https://github.com/fastapi/fastapi/pull/14693) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for pt (update-outdated). PR [#14724](https://github.com/fastapi/fastapi/pull/14724) by [@tiangolo](https://github.com/tiangolo). From 74cc27fd5aa1e9376d30105fe014ab8b2b1319b1 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem <svlandeg@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:32:34 +0100 Subject: [PATCH 105/243] =?UTF-8?q?=F0=9F=94=A7=20Ensure=20that=20an=20edi?= =?UTF-8?q?t=20to=20`uv.lock`=20gets=20the=20`internal`=20label=20(#14759)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add uv.lock to files for labeling the PR with 'internal' --- .github/labeler.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index cdaefbf2d..57c5e1120 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -31,6 +31,7 @@ internal: - .pre-commit-config.yaml - pdm_build.py - requirements*.txt + - uv.lock - docs/en/data/sponsors.yml - docs/en/overrides/main.html - all-globs-to-all-files: From 597b435ae7f0c4694f654bce6c7c27206aa690ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:33:00 +0000 Subject: [PATCH 106/243] =?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 1cd2b887f..f1321b780 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -36,6 +36,7 @@ hide: ### Internal +* 🔧 Ensure that an edit to `uv.lock` gets the `internal` label. PR [#14759](https://github.com/fastapi/fastapi/pull/14759) by [@svlandeg](https://github.com/svlandeg). * 🔧 Update sponsors: remove Requestly. PR [#14735](https://github.com/fastapi/fastapi/pull/14735) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, LambdaTest changes to TestMu AI. PR [#14734](https://github.com/fastapi/fastapi/pull/14734) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/cache from 4 to 5. PR [#14511](https://github.com/fastapi/fastapi/pull/14511) by [@dependabot[bot]](https://github.com/apps/dependabot). From eaf07c5d849ce4564b8708b4965a232fd026f1fe Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Sun, 25 Jan 2026 00:16:10 +0300 Subject: [PATCH 107/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20ko=20(update=20outdated,=20found=20by=20fixer=20tool)=20(?= =?UTF-8?q?#14738)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update outdated pages found by fixer tool * Re-translate with updated prompt (fixed translation for `you`) * Re-translate with `gtpt-5` model * Re-translate with new preferred translation for `burger` * Re-translate with new preferred translations for `app` and `command` --- docs/ko/docs/async.md | 372 ++++++++++++++------------ docs/ko/docs/fastapi-cli.md | 96 +++---- docs/ko/docs/features.md | 176 ++++++------ docs/ko/docs/help-fastapi.md | 199 +++++++------- docs/ko/docs/history-design-future.md | 82 +++--- 5 files changed, 468 insertions(+), 457 deletions(-) diff --git a/docs/ko/docs/async.md b/docs/ko/docs/async.md index ec503d540..36f1ca6bf 100644 --- a/docs/ko/docs/async.md +++ b/docs/ko/docs/async.md @@ -1,18 +1,18 @@ -# 동시성과 async / await +# 동시성과 async / await { #concurrency-and-async-await } -*경로 작동 함수*에서의 `async def` 문법에 대한 세부사항과 비동기 코드, 동시성 및 병렬성에 대한 배경 +*경로 처리 함수*에서의 `async def` 문법에 대한 세부사항과 비동기 코드, 동시성 및 병렬성에 대한 배경 -## 바쁘신 경우 +## 바쁘신가요? { #in-a-hurry } -<strong>요약</strong> +<abbr title="too long; didn't read - 너무 길어서 읽지 않음"><strong>TL;DR:</strong></abbr> -다음과 같이 `await`를 사용해 호출하는 제3의 라이브러리를 사용하는 경우: +다음과 같이 `await`를 사용해 호출하라고 안내하는 제3자 라이브러리를 사용하는 경우: ```Python results = await some_library() ``` -다음처럼 *경로 작동 함수*를 `async def`를 사용해 선언하십시오: +다음처럼 *경로 처리 함수*를 `async def`를 사용해 선언하십시오: ```Python hl_lines="2" @app.get('/') @@ -29,7 +29,7 @@ async def read_results(): --- -데이터베이스, API, 파일시스템 등과 의사소통하는 제3의 라이브러리를 사용하고, 그것이 `await`를 지원하지 않는 경우(현재 거의 모든 데이터베이스 라이브러리가 그러합니다), *경로 작동 함수*를 일반적인 `def`를 사용해 선언하십시오: +데이터베이스, API, 파일시스템 등과 의사소통하는 제3자 라이브러리를 사용하고, 그것이 `await` 사용을 지원하지 않는 경우(현재 대부분의 데이터베이스 라이브러리가 그러합니다), *경로 처리 함수*를 일반적인 `def`를 사용해 선언하십시오: ```Python hl_lines="2" @app.get('/') @@ -40,23 +40,23 @@ def results(): --- -만약 당신의 응용프로그램이 (어째서인지) 다른 무엇과 의사소통하고 그것이 응답하기를 기다릴 필요가 없다면 `async def`를 사용하십시오. +만약 여러분의 애플리케이션이 (어째서인지) 다른 어떤 것과도 통신하고 그 응답을 기다릴 필요가 없다면, 내부에서 `await`를 사용할 필요가 없더라도 `async def`를 사용하세요. --- -모르겠다면, 그냥 `def`를 사용하십시오. +잘 모르겠다면, 일반적인 `def`를 사용하세요. --- -**참고**: *경로 작동 함수*에서 필요한만큼 `def`와 `async def`를 혼용할 수 있고, 가장 알맞은 것을 선택해서 정의할 수 있습니다. FastAPI가 자체적으로 알맞은 작업을 수행할 것입니다. +**참고**: *경로 처리 함수*에서 필요한 만큼 `def`와 `async def`를 혼용할 수 있으며, 각각에 대해 가장 알맞은 옵션을 선택해 정의하면 됩니다. FastAPI가 올바르게 처리합니다. -어찌되었든, 상기 어떠한 경우라도, FastAPI는 여전히 비동기적으로 작동하고 매우 빠릅니다. +어쨌든 위의 어떤 경우에서도 FastAPI는 여전히 비동기적으로 동작하며 매우 빠릅니다. -그러나 상기 작업을 수행함으로써 어느 정도의 성능 최적화가 가능합니다. +하지만 위의 단계를 따르면, 몇 가지 성능 최적화를 할 수 있습니다. -## 기술적 세부사항 +## 기술적 세부사항 { #technical-details } -최신 파이썬 버전은 `async`와 `await` 문법과 함께 **“코루틴”**이라고 하는 것을 사용하는 **“비동기 코드”**를 지원합니다. +최신 파이썬 버전은 **“코루틴”**이라고 하는 것을 사용하는 **“비동기 코드”**를 **`async` 및 `await`** 문법과 함께 지원합니다. 아래 섹션들에서 해당 문장을 부분별로 살펴보겠습니다: @@ -64,251 +64,283 @@ def results(): * **`async`와 `await`** * **코루틴** -## 비동기 코드 +## 비동기 코드 { #asynchronous-code } -비동기 코드란 언어 💬 가 코드의 어느 한 부분에서, 컴퓨터 / 프로그램🤖에게 *다른 무언가*가 어딘가에서 끝날 때까지 기다려야한다고 말하는 방식입니다. *다른 무언가*가 “느린-파일" 📝 이라고 불린다고 가정해봅시다. +비동기 코드는 언어 💬 가 코드의 어느 한 부분에서 컴퓨터/프로그램 🤖 에게, 어느 시점에는 어딘가에서 *다른 무언가*가 끝날 때까지 기다려야 한다고 말할 수 있는 방법이 있다는 의미입니다. 그 *다른 무언가*를 "slow-file" 📝 이라고 해보겠습니다. -따라서 “느린-파일” 📝이 끝날때까지 컴퓨터는 다른 작업을 수행할 수 있습니다. +따라서 그 시간 동안 컴퓨터는 "slow-file" 📝 이 끝나는 동안 다른 작업을 하러 갈 수 있습니다. -그 다음 컴퓨터 / 프로그램 🤖 은 다시 기다리고 있기 때문에 기회가 있을 때마다 다시 돌아오거나, 혹은 당시에 수행해야하는 작업들이 완료될 때마다 다시 돌아옵니다. 그리고 그것 🤖 은 기다리고 있던 작업 중 어느 것이 이미 완료되었는지, 그것 🤖 이 해야하는 모든 작업을 수행하면서 확인합니다. +그 다음 컴퓨터/프로그램 🤖 은 다시 기다리는 중이기 때문에 기회가 있을 때마다 돌아오거나, 혹은 그 시점에 해야 할 작업을 모두 끝낼 때마다 돌아옵니다. 그리고 기다리던 작업 중 이미 끝난 것이 있는지 확인하면서, 해야 했던 작업을 수행합니다. -다음으로, 그것 🤖 은 완료할 첫번째 작업에 착수하고(우리의 "느린-파일" 📝 이라고 가정합시다) 그에 대해 수행해야하는 작업을 계속합니다. +다음으로, 완료된 첫 번째 작업(우리의 "slow-file" 📝 이라고 해보겠습니다)을 가져와서, 그에 대해 해야 했던 작업을 계속합니다. -"다른 무언가를 기다리는 것"은 일반적으로 비교적 "느린" (프로세서와 RAM 메모리 속도에 비해) <abbr title="Input and Output">I/O</abbr> 작업을 의미합니다. 예를 들면 다음의 것들을 기다리는 것입니다: +이 "다른 무언가를 기다리는 것"은 일반적으로 프로세서와 RAM 메모리 속도에 비해 상대적으로 "느린" <abbr title="Input and Output - 입력/출력">I/O</abbr> 작업을 의미합니다. 예를 들어 다음을 기다리는 것입니다: -* 네트워크를 통해 클라이언트로부터 전송되는 데이터 -* 네트워크를 통해 클라이언트가 수신할, 당신의 프로그램으로부터 전송되는 데이터 -* 시스템이 읽고 프로그램에 전달할 디스크 내의 파일 내용 -* 당신의 프로그램이 시스템에 전달하는, 디스크에 작성될 내용 +* 네트워크를 통해 클라이언트가 데이터를 보내는 것 +* 네트워크를 통해 클라이언트가 여러분의 프로그램이 보낸 데이터를 받는 것 +* 시스템이 디스크의 파일 내용을 읽어서 프로그램에 전달하는 것 +* 프로그램이 시스템에 전달한 내용을 디스크에 쓰는 것 * 원격 API 작업 -* 완료될 데이터베이스 작업 -* 결과를 반환하는 데이터베이스 쿼리 -* 기타 +* 데이터베이스 작업이 완료되는 것 +* 데이터베이스 쿼리가 결과를 반환하는 것 +* 기타 등등 -수행 시간의 대부분이 <abbr title="Input and Output">I/O</abbr> 작업을 기다리는데에 소요되기 때문에, "I/O에 묶인" 작업이라고 불립니다. +실행 시간의 대부분이 <abbr title="Input and Output - 입력/출력">I/O</abbr> 작업을 기다리는 데 소비되기 때문에, 이를 "I/O bound" 작업이라고 부릅니다. -이것은 "비동기"라고 불리는데 컴퓨터 / 프로그램이 작업 결과를 가지고 일을 수행할 수 있도록, 느린 작업에 "동기화"되어 아무것도 하지 않으면서 작업이 완료될 정확한 시점만을 기다릴 필요가 없기 때문입니다. +이것은 컴퓨터/프로그램이 느린 작업과 "동기화"되어, 아무것도 하지 않은 채 그 작업이 끝나는 정확한 시점만 기다렸다가 결과를 가져와 일을 계속할 필요가 없기 때문에 "비동기"라고 불립니다. -이 대신에, "비동기" 시스템에서는, 작업은 일단 완료되면, 컴퓨터 / 프로그램이 수행하고 있는 일을 완료하고 이후 다시 돌아와서 그것의 결과를 받아 이를 사용해 작업을 지속할 때까지 잠시 (몇 마이크로초) 대기할 수 있습니다. +대신 "비동기" 시스템에서는, 작업이 끝나면 컴퓨터/프로그램이 하러 갔던 일을 마칠 때까지 잠시(몇 마이크로초) 줄에서 기다렸다가, 다시 돌아와 결과를 받아 이를 사용해 작업을 계속할 수 있습니다. -"동기"("비동기"의 반대)는 컴퓨터 / 프로그램이 상이한 작업들간 전환을 하기 전에 그것이 대기를 동반하게 될지라도 모든 순서를 따르기 때문에 "순차"라는 용어로도 흔히 불립니다. +"동기"(“비동기”의 반대)는 보통 "순차"라는 용어로도 불리는데, 컴퓨터/프로그램이 다른 작업으로 전환하기 전에 모든 단계를 순서대로 따르기 때문이며, 그 단계들에 기다림이 포함되어 있더라도 마찬가지입니다. -### 동시성과 버거 +### 동시성과 햄버거 { #concurrency-and-burgers } -위에서 설명한 **비동기** 코드에 대한 개념은 종종 **"동시성"**이라고도 불립니다. 이것은 **"병렬성"**과는 다릅니다. +위에서 설명한 **비동기** 코드에 대한 개념은 때때로 **"동시성"**이라고도 불립니다. 이는 **"병렬성"**과는 다릅니다. -**동시성**과 **병렬성**은 모두 "동시에 일어나는 서로 다른 일들"과 관련이 있습니다. +**동시성**과 **병렬성**은 모두 "대략 같은 시간에 일어나는 서로 다른 일들"과 관련이 있습니다. 하지만 *동시성*과 *병렬성*의 세부적인 개념에는 꽤 차이가 있습니다. -차이를 확인하기 위해, 다음의 버거에 대한 이야기를 상상해보십시오: +차이를 보기 위해, 다음의 햄버거 이야기를 상상해보세요: -### 동시 버거 +### 동시 햄버거 { #concurrent-burgers } -당신은 짝사랑 상대 😍 와 패스트푸드 🍔 를 먹으러 갔습니다. 당신은 점원 💁 이 당신 앞에 있는 사람들의 주문을 받을 동안 줄을 서서 기다리고 있습니다. +여러분은 짝사랑 상대와 패스트푸드를 먹으러 갔고, 점원이 여러분 앞 사람들의 주문을 받는 동안 줄을 서서 기다립니다. 😍 -이제 당신의 순서가 되어서, 당신은 당신과 짝사랑 상대 😍 를 위한 두 개의 고급스러운 버거 🍔 를 주문합니다. +<img src="/img/async/concurrent-burgers/concurrent-burgers-01.png" class="illustration"> -당신이 돈을 냅니다 💸. +이제 여러분 차례가 되어, 여러분과 짝사랑 상대를 위해 매우 고급스러운 햄버거 2개를 주문합니다. 🍔🍔 -점원 💁 은 주방 👨‍🍳 에 요리를 하라고 전달하고, 따라서 그들은 당신의 버거 🍔 를 준비해야한다는 사실을 알게됩니다(그들이 지금은 당신 앞 고객들의 주문을 준비하고 있을지라도 말입니다). +<img src="/img/async/concurrent-burgers/concurrent-burgers-02.png" class="illustration"> -점원 💁 은 당신의 순서가 적힌 번호표를 줍니다. +점원은 주방의 요리사에게 무언가를 말해, (지금은 앞선 손님들의 주문을 준비하고 있더라도) 여러분의 햄버거를 준비해야 한다는 것을 알게 합니다. -기다리는 동안, 당신은 짝사랑 상대 😍 와 함께 테이블을 고르고, 자리에 앉아 오랫동안 (당신이 주문한 버거는 꽤나 고급스럽기 때문에 준비하는데 시간이 조금 걸립니다 ✨🍔✨) 대화를 나눕니다. +<img src="/img/async/concurrent-burgers/concurrent-burgers-03.png" class="illustration"> -짝사랑 상대 😍 와 테이블에 앉아서 버거 🍔 를 기다리는 동안, 그 사람 😍 이 얼마나 멋지고, 사랑스럽고, 똑똑한지 감탄하며 시간을 보냅니다 ✨😍✨. +여러분이 돈을 냅니다. 💸 -짝사랑 상대 😍 와 기다리면서 얘기하는 동안, 때때로, 당신은 당신의 차례가 되었는지 보기 위해 카운터의 번호를 확인합니다. +점원은 여러분 차례 번호를 줍니다. -그러다 어느 순간, 당신의 차례가 됩니다. 카운터에 가서, 버거 🍔 를 받고, 테이블로 다시 돌아옵니다. +<img src="/img/async/concurrent-burgers/concurrent-burgers-04.png" class="illustration"> -당신과 짝사랑 상대 😍 는 버거 🍔 를 먹으며 좋은 시간을 보냅니다 ✨. +기다리는 동안, 여러분은 짝사랑 상대와 함께 자리를 고르고 앉아 오랫동안 대화를 나눕니다(여러분의 햄버거는 매우 고급스럽기 때문에 준비하는 데 시간이 좀 걸립니다). + +짝사랑 상대와 테이블에 앉아 햄버거를 기다리는 동안, 그 사람이 얼마나 멋지고 귀엽고 똑똑한지 감탄하며 시간을 보낼 수 있습니다 ✨😍✨. + +<img src="/img/async/concurrent-burgers/concurrent-burgers-05.png" class="illustration"> + +기다리며 대화하는 동안, 때때로 여러분은 카운터에 표시되는 번호를 확인해 여러분 차례인지 봅니다. + +그러다 어느 순간 마침내 여러분 차례가 됩니다. 여러분은 카운터에 가서 햄버거를 받고, 테이블로 돌아옵니다. + +<img src="/img/async/concurrent-burgers/concurrent-burgers-06.png" class="illustration"> + +여러분과 짝사랑 상대는 햄버거를 먹으며 좋은 시간을 보냅니다. ✨ + +<img src="/img/async/concurrent-burgers/concurrent-burgers-07.png" class="illustration"> + +/// info | 정보 + +아름다운 일러스트: <a href="https://www.instagram.com/ketrinadrawsalot" class="external-link" target="_blank">Ketrina Thompson</a>. 🎨 + +/// --- -당신이 이 이야기에서 컴퓨터 / 프로그램 🤖 이라고 상상해보십시오. +이 이야기에서 여러분이 컴퓨터/프로그램 🤖 이라고 상상해보세요. -줄을 서서 기다리는 동안, 당신은 아무것도 하지 않고 😴 당신의 차례를 기다리며, 어떠한 "생산적인" 일도 하지 않습니다. 하지만 점원 💁 이 (음식을 준비하지는 않고) 주문을 받기만 하기 때문에 줄이 빨리 줄어들어서 괜찮습니다. +줄을 서 있는 동안, 여러분은 그냥 쉬고 😴, 차례를 기다리며, 그다지 "생산적인" 일을 하지 않습니다. 하지만 점원은 주문만 받지(음식을 준비하진 않기) 때문에 줄이 빠르게 줄어들어 괜찮습니다. -그다음, 당신이 차례가 오면, 당신은 실제로 "생산적인" 일 🤓 을 합니다. 당신은 메뉴를 보고, 무엇을 먹을지 결정하고, 짝사랑 상대 😍 의 선택을 묻고, 돈을 내고 💸 , 맞는 카드를 냈는지 확인하고, 비용이 제대로 지불되었는지 확인하고, 주문이 제대로 들어갔는지 확인을 하는 작업 등등을 수행합니다. +그 다음 여러분 차례가 되면, 여러분은 실제로 "생산적인" 일을 합니다. 메뉴를 처리하고, 무엇을 먹을지 결정하고, 짝사랑 상대의 선택을 확인하고, 결제하고, 올바른 현금이나 카드를 냈는지 확인하고, 정확히 청구되었는지 확인하고, 주문에 올바른 항목들이 들어갔는지 확인하는 등등을 합니다. -하지만 이후에는, 버거 🍔 를 아직 받지 못했음에도, 버거가 준비될 때까지 기다려야 🕙 하기 때문에 점원 💁 과의 작업은 "일시정지" ⏸ 상태입니다. +하지만 그 다음에는, 아직 햄버거를 받지 못했더라도, 햄버거가 준비될 때까지 기다려야 🕙 하므로 점원과의 작업은 "일시정지" ⏸ 상태입니다. -하지만 번호표를 받고 카운터에서 나와 테이블에 앉으면, 당신은 짝사랑 상대 😍 와 그 "작업" ⏯ 🤓 에 번갈아가며 🔀 집중합니다. 그러면 당신은 다시 짝사랑 상대 😍 에게 작업을 거는 매우 "생산적인" 일 🤓 을 합니다. +하지만 번호를 들고 카운터에서 벗어나 테이블에 앉으면, 여러분은 짝사랑 상대에게 관심을 전환 🔀 하고, 그에 대한 "작업" ⏯ 🤓 을 할 수 있습니다. 그러면 여러분은 다시 짝사랑 상대에게 작업을 거는 매우 "생산적인" 일을 하게 됩니다 😍. -점원 💁 이 카운터 화면에 당신의 번호를 표시함으로써 "버거 🍔 가 준비되었습니다"라고 해도, 당신은 즉시 뛰쳐나가지는 않을 것입니다. 당신은 당신의 번호를 갖고있고, 다른 사람들은 그들의 번호를 갖고있기 때문에, 아무도 당신의 버거 🍔 를 훔쳐가지 않는다는 사실을 알기 때문입니다. +그 다음 점원 💁 이 카운터 화면에 여러분 번호를 띄워 "햄버거를 만들었어요"라고 말하지만, 표시된 번호가 여러분 차례로 바뀌었다고 해서 즉시 미친 듯이 뛰어가지는 않습니다. 여러분은 여러분 번호를 갖고 있고, 다른 사람들은 그들의 번호를 갖고 있으니, 아무도 여러분 햄버거를 훔쳐갈 수 없다는 것을 알기 때문입니다. -그래서 당신은 짝사랑 상대 😍 가 이야기를 끝낼 때까지 기다린 후 (현재 작업 완료 ⏯ / 진행 중인 작업 처리 🤓 ), 정중하게 미소짓고 버거를 가지러 가겠다고 말합니다 ⏸. +그래서 여러분은 짝사랑 상대가 이야기를 끝낼 때까지 기다린 다음(현재 작업 ⏯ / 처리 중인 작업 🤓 을 끝내고), 부드럽게 미소 지으며 햄버거를 가지러 가겠다고 말합니다 ⏸. -그다음 당신은 카운터에 가서 🔀 , 초기 작업을 이제 완료하고 ⏯ , 버거 🍔 를 받고, 감사하다고 말하고 테이블로 가져옵니다. 이로써 카운터와의 상호작용 단계 / 작업이 종료됩니다 ⏹. +그 다음 여러분은 카운터로 가서 🔀, 이제 끝난 초기 작업 ⏯ 으로 돌아와 햄버거를 받고, 감사 인사를 하고, 테이블로 가져옵니다. 이로써 카운터와 상호작용하는 그 단계/작업이 끝납니다 ⏹. 그리고 이는 새로운 작업인 "햄버거 먹기" 🔀 ⏯ 를 만들지만, 이전 작업인 "햄버거 받기"는 끝났습니다 ⏹. -이전 작업인 "버거 받기"가 종료되면 ⏹ "버거 먹기"라는 새로운 작업이 생성됩니다 🔀 ⏯. +### 병렬 햄버거 { #parallel-burgers } -### 병렬 버거 +이제 이것이 "동시 햄버거"가 아니라 "병렬 햄버거"라고 상상해봅시다. -이제 "동시 버거"가 아닌 "병렬 버거"를 상상해보십시오. +여러분은 짝사랑 상대와 함께 병렬 패스트푸드를 먹으러 갑니다. -당신은 짝사랑 상대 😍 와 함께 병렬 패스트푸드 🍔 를 먹으러 갔습니다. +여러분은 여러 명(예: 8명)의 점원이 동시에 요리사이기도 하여 여러분 앞 사람들의 주문을 받는 동안 줄을 서 있습니다. -당신은 여러명(8명이라고 가정합니다)의 점원이 당신 앞 사람들의 주문을 받으며 동시에 요리 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳 도 하는 동안 줄을 서서 기다립니다. +여러분 앞의 모든 사람들은, 8명의 점원 각각이 다음 주문을 받기 전에 바로 햄버거를 준비하러 가기 때문에, 카운터를 떠나지 않고 햄버거가 준비될 때까지 기다립니다. -당신 앞 모든 사람들이 버거가 준비될 때까지 카운터에서 떠나지 않고 기다립니다 🕙 . 왜냐하면 8명의 직원들이 다음 주문을 받기 전에 버거를 준비하러 가기 때문입니다. +<img src="/img/async/parallel-burgers/parallel-burgers-01.png" class="illustration"> -마침내 당신의 차례가 왔고, 당신은 당신과 짝사랑 상대 😍 를 위한 두 개의 고급스러운 버거 🍔 를 주문합니다. +마침내 여러분 차례가 되어, 여러분과 짝사랑 상대를 위해 매우 고급스러운 햄버거 2개를 주문합니다. -당신이 비용을 지불합니다 💸 . +여러분이 돈을 냅니다 💸. -점원이 주방에 갑니다 👨‍🍳 . +<img src="/img/async/parallel-burgers/parallel-burgers-02.png" class="illustration"> -당신은 번호표가 없기 때문에 누구도 당신의 버거 🍔 를 대신 가져갈 수 없도록 카운터에 서서 기다립니다 🕙 . +점원은 주방으로 갑니다. -당신과 짝사랑 상대 😍 는 다른 사람이 새치기해서 버거를 가져가지 못하게 하느라 바쁘기 때문에 🕙 , 짝사랑 상대에게 주의를 기울일 수 없습니다 😞 . +여러분은 번호표가 없으므로, 다른 사람이 여러분보다 먼저 햄버거를 가져가지 못하도록 카운터 앞에 서서 기다립니다 🕙. -이것은 "동기" 작업이고, 당신은 점원/요리사 👨‍🍳 와 "동기화" 되었습니다. 당신은 기다리고 🕙 , 점원/요리사 👨‍🍳 가 버거 🍔 준비를 완료한 후 당신에게 주거나, 누군가가 그것을 가져가는 그 순간에 그 곳에 있어야합니다. +<img src="/img/async/parallel-burgers/parallel-burgers-03.png" class="illustration"> -카운터 앞에서 오랫동안 기다린 후에 🕙 , 점원/요리사 👨‍🍳 가 당신의 버거 🍔 를 가지고 돌아옵니다. +여러분과 짝사랑 상대는 햄버거가 나오면 다른 사람이 끼어들어 가져가지 못하게 하느라 바쁘기 때문에, 짝사랑 상대에게 집중할 수 없습니다. 😞 -당신은 버거를 받고 짝사랑 상대와 함께 테이블로 돌아옵니다. +이것은 "동기" 작업이며, 여러분은 점원/요리사 👨‍🍳 와 "동기화"되어 있습니다. 점원/요리사 👨‍🍳 가 햄버거를 완성해 여러분에게 주는 정확한 순간에 그 자리에 있어야 하므로, 여러분은 기다려야 🕙 하고, 그렇지 않으면 다른 사람이 가져갈 수도 있습니다. -단지 먹기만 하다가, 다 먹었습니다 🍔 ⏹. +<img src="/img/async/parallel-burgers/parallel-burgers-04.png" class="illustration"> -카운터 앞에서 기다리면서 🕙 너무 많은 시간을 허비했기 때문에 대화를 하거나 작업을 걸 시간이 거의 없었습니다 😞 . +그러다 점원/요리사 👨‍🍳 가 카운터 앞에서 오랫동안 기다린 🕙 끝에 마침내 햄버거를 가지고 돌아옵니다. + +<img src="/img/async/parallel-burgers/parallel-burgers-05.png" class="illustration"> + +여러분은 햄버거를 받아 짝사랑 상대와 테이블로 갑니다. + +그냥 먹고, 끝입니다. ⏹ + +<img src="/img/async/parallel-burgers/parallel-burgers-06.png" class="illustration"> + +대부분의 시간을 카운터 앞에서 기다리는 데 🕙 썼기 때문에, 대화하거나 작업을 걸 시간은 많지 않았습니다. 😞 + +/// info | 정보 + +아름다운 일러스트: <a href="https://www.instagram.com/ketrinadrawsalot" class="external-link" target="_blank">Ketrina Thompson</a>. 🎨 + +/// --- -이 병렬 버거 시나리오에서, 당신은 기다리고 🕙 , 오랜 시간동안 "카운터에서 기다리는" 🕙 데에 주의를 기울이는 ⏯ 두 개의 프로세서(당신과 짝사랑 상대😍)를 가진 컴퓨터 / 프로그램 🤖 입니다. +이 병렬 햄버거 시나리오에서, 여러분은 두 개의 프로세서(여러분과 짝사랑 상대)를 가진 컴퓨터/프로그램 🤖 이며, 둘 다 기다리고 🕙 오랫동안 "카운터에서 기다리기" 🕙 에 주의를 ⏯ 기울입니다. -패스트푸드점에는 8개의 프로세서(점원/요리사) 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳 가 있습니다. 동시 버거는 단 두 개(한 명의 직원과 한 명의 요리사) 💁 👨‍🍳 만을 가지고 있었습니다. +패스트푸드점에는 8개의 프로세서(점원/요리사)가 있습니다. 동시 햄버거 가게는 2개(점원 1명, 요리사 1명)만 있었을 것입니다. -하지만 여전히, 병렬 버거 예시가 최선은 아닙니다 😞 . +하지만 여전히 최종 경험은 그다지 좋지 않습니다. 😞 --- -이 예시는 버거🍔 이야기와 결이 같습니다. +이것이 햄버거의 병렬 버전에 해당하는 이야기입니다. 🍔 -더 "현실적인" 예시로, 은행을 상상해보십시오. +좀 더 "현실적인" 예시로, 은행을 상상해보세요. -최근까지, 대다수의 은행에는 다수의 은행원들 👨‍💼👨‍💼👨‍💼👨‍💼 과 긴 줄 🕙🕙🕙🕙🕙🕙🕙🕙 이 있습니다. +최근까지 대부분의 은행에는 여러 은행원 👨‍💼👨‍💼👨‍💼👨‍💼 과 긴 줄 🕙🕙🕙🕙🕙🕙🕙🕙 이 있었습니다. -모든 은행원들은 한 명 한 명의 고객들을 차례로 상대합니다 👨‍💼⏯ . +모든 은행원이 한 고객씩 순서대로 모든 일을 처리합니다 👨‍💼⏯. -그리고 당신은 오랫동안 줄에서 기다려야하고 🕙 , 그렇지 않으면 당신의 차례를 잃게 됩니다. +그리고 여러분은 오랫동안 줄에서 기다려야 🕙 하며, 그렇지 않으면 차례를 잃습니다. -아마 당신은 은행 🏦 심부름에 짝사랑 상대 😍 를 데려가고 싶지는 않을 것입니다. +아마 은행 🏦 업무를 보러 갈 때 짝사랑 상대 😍 를 데려가고 싶지는 않을 것입니다. -### 버거 예시의 결론 +### 햄버거 예시의 결론 { #burger-conclusion } -"짝사랑 상대와의 패스트푸드점 버거" 시나리오에서, 오랜 기다림 🕙 이 있기 때문에 동시 시스템 ⏸🔀⏯ 을 사용하는 것이 더 합리적입니다. +"짝사랑 상대와의 패스트푸드점 햄버거" 시나리오에서는 기다림 🕙 이 많기 때문에, 동시 시스템 ⏸🔀⏯ 을 사용하는 것이 훨씬 더 합리적입니다. -대다수의 웹 응용프로그램의 경우가 그러합니다. +대부분의 웹 애플리케이션이 그렇습니다. -매우 많은 수의 유저가 있지만, 서버는 그들의 요청을 전송하기 위해 그닥-좋지-않은 연결을 기다려야 합니다 🕙 . +매우 많은 사용자들이 있고, 서버는 그들의 좋지 않은 연결을 통해 요청이 전송되기를 기다립니다 🕙. -그리고 응답이 돌아올 때까지 다시 기다려야 합니다 🕙 . +그리고 응답이 돌아오기를 다시 기다립니다 🕙. -이 "기다림" 🕙 은 마이크로초 단위이지만, 모두 더해지면, 결국에는 매우 긴 대기시간이 됩니다. +이 "기다림" 🕙 은 마이크로초 단위로 측정되지만, 모두 합치면 결국 꽤 많은 대기 시간이 됩니다. -따라서 웹 API를 위해 비동기 ⏸🔀⏯ 코드를 사용하는 것이 합리적입니다. +그래서 웹 API에는 비동기 ⏸🔀⏯ 코드를 사용하는 것이 매우 합리적입니다. -대부분의 존재하는 유명한 파이썬 프레임워크 (Flask와 Django 등)은 새로운 비동기 기능들이 파이썬에 존재하기 전에 만들어졌습니다. 그래서, 그들의 배포 방식은 병렬 실행과 새로운 기능만큼 강력하지는 않은 예전 버전의 비동기 실행을 지원합니다. +이러한 종류의 비동기성은 NodeJS가 인기 있는 이유(비록 NodeJS가 병렬은 아니지만)이자, 프로그래밍 언어로서 Go의 강점입니다. -비동기 웹 파이썬(ASGI)에 대한 주요 명세가 웹소켓을 지원하기 위해 Django에서 개발 되었음에도 그렇습니다. +그리고 이것이 **FastAPI**로 얻는 것과 같은 수준의 성능입니다. -이러한 종류의 비동기성은 (NodeJS는 병렬적이지 않음에도) NodeJS가 사랑받는 이유이고, 프로그래밍 언어로서의 Go의 강점입니다. +또한 병렬성과 비동기성을 동시에 사용할 수 있으므로, 대부분의 테스트된 NodeJS 프레임워크보다 더 높은 성능을 얻고, C에 더 가까운 컴파일 언어인 Go와 동등한 성능을 얻을 수 있습니다 <a href="https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=query&l=zijmkf-1" class="external-link" target="_blank">(모두 Starlette 덕분입니다)</a>. -그리고 **FastAPI**를 사용함으로써 동일한 성능을 낼 수 있습니다. +### 동시성이 병렬성보다 더 나은가요? { #is-concurrency-better-than-parallelism } -또한 병렬성과 비동기성을 동시에 사용할 수 있기 때문에, 대부분의 테스트가 완료된 NodeJS 프레임워크보다 더 높은 성능을 얻고 C에 더 가까운 컴파일 언어인 Go와 동등한 성능을 얻을 수 있습니다<a href="https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=query&l=zijmkf-1" class="external-link" target="_blank">(모두 Starlette 덕분입니다)</a>. +아니요! 그게 이 이야기의 교훈은 아닙니다. -### 동시성이 병렬성보다 더 나은가? +동시성은 병렬성과 다릅니다. 그리고 많은 기다림이 포함되는 **특정한** 시나리오에서는 더 낫습니다. 그 때문에 웹 애플리케이션 개발에서는 일반적으로 병렬성보다 훨씬 더 낫습니다. 하지만 모든 것에 해당하진 않습니다. -그렇지 않습니다! 그것이 이야기의 교훈은 아닙니다. +그래서 균형을 맞추기 위해, 다음의 짧은 이야기를 상상해보세요: -동시성은 병렬성과 다릅니다. 그리고 그것은 많은 대기를 필요로하는 **특정한** 시나리오에서는 더 낫습니다. 이로 인해, 웹 응용프로그램 개발에서 동시성이 병렬성보다 일반적으로 훨씬 낫습니다. 하지만 모든 경우에 그런 것은 아닙니다. - -따라서, 균형을 맞추기 위해, 다음의 짧은 이야기를 상상해보십시오: - -> 당신은 크고, 더러운 집을 청소해야합니다. +> 여러분은 크고 더러운 집을 청소해야 합니다. *네, 이게 전부입니다*. --- -어디에도 대기 🕙 는 없고, 집안 곳곳에서 해야하는 많은 작업들만 있습니다. +어디에도 기다림 🕙 은 없고, 집의 여러 장소에서 해야 할 일이 많을 뿐입니다. -버거 예시처럼 처음에는 거실, 그 다음은 부엌과 같은 식으로 순서를 정할 수도 있으나, 무엇도 기다리지 🕙 않고 계속해서 청소 작업만 수행하기 때문에, 순서는 아무런 영향을 미치지 않습니다. +햄버거 예시처럼 거실부터, 그 다음은 부엌처럼 순서를 정할 수도 있지만, 어떤 것도 기다리지 🕙 않고 계속 청소만 하기 때문에, 순서는 아무런 영향을 주지 않습니다. -순서가 있든 없든 동일한 시간이 소요될 것이고(동시성) 동일한 양의 작업을 하게 될 것입니다. +순서가 있든 없든(동시성) 끝내는 데 걸리는 시간은 같고, 같은 양의 일을 하게 됩니다. -하지만 이 경우에서, 8명의 전(前)-점원/요리사이면서-현(現)-청소부 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳 를 고용할 수 있고, 그들 각자(그리고 당신)가 집의 한 부분씩 맡아 청소를 한다면, 당신은 **병렬적**으로 작업을 수행할 수 있고, 조금의 도움이 있다면, 훨씬 더 빨리 끝낼 수 있습니다. +하지만 이 경우, 전(前) 점원/요리사이자 현(現) 청소부가 된 8명을 데려올 수 있고, 각자(그리고 여러분)가 집의 구역을 하나씩 맡아 청소한다면, 추가 도움과 함께 모든 일을 **병렬**로 수행하여 훨씬 더 빨리 끝낼 수 있습니다. -이 시나리오에서, (당신을 포함한) 각각의 청소부들은 프로세서가 될 것이고, 각자의 역할을 수행합니다. +이 시나리오에서 (여러분을 포함한) 각 청소부는 프로세서가 되어, 맡은 일을 수행합니다. -실행 시간의 대부분이 대기가 아닌 실제 작업에 소요되고, 컴퓨터에서 작업은 <abbr title="Central Processing Unit">CPU</abbr>에서 이루어지므로, 이러한 문제를 "CPU에 묶였"다고 합니다. +그리고 실행 시간의 대부분이 기다림이 아니라 실제 작업에 쓰이고, 컴퓨터에서 작업은 <abbr title="Central Processing Unit - 중앙 처리 장치">CPU</abbr>가 수행하므로, 이런 문제를 "CPU bound"라고 부릅니다. --- -CPU에 묶인 연산에 관한 흔한 예시는 복잡한 수학 처리를 필요로 하는 경우입니다. +CPU bound 작업의 흔한 예시는 복잡한 수학 처리가 필요한 것들입니다. 예를 들어: -* **오디오** 또는 **이미지** 처리. -* **컴퓨터 비전**: 하나의 이미지는 수백개의 픽셀로 구성되어있고, 각 픽셀은 3개의 값 / 색을 갖고 있으며, 일반적으로 해당 픽셀들에 대해 동시에 무언가를 계산해야하는 처리. -* **머신러닝**: 일반적으로 많은 "행렬"과 "벡터" 곱셈이 필요합니다. 거대한 스프레드 시트에 수들이 있고 그 수들을 동시에 곱해야 한다고 생각해보십시오. -* **딥러닝**: 머신러닝의 하위영역으로, 동일한 예시가 적용됩니다. 단지 이 경우에는 하나의 스프레드 시트에 곱해야할 수들이 있는 것이 아니라, 거대한 세트의 스프레드 시트들이 있고, 많은 경우에, 이 모델들을 만들고 사용하기 위해 특수한 프로세서를 사용합니다. +* **오디오** 또는 **이미지** 처리 +* **컴퓨터 비전**: 이미지는 수백만 개의 픽셀로 구성되며, 각 픽셀은 3개의 값/색을 갖습니다. 보통 그 픽셀들에 대해 동시에 무언가를 계산해야 합니다. +* **머신러닝**: 보통 많은 "matrix"와 "vector" 곱셈이 필요합니다. 숫자가 있는 거대한 스프레드시트를 생각하고, 그 모든 수를 동시에 곱한다고 생각해보세요. +* **딥러닝**: 머신러닝의 하위 분야이므로 동일하게 적용됩니다. 다만 곱해야 할 숫자가 있는 스프레드시트가 하나가 아니라, 아주 큰 집합이며, 많은 경우 그 모델을 만들고/또는 사용하기 위해 특별한 프로세서를 사용합니다. -### 동시성 + 병렬성: 웹 + 머신러닝 +### 동시성 + 병렬성: 웹 + 머신러닝 { #concurrency-parallelism-web-machine-learning } -**FastAPI**를 사용하면 웹 개발에서는 매우 흔한 동시성의 이점을 (NodeJS의 주된 매력만큼) 얻을 수 있습니다. +**FastAPI**를 사용하면 웹 개발에서 매우 흔한 동시성의 이점을( NodeJS의 주요 매력과 같은) 얻을 수 있습니다. -뿐만 아니라 머신러닝 시스템과 같이 **CPU에 묶인** 작업을 위해 병렬성과 멀티프로세싱(다수의 프로세스를 병렬적으로 동작시키는 것)을 이용하는 것도 가능합니다. +또한 머신러닝 시스템처럼 **CPU bound** 워크로드에 대해 병렬성과 멀티프로세싱(여러 프로세스를 병렬로 실행)을 활용할 수도 있습니다. -파이썬이 **데이터 사이언스**, 머신러닝과 특히 딥러닝에 의 주된 언어라는 간단한 사실에 더해서, 이것은 FastAPI를 데이터 사이언스 / 머신러닝 웹 API와 응용프로그램에 (다른 것들보다) 좋은 선택지가 되게 합니다. +이것은 파이썬이 **데이터 사이언스**, 머신러닝, 특히 딥러닝의 주요 언어라는 단순한 사실과 더해져, FastAPI를 데이터 사이언스/머신러닝 웹 API 및 애플리케이션(그 외에도 많은 것들)에 매우 잘 맞는 선택으로 만들어 줍니다. -배포시 병렬을 어떻게 가능하게 하는지 알고싶다면, [배포](deployment/index.md){.internal-link target=_blank}문서를 참고하십시오. +프로덕션에서 이 병렬성을 어떻게 달성하는지 보려면 [배포](deployment/index.md){.internal-link target=_blank} 섹션을 참고하세요. -## `async`와 `await` +## `async`와 `await` { #async-and-await } -최신 파이썬 버전에는 비동기 코드를 정의하는 매우 직관적인 방법이 있습니다. 이는 이것을 평범한 "순차적" 코드로 보이게 하고, 적절한 순간에 당신을 위해 "대기"합니다. +최신 파이썬 버전에는 비동기 코드를 정의하는 매우 직관적인 방법이 있습니다. 이 방법은 이를 평범한 "순차" 코드처럼 보이게 하고, 적절한 순간에 여러분을 위해 "기다림"을 수행합니다. -연산이 결과를 전달하기 전에 대기를 해야하고 새로운 파이썬 기능들을 지원한다면, 이렇게 코드를 작성할 수 있습니다: +결과를 주기 전에 기다림이 필요한 작업이 있고, 이러한 새로운 파이썬 기능을 지원한다면, 다음과 같이 작성할 수 있습니다: ```Python burgers = await get_burgers(2) ``` -여기서 핵심은 `await`입니다. 이것은 파이썬에게 `burgers` 결과를 저장하기 이전에 `get_burgers(2)`의 작업이 완료되기를 🕙 기다리라고 ⏸ 말합니다. 이로 인해, 파이썬은 그동안 (다른 요청을 받는 것과 같은) 다른 작업을 수행해도 된다는 것을 🔀 ⏯ 알게될 것입니다. +여기서 핵심은 `await`입니다. 이는 파이썬에게 `get_burgers(2)`가 그 일을 끝낼 때까지 🕙 기다리도록 ⏸ 말하고, 그 결과를 `burgers`에 저장하기 전에 완료되기를 기다리라고 합니다. 이를 통해 파이썬은 그동안(예: 다른 요청을 받는 것처럼) 다른 일을 하러 갈 수 있다는 것 🔀 ⏯ 을 알게 됩니다. -`await`가 동작하기 위해, 이것은 비동기를 지원하는 함수 내부에 있어야 합니다. 이를 위해서 함수를 `async def`를 사용해 정의하기만 하면 됩니다: +`await`가 동작하려면, 이 비동기성을 지원하는 함수 내부에 있어야 합니다. 그러려면 `async def`로 선언하기만 하면 됩니다: ```Python hl_lines="1" async def get_burgers(number: int): - # Do some asynchronous stuff to create the burgers + # 햄버거를 만들기 위한 비동기 처리를 수행 return burgers ``` -...`def`를 사용하는 대신: +...`def` 대신: ```Python hl_lines="2" -# This is not asynchronous +# 비동기가 아닙니다 def get_sequential_burgers(number: int): - # Do some sequential stuff to create the burgers + # 햄버거를 만들기 위한 순차 처리를 수행 return burgers ``` -`async def`를 사용하면, 파이썬은 해당 함수 내에서 `await` 표현에 주의해야한다는 사실과, 해당 함수의 실행을 "일시정지"⏸하고 다시 돌아오기 전까지 다른 작업을 수행🔀할 수 있다는 것을 알게됩니다. +`async def`를 사용하면, 파이썬은 그 함수 내부에서 `await` 표현식에 주의해야 하며, 그 함수의 실행을 "일시정지" ⏸ 하고 다시 돌아오기 전에 다른 일을 하러 갈 수 있다는 것 🔀 을 알게 됩니다. -`async def`f 함수를 호출하고자 할 때, "대기"해야합니다. 따라서, 아래는 동작하지 않습니다. +`async def` 함수를 호출하고자 할 때는, 그 함수를 "await" 해야 합니다. 따라서 아래는 동작하지 않습니다: ```Python -# This won't work, because get_burgers was defined with: async def +# 동작하지 않습니다. get_burgers는 async def로 정의되었습니다 burgers = get_burgers(2) ``` --- -따라서, `await`f를 사용해서 호출할 수 있는 라이브러리를 사용한다면, 다음과 같이 `async def`를 사용하는 *경로 작동 함수*를 생성해야 합니다: +따라서, `await`로 호출할 수 있다고 말하는 라이브러리를 사용한다면, 다음과 같이 그것을 사용하는 *경로 처리 함수*를 `async def`로 만들어야 합니다: ```Python hl_lines="2-3" @app.get('/burgers') @@ -317,94 +349,96 @@ async def read_burgers(): return burgers ``` -### 더 세부적인 기술적 사항 +### 더 세부적인 기술적 사항 { #more-technical-details } -`await`가 `async def`를 사용하는 함수 내부에서만 사용이 가능하다는 것을 눈치채셨을 것입니다. +`await`는 `async def`로 정의된 함수 내부에서만 사용할 수 있다는 것을 눈치채셨을 것입니다. -하지만 동시에, `async def`로 정의된 함수들은 "대기"되어야만 합니다. 따라서, `async def`를 사용한 함수들은 역시 `async def`를 사용한 함수 내부에서만 호출될 수 있습니다. +하지만 동시에, `async def`로 정의된 함수는 "await" 되어야 합니다. 따라서 `async def`를 가진 함수는 `async def`로 정의된 함수 내부에서만 호출될 수 있습니다. -그렇다면 닭이 먼저냐, 달걀이 먼저냐, 첫 `async` 함수를 어떻게 호출할 수 있겠습니까? +그렇다면, 닭이 먼저냐 달걀이 먼저냐처럼, 첫 번째 `async` 함수는 어떻게 호출할 수 있을까요? -**FastAPI**를 사용해 작업한다면 이것을 걱정하지 않아도 됩니다. 왜냐하면 그 "첫" 함수는 당신의 *경로 작동 함수*가 될 것이고, FastAPI는 어떻게 올바르게 처리할지 알고있기 때문입니다. +**FastAPI**로 작업한다면 걱정할 필요가 없습니다. 그 "첫" 함수는 여러분의 *경로 처리 함수*가 될 것이고, FastAPI는 올바르게 처리하는 방법을 알고 있기 때문입니다. -하지만 FastAPI를 사용하지 않고 `async` / `await`를 사용하고 싶다면, 이 역시 가능합니다. +하지만 FastAPI 없이 `async` / `await`를 사용하고 싶다면, 그것도 가능합니다. -### 당신만의 비동기 코드 작성하기 +### 여러분만의 async 코드 작성하기 { #write-your-own-async-code } -Starlette(그리고 FastAPI)는 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>를 기반으로 하고있고, 따라서 파이썬 표준 라이브러리인 <a href="https://docs.python.org/3/library/asyncio-task.html" class="external-link" target="_blank">asyncio</a> 및 <a href="https://trio.readthedocs.io/en/stable/" class="external-link" target="_blank">Trio</a>와 호환됩니다. +Starlette(그리고 **FastAPI**)는 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>를 기반으로 하고 있으며, 파이썬 표준 라이브러리 <a href="https://docs.python.org/3/library/asyncio-task.html" class="external-link" target="_blank">asyncio</a>와 <a href="https://trio.readthedocs.io/en/stable/" class="external-link" target="_blank">Trio</a> 모두와 호환됩니다. -특히, 코드에서 고급 패턴이 필요한 고급 동시성을 사용하는 경우 직접적으로 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>를 사용할 수 있습니다. +특히, 코드에서 더 고급 패턴이 필요한 고급 동시성 사용 사례에서는 직접 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>를 사용할 수 있습니다. -FastAPI를 사용하지 않더라도, 높은 호환성 및 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>의 이점(예: *구조화된 동시성*)을 취하기 위해 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>를 사용해 비동기 응용프로그램을 작성할 수 있습니다. +그리고 FastAPI를 사용하지 않더라도, 높은 호환성을 확보하고 그 이점(예: *structured concurrency*)을 얻기 위해 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>로 여러분만의 async 애플리케이션을 작성할 수도 있습니다. -### 비동기 코드의 다른 형태 +저는 AnyIO 위에 얇은 레이어로 또 다른 라이브러리를 만들었는데, 타입 어노테이션을 조금 개선하고 더 나은 **자동완성**, **인라인 오류** 등을 얻기 위한 것입니다. 또한 **이해**하고 **여러분만의 async 코드**를 작성하도록 돕는 친절한 소개와 튜토리얼도 제공합니다: <a href="https://asyncer.tiangolo.com/" class="external-link" target="_blank">Asyncer</a>. 특히 **async 코드와 일반**(blocking/동기) 코드를 **결합**해야 한다면 아주 유용합니다. -파이썬에서 `async`와 `await`를 사용하게 된 것은 비교적 최근의 일입니다. +### 비동기 코드의 다른 형태 { #other-forms-of-asynchronous-code } -하지만 이로 인해 비동기 코드 작업이 훨씬 간단해졌습니다. +`async`와 `await`를 사용하는 이 스타일은 언어에서 비교적 최근에 추가되었습니다. -같은 (또는 거의 유사한) 문법은 최신 버전의 자바스크립트(브라우저와 NodeJS)에도 추가되었습니다. +하지만 비동기 코드를 다루는 일을 훨씬 더 쉽게 만들어 줍니다. -하지만 그 이전에, 비동기 코드를 처리하는 것은 꽤 복잡하고 어려운 일이었습니다. +거의 동일한 문법이 최근 브라우저와 NodeJS의 최신 JavaScript에도 포함되었습니다. -파이썬의 예전 버전이라면, 스레드 또는 <a href="https://www.gevent.org/" class="external-link" target="_blank">Gevent</a>를 사용할 수 있을 것입니다. 하지만 코드를 이해하고, 디버깅하고, 이에 대해 생각하는게 훨씬 복잡합니다. +하지만 그 이전에는 비동기 코드를 처리하는 것이 훨씬 더 복잡하고 어려웠습니다. -예전 버전의 NodeJS / 브라우저 자바스크립트라면, "콜백 함수"를 사용했을 것입니다. 그리고 이로 인해 "콜백 지옥"에 빠지게 될 수 있습니다. +이전 버전의 파이썬에서는 스레드 또는 <a href="https://www.gevent.org/" class="external-link" target="_blank">Gevent</a>를 사용할 수 있었을 것입니다. 하지만 코드를 이해하고, 디버깅하고, 이에 대해 생각하는 것이 훨씬 더 복잡합니다. -## 코루틴 +이전 버전의 NodeJS/브라우저 JavaScript에서는 "callback"을 사용했을 것입니다. 이는 "callback hell"로 이어집니다. -**코루틴**은 `async def` 함수가 반환하는 것을 칭하는 매우 고급스러운 용어일 뿐입니다. 파이썬은 그것이 시작되고 어느 시점에서 완료되지만 내부에 `await`가 있을 때마다 내부적으로 일시정지⏸될 수도 있는 함수와 유사한 것이라는 사실을 알고있습니다. +## 코루틴 { #coroutines } -그러나 `async` 및 `await`와 함께 비동기 코드를 사용하는 이 모든 기능들은 "코루틴"으로 간단히 요약됩니다. 이것은 Go의 주된 핵심 기능인 "고루틴"에 견줄 수 있습니다. +**코루틴**은 `async def` 함수가 반환하는 것에 대한 매우 고급스러운 용어일 뿐입니다. 파이썬은 그것이 함수와 비슷한 무언가로서 시작할 수 있고, 어느 시점에 끝나지만, 내부에 `await`가 있을 때마다 내부적으로도 일시정지 ⏸ 될 수 있다는 것을 알고 있습니다. -## 결론 +하지만 `async` 및 `await`와 함께 비동기 코드를 사용하는 이 모든 기능은 종종 "코루틴"을 사용한다고 요약됩니다. 이는 Go의 주요 핵심 기능인 "Goroutines"에 비견됩니다. -상기 문장을 다시 한 번 봅시다: +## 결론 { #conclusion } -> 최신 파이썬 버전은 **`async` 및 `await`** 문법과 함께 **“코루틴”**이라고 하는 것을 사용하는 **“비동기 코드”**를 지원합니다. +위의 같은 문장을 다시 봅시다: -이제 이 말을 조금 더 이해할 수 있을 것입니다. ✨ +> 최신 파이썬 버전은 **“코루틴”**이라고 하는 것을 사용하는 **“비동기 코드”**를 **`async` 및 `await`** 문법과 함께 지원합니다. -이것이 (Starlette을 통해) FastAPI를 강하게 하면서 그것이 인상적인 성능을 낼 수 있게 합니다. +이제 더 이해가 될 것입니다. ✨ -## 매우 세부적인 기술적 사항 +이 모든 것이 FastAPI(Starlette을 통해)를 구동하고, 인상적인 성능을 내게 하는 원동력입니다. + +## 매우 세부적인 기술적 사항 { #very-technical-details } /// warning | 경고 -이 부분은 넘어가도 됩니다. +이 부분은 아마 건너뛰어도 됩니다. 이것들은 **FastAPI**가 내부적으로 어떻게 동작하는지에 대한 매우 세부적인 기술사항입니다. -만약 기술적 지식(코루틴, 스레드, 블록킹 등)이 있고 FastAPI가 어떻게 `async def` vs `def`를 다루는지 궁금하다면, 계속하십시오. +(코루틴, 스레드, 블로킹 등) 같은 기술 지식이 꽤 있고 FastAPI가 `async def`와 일반 `def`를 어떻게 처리하는지 궁금하다면, 계속 읽어보세요. /// -### 경로 작동 함수 +### 경로 처리 함수 { #path-operation-functions } -경로 작동 함수를 `async def` 대신 일반적인 `def`로 선언하는 경우, (서버를 차단하는 것처럼) 그것을 직접 호출하는 대신 대기중인 외부 스레드풀에서 실행됩니다. +*경로 처리 함수*를 `async def` 대신 일반적인 `def`로 선언하면, (서버를 블로킹할 수 있으므로 직접 호출하는 대신) 외부 스레드풀에서 실행되고 그 결과를 await 합니다. -만약 상기에 묘사된대로 동작하지 않는 비동기 프로그램을 사용해왔고 약간의 성능 향상 (약 100 나노초)을 위해 `def`를 사용해서 계산만을 위한 사소한 *경로 작동 함수*를 정의해왔다면, **FastAPI**는 이와는 반대라는 것에 주의하십시오. 이러한 경우에, *경로 작동 함수*가 블로킹 <abbr title="Input/Output: 디스크 읽기 또는 쓰기, 네트워크 통신.">I/O</abbr>를 수행하는 코드를 사용하지 않는 한 `async def`를 사용하는 편이 더 낫습니다. +위에서 설명한 방식으로 동작하지 않는 다른 async 프레임워크를 사용해본 적이 있고, 아주 작은 성능 향상(약 100 나노초)을 위해 계산만 하는 사소한 *경로 처리 함수*를 일반 `def`로 정의하곤 했다면, **FastAPI**에서는 그 효과가 정반대가 될 수 있다는 점에 유의하세요. 이런 경우에는 *경로 처리 함수*에서 블로킹 <abbr title="Input/Output - 입력/출력: 디스크 읽기 또는 쓰기, 네트워크 통신.">I/O</abbr> 를 수행하는 코드를 사용하지 않는 한 `async def`를 사용하는 편이 더 낫습니다. -하지만 두 경우 모두, FastAPI가 당신이 전에 사용하던 프레임워크보다 [더 빠를](index.md#_11){.internal-link target=_blank} (최소한 비견될) 확률이 높습니다. +그럼에도 두 경우 모두, **FastAPI**는 이전에 사용하던 프레임워크보다 [여전히 더 빠를](index.md#performance){.internal-link target=_blank} 가능성이 높습니다(또는 최소한 비슷합니다). -### 의존성 +### 의존성 { #dependencies } -의존성에도 동일하게 적용됩니다. 의존성이 `async def`가 아닌 표준 `def` 함수라면, 외부 스레드풀에서 실행됩니다. +[의존성](tutorial/dependencies/index.md){.internal-link target=_blank}에도 동일하게 적용됩니다. 의존성이 `async def` 대신 표준 `def` 함수라면, 외부 스레드풀에서 실행됩니다. -### 하위-의존성 +### 하위 의존성 { #sub-dependencies } -함수 정의시 매개변수로 서로를 필요로하는 다수의 의존성과 하위-의존성을 가질 수 있고, 그 중 일부는 `async def`로, 다른 일부는 일반적인 `def`로 생성되었을 수 있습니다. 이것은 여전히 잘 동작하고, 일반적인 `def`로 생성된 것들은 "대기"되는 대신에 (스레드풀로부터) 외부 스레드에서 호출됩니다. +서로를 필요로 하는 여러 의존성과 [하위 의존성](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank}을 함수 정의의 매개변수로 가질 수 있으며, 그중 일부는 `async def`로, 다른 일부는 일반 `def`로 생성되었을 수 있습니다. 그래도 정상 동작하며, 일반 `def`로 생성된 것들은 "await"되는 대신 (스레드풀에서) 외부 스레드에서 호출됩니다. -### 다른 유틸리티 함수 +### 다른 유틸리티 함수 { #other-utility-functions } -직접 호출되는 다른 모든 유틸리티 함수는 일반적인 `def`나 `async def`로 생성될 수 있고 FastAPI는 이를 호출하는 방식에 영향을 미치지 않습니다. +직접 호출하는 다른 모든 유틸리티 함수는 일반 `def`나 `async def`로 생성될 수 있으며, FastAPI는 호출 방식에 영향을 주지 않습니다. -이것은 FastAPI가 당신을 위해 호출하는 함수와는 반대입니다: *경로 작동 함수*와 의존성 +이는 FastAPI가 여러분을 위해 호출하는 함수(즉, *경로 처리 함수*와 의존성)와 대비됩니다. -만약 당신의 유틸리티 함수가 `def`를 사용한 일반적인 함수라면, 스레드풀에서가 아니라 직접 호출(당신이 코드에 작성한 대로)될 것이고, `async def`로 생성된 함수라면 코드에서 호출할 때 그 함수를 `await` 해야 합니다. +유틸리티 함수가 `def`로 만든 일반 함수라면, 스레드풀이 아니라 직접(코드에 작성한 대로) 호출됩니다. 그리고 `async def`로 생성된 함수라면, 코드에서 호출할 때 그 함수를 `await` 해야 합니다. --- -다시 말하지만, 이것은 당신이 이것에 대해 찾고있던 경우에 한해 유용할 매우 세부적인 기술사항입니다. +다시 말하지만, 이것들은 아마도 이를 찾고 있었던 경우에 유용한 매우 세부적인 기술사항입니다. -그렇지 않은 경우, 상기의 가이드라인만으로도 충분할 것입니다: [바쁘신 경우](#_1). +그렇지 않다면, 위 섹션의 가이드라인이면 충분합니다: <a href="#in-a-hurry">바쁘신가요?</a>. diff --git a/docs/ko/docs/fastapi-cli.md b/docs/ko/docs/fastapi-cli.md index a1160c71f..0d87ce321 100644 --- a/docs/ko/docs/fastapi-cli.md +++ b/docs/ko/docs/fastapi-cli.md @@ -1,83 +1,75 @@ -# FastAPI CLI +# FastAPI CLI { #fastapi-cli } -**FastAPI CLI**는 FastAPI 애플리케이션을 실행하고, 프로젝트를 관리하는 등 다양한 작업을 수행할 수 있는 커맨드 라인 프로그램입니다. +**FastAPI CLI**는 FastAPI 애플리케이션을 서빙하고, FastAPI 프로젝트를 관리하는 등 다양한 작업에 사용할 수 있는 커맨드 라인 프로그램입니다. -FastAPI를 설치할 때 (예: `pip install "fastapi[standard]"` 명령어를 사용할 경우), `fastapi-cli`라는 패키지가 포함됩니다. 이 패키지는 터미널에서 사용할 수 있는 `fastapi` 명령어를 제공합니다. +FastAPI를 설치할 때(예: `pip install "fastapi[standard]"`), `fastapi-cli`라는 패키지가 포함되며, 이 패키지는 터미널에서 `fastapi` 명령어를 제공합니다. -개발용으로 FastAPI 애플리케이션을 실행하려면 다음과 같이 `fastapi dev` 명령어를 사용할 수 있습니다: +개발용으로 FastAPI 애플리케이션을 실행하려면 `fastapi dev` 명령어를 사용할 수 있습니다: <div class="termy"> ```console -$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:single">main.py</u> -<font color="#3465A4">INFO </font> Using path <font color="#3465A4">main.py</font> -<font color="#3465A4">INFO </font> Resolved absolute path <font color="#75507B">/home/user/code/awesomeapp/</font><font color="#AD7FA8">main.py</font> -<font color="#3465A4">INFO </font> Searching for package file structure from directories with <font color="#3465A4">__init__.py</font> files -<font color="#3465A4">INFO </font> Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> +$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:solid">main.py</u> - ╭─ <font color="#8AE234"><b>Python module file</b></font> ─╮ - │ │ - │ 🐍 main.py │ - │ │ - ╰──────────────────────╯ + <span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting development server 🚀 -<font color="#3465A4">INFO </font> Importing module <font color="#4E9A06">main</font> -<font color="#3465A4">INFO </font> Found importable FastAPI app + Searching for package file structure from directories with + <font color="#3465A4">__init__.py</font> files + Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> - ╭─ <font color="#8AE234"><b>Importable FastAPI app</b></font> ─╮ - │ │ - │ <span style="background-color:#272822"><font color="#FF4689">from</font></span><span style="background-color:#272822"><font color="#F8F8F2"> main </font></span><span style="background-color:#272822"><font color="#FF4689">import</font></span><span style="background-color:#272822"><font color="#F8F8F2"> app</font></span><span style="background-color:#272822"> </span> │ - │ │ - ╰──────────────────────────╯ + <span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py -<font color="#3465A4">INFO </font> Using import string <font color="#8AE234"><b>main:app</b></font> + <span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with the + following code: - <span style="background-color:#C4A000"><font color="#2E3436">╭────────── FastAPI CLI - Development mode ───────────╮</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ Serving at: http://127.0.0.1:8000 │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ API docs: http://127.0.0.1:8000/docs │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ Running in development mode, for production use: │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ </font></span><span style="background-color:#C4A000"><font color="#555753"><b>fastapi run</b></font></span><span style="background-color:#C4A000"><font color="#2E3436"> │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">╰─────────────────────────────────────────────────────╯</font></span> + <u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> -<font color="#4E9A06">INFO</font>: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] -<font color="#4E9A06">INFO</font>: Uvicorn running on <b>http://127.0.0.1:8000</b> (Press CTRL+C to quit) -<font color="#4E9A06">INFO</font>: Started reloader process [<font color="#34E2E2"><b>2265862</b></font>] using <font color="#34E2E2"><b>WatchFiles</b></font> -<font color="#4E9A06">INFO</font>: Started server process [<font color="#06989A">2265873</font>] -<font color="#4E9A06">INFO</font>: Waiting for application startup. -<font color="#4E9A06">INFO</font>: Application startup complete. + <span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000/docs</u></font> + + <span style="background-color:#007166"><font color="#D3D7CF"> tip </font></span> Running in development mode, for production use: + <b>fastapi run</b> + + Logs: + + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Will watch for changes in these directories: + <b>[</b><font color="#4E9A06">'/home/user/code/awesomeapp'</font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> <b>(</b>Press CTRL+C to + quit<b>)</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started reloader process <b>[</b><font color="#34E2E2"><b>383138</b></font><b>]</b> using WatchFiles + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>383153</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. ``` </div> -`fastapi`라고 불리는 명령어 프로그램은 **FastAPI CLI**입니다. +`fastapi`라고 불리는 커맨드 라인 프로그램은 **FastAPI CLI**입니다. -FastAPI CLI는 Python 프로그램의 경로(예: `main.py`)를 인수로 받아, `FastAPI` 인스턴스(일반적으로 `app`으로 명명)를 자동으로 감지하고 올바른 임포트 과정을 결정한 후 이를 실행합니다. +FastAPI CLI는 Python 프로그램의 경로(예: `main.py`)를 받아 `FastAPI` 인스턴스(일반적으로 `app`으로 이름을 붙임)를 자동으로 감지하고, 올바른 임포트 과정을 결정한 다음 서빙합니다. -프로덕션 환경에서는 `fastapi run` 명령어를 사용합니다. 🚀 +프로덕션에서는 대신 `fastapi run`을 사용합니다. 🚀 -내부적으로, **FastAPI CLI**는 고성능의, 프로덕션에 적합한, ASGI 서버인 <a href="https://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a>을 사용합니다. 😎 +내부적으로 **FastAPI CLI**는 고성능의, 프로덕션에 적합한 ASGI 서버인 <a href="https://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a>을 사용합니다. 😎 -## `fastapi dev` +## `fastapi dev` { #fastapi-dev } -`fastapi dev` 명령을 실행하면 개발 모드가 시작됩니다. +`fastapi dev`를 실행하면 개발 모드가 시작됩니다. -기본적으로 **자동 재시작(auto-reload)** 기능이 활성화되어, 코드에 변경이 생기면 서버를 자동으로 다시 시작합니다. 하지만 이 기능은 리소스를 많이 사용하며, 비활성화했을 때보다 안정성이 떨어질 수 있습니다. 따라서 개발 환경에서만 사용하는 것이 좋습니다. 또한, 서버는 컴퓨터가 자체적으로 통신할 수 있는 IP 주소(`localhost`)인 `127.0.0.1`에서 연결을 대기합니다. +기본적으로 **auto-reload**가 활성화되어 코드에 변경이 생기면 서버를 자동으로 다시 로드합니다. 이는 리소스를 많이 사용하며, 비활성화했을 때보다 안정성이 떨어질 수 있습니다. 개발 환경에서만 사용해야 합니다. 또한 컴퓨터가 자신과만 통신하기 위한(`localhost`) IP인 `127.0.0.1`에서 연결을 대기합니다. -## `fastapi run` +## `fastapi run` { #fastapi-run } -`fastapi run` 명령을 실행하면 기본적으로 프로덕션 모드로 FastAPI가 시작됩니다. +`fastapi run`을 실행하면 기본적으로 프로덕션 모드로 FastAPI가 시작됩니다. -기본적으로 **자동 재시작(auto-reload)** 기능이 비활성화되어 있습니다. 또한, 사용 가능한 모든 IP 주소인 `0.0.0.0`에서 연결을 대기하므로 해당 컴퓨터와 통신할 수 있는 모든 사람이 공개적으로 액세스할 수 있습니다. 이는 일반적으로 컨테이너와 같은 프로덕션 환경에서 실행하는 방법입니다. +기본적으로 **auto-reload**는 비활성화되어 있습니다. 또한 사용 가능한 모든 IP 주소를 의미하는 `0.0.0.0`에서 연결을 대기하므로, 해당 컴퓨터와 통신할 수 있는 누구에게나 공개적으로 접근 가능해집니다. 보통 프로덕션에서는 이렇게 실행하며, 예를 들어 컨테이너에서 이런 방식으로 실행합니다. -애플리케이션을 배포하는 방식에 따라 다르지만, 대부분 "종료 프록시(termination proxy)"를 활용해 HTTPS를 처리하는 것이 좋습니다. 배포 서비스 제공자가 이 작업을 대신 처리해줄 수도 있고, 직접 설정해야 할 수도 있습니다. +대부분의 경우 위에 "termination proxy"를 두고 HTTPS를 처리하게(그리고 처리해야) 됩니다. 이는 애플리케이션을 배포하는 방식에 따라 달라지며, 제공자가 이 작업을 대신 처리해줄 수도 있고 직접 설정해야 할 수도 있습니다. -/// tip +/// tip | 팁 -자세한 내용은 [deployment documentation](deployment/index.md){.internal-link target=\_blank}에서 확인할 수 있습니다. +자세한 내용은 [배포 문서](deployment/index.md){.internal-link target=_blank}에서 확인할 수 있습니다. /// diff --git a/docs/ko/docs/features.md b/docs/ko/docs/features.md index dfbf47999..17cc9289f 100644 --- a/docs/ko/docs/features.md +++ b/docs/ko/docs/features.md @@ -1,43 +1,43 @@ -# 기능 +# 기능 { #features } -## FastAPI의 기능 +## FastAPI의 기능 { #fastapi-features } **FastAPI**는 다음과 같은 기능을 제공합니다: -### 개방형 표준을 기반으로 +### 개방형 표준을 기반으로 { #based-on-open-standards } -* <abbr title="엔드포인트, 라우트로도 알려져 있습니다">경로</abbr><abbr title="POST, GET, PUT, DELETE와 같은 HTTP 메소드로 알려져 있습니다">작동</abbr>, 매개변수, 본문 요청, 보안 그 외의 선언을 포함한 API 생성을 위한 <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank"><strong>OpenAPI</strong></a> -* <a href="https://json-schema.org/" class="external-link" target="_blank"><strong>JSON Schema</strong></a> (OpenAPI 자체가 JSON Schema를 기반으로 하고 있습니다)를 사용한 자동 데이터 모델 문서화. -* 단순히 떠올려서 덧붙인 기능이 아닙니다. 세심한 검토를 거친 후, 이러한 표준을 기반으로 설계되었습니다. -* 이는 또한 다양한 언어로 자동적인 **클라이언트 코드 생성**을 사용할 수 있게 지원합니다. +* <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank"><strong>OpenAPI</strong></a>: <abbr title="또한 다음으로도 불립니다: 엔드포인트, 라우트">path</abbr> <abbr title="HTTP 메소드(POST, GET, PUT, DELETE 등)로도 알려져 있습니다">operations</abbr>, 매개변수, 요청 본문, 보안 등의 선언을 포함하여 API를 생성합니다. +* <a href="https://json-schema.org/" class="external-link" target="_blank"><strong>JSON Schema</strong></a>를 사용한 자동 데이터 모델 문서화(OpenAPI 자체가 JSON Schema를 기반으로 하기 때문입니다). +* 단순히 떠올려서 덧붙인 레이어가 아니라, 세심한 검토를 거친 뒤 이러한 표준을 중심으로 설계되었습니다. +* 이는 또한 다양한 언어로 자동 **클라이언트 코드 생성**을 사용할 수 있게 해줍니다. -### 문서 자동화 +### 문서 자동화 { #automatic-docs } -대화형 API 문서와 웹 탐색 유저 인터페이스를 제공합니다. 프레임워크가 OpenAPI를 기반으로 하기에, 2가지 옵션이 기본적으로 들어간 여러 옵션이 존재합니다. +대화형 API 문서와 탐색용 웹 사용자 인터페이스를 제공합니다. 프레임워크가 OpenAPI를 기반으로 하기에 여러 옵션이 있으며, 기본으로 2가지가 포함됩니다. -* 대화형 탐색 <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank"><strong>Swagger UI</strong></a>를 이용해, 브라우저에서 바로 여러분의 API를 호출하거나 테스트할 수 있습니다. +* 대화형 탐색이 가능한 <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank"><strong>Swagger UI</strong></a>로 브라우저에서 직접 API를 호출하고 테스트할 수 있습니다. ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank"><strong>ReDoc</strong></a>을 이용해 API 문서화를 대체할 수 있습니다. +* <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank"><strong>ReDoc</strong></a>을 이용한 대체 API 문서화. ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### 그저 현대 파이썬 +### 그저 현대 파이썬 { #just-modern-python } -(Pydantic 덕분에) FastAPI는 표준 **파이썬 3.6 타입** 선언에 기반하고 있습니다. 새로 배울 문법이 없습니다. 그저 표준적인 현대 파이썬입니다. +( Pydantic 덕분에) 모든 것이 표준 **Python 타입** 선언을 기반으로 합니다. 새로 배울 문법이 없습니다. 그저 표준적인 현대 파이썬입니다. -만약 여러분이 파이썬 타입을 어떻게 사용하는지에 대한 2분 정도의 복습이 필요하다면 (비록 여러분이 FastAPI를 사용하지 않는다 하더라도), 다음의 짧은 자습서를 확인하세요: [파이썬 타입](python-types.md){.internal-link target=\_blank}. +Python 타입을 어떻게 사용하는지 2분 정도 복습이 필요하다면(FastAPI를 사용하지 않더라도), 다음의 짧은 자습서를 확인하세요: [Python 타입](python-types.md){.internal-link target=_blank}. -여러분은 타입을 이용한 표준 파이썬을 다음과 같이 적을 수 있습니다: +여러분은 타입이 있는 표준 Python을 다음과 같이 작성합니다: ```Python from datetime import date from pydantic import BaseModel -# 변수를 str로 선언 -# 그 후 함수 안에서 편집기 지원을 받으세요 +# 변수를 str로 선언합니다 +# 그리고 함수 내부에서 편집기 지원을 받습니다 def main(user_id: str): return user_id @@ -49,7 +49,7 @@ class User(BaseModel): joined: date ``` -위의 코드는 다음과 같이 사용될 수 있습니다: +그 다음 다음과 같이 사용할 수 있습니다: ```Python my_user: User = User(id=3, name="John Doe", joined="2018-07-19") @@ -65,23 +65,23 @@ my_second_user: User = User(**second_user_data) /// info | 정보 -`**second_user_data`가 뜻하는 것: +`**second_user_data`는 다음을 의미합니다: -`second_user_data` 딕셔너리의 키와 값을 키-값 인자로서 바로 넘겨줍니다. 다음과 동일합니다: `User(id=4, name="Mary", joined="2018-11-30")` +`second_user_data` `dict`의 키와 값을 키-값 인자로서 바로 넘겨주는 것으로, 다음과 동일합니다: `User(id=4, name="Mary", joined="2018-11-30")` /// -### 편집기 지원 +### 편집기 지원 { #editor-support } -모든 프레임워크는 사용하기 쉽고 직관적으로 설계되었으며, 좋은 개발 경험을 보장하기 위해 개발을 시작하기도 전에 모든 결정들은 여러 편집기에서 테스트됩니다. +프레임워크 전체는 사용하기 쉽고 직관적으로 설계되었으며, 최고의 개발 경험을 보장하기 위해 개발을 시작하기도 전에 모든 결정은 여러 편집기에서 테스트되었습니다. -최근 파이썬 개발자 설문조사에서 <a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank">"자동 완성"이 가장 많이 사용되는 기능</a>이라는 것이 밝혀졌습니다. +Python 개발자 설문조사에서 <a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank">가장 많이 사용되는 기능 중 하나가 "자동 완성"이라는 점</a>이 분명합니다. -**FastAPI** 프레임워크의 모든 부분은 이를 충족하기 위해 설계되었습니다. 자동완성은 어느 곳에서나 작동합니다. +**FastAPI** 프레임워크 전체는 이를 만족하기 위해 만들어졌습니다. 자동 완성은 어디서나 작동합니다. -여러분은 문서로 다시 돌아올 일이 거의 없을 겁니다. +문서로 다시 돌아올 일은 거의 없을 것입니다. -다음은 편집기가 어떻게 여러분을 도와주는지 보여줍니다: +편집기가 여러분을 어떻게 도와줄 수 있는지 살펴보세요: * <a href="https://code.visualstudio.com/" class="external-link" target="_blank">Visual Studio Code</a>에서: @@ -91,111 +91,111 @@ my_second_user: User = User(**second_user_data) ![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) -여러분이 이전에 불가능하다고 고려했던 코드도 완성할 수 있을 겁니다. 예를 들어, 요청에서 전달되는 (중첩될 수도 있는)JSON 본문 내부에 있는 `price` 키입니다. +이전에 불가능하다고 생각했을 코드에서도 자동 완성을 받을 수 있습니다. 예를 들어, 요청에서 전달되는(중첩될 수도 있는) JSON 본문 내부의 `price` 키 같은 경우입니다. -잘못된 키 이름을 적을 일도, 문서를 왔다 갔다할 일도 없으며, 혹은 마지막으로 `username` 또는 `user_name`을 사용했는지 찾기 위해 위 아래로 스크롤할 일도 없습니다. +더 이상 잘못된 키 이름을 입력하거나, 문서 사이를 왔다 갔다 하거나, `username`을 썼는지 `user_name`을 썼는지 찾으려고 위아래로 스크롤할 필요가 없습니다. -### 토막 정보 +### 간결함 { #short } -어느 곳에서나 선택적 구성이 가능한 모든 것에 합리적인 기본값이 설정되어 있습니다. 모든 매개변수는 여러분이 필요하거나, 원하는 API를 정의하기 위해 미세하게 조정할 수 있습니다. +선택적 구성을 어디서나 할 수 있도록 하면서도, 모든 것에 합리적인 **기본값**이 설정되어 있습니다. 모든 매개변수는 필요한 작업을 하거나 필요한 API를 정의하기 위해 미세하게 조정할 수 있습니다. -하지만 기본적으로 모든 것이 "그냥 작동합니다". +하지만 기본적으로 모든 것이 **"그냥 작동합니다"**. -### 검증 +### 검증 { #validation } -* 다음을 포함한, 대부분의 (혹은 모든?) 파이썬 **데이터 타입** 검증할 수 있습니다: +* 다음을 포함해 대부분(혹은 전부?)의 Python **데이터 타입**에 대한 검증: * JSON 객체 (`dict`). * 아이템 타입을 정의하는 JSON 배열 (`list`). - * 최소 길이와 최대 길이를 정의하는 문자열 (`str`) 필드. - * 최솟값과 최댓값을 가지는 숫자 (`int`, `float`), 그 외. + * 최소/최대 길이를 정의하는 문자열(`str`) 필드. + * 최소/최대 값을 가지는 숫자(`int`, `float`) 등. -* 다음과 같이 더욱 이색적인 타입에 대해 검증할 수 있습니다: +* 다음과 같은 좀 더 이색적인 타입에 대한 검증: * URL. - * 이메일. + * Email. * UUID. - * ...다른 것들. + * ...그 외. -모든 검증은 견고하면서 잘 확립된 **Pydantic**에 의해 처리됩니다. +모든 검증은 잘 확립되어 있고 견고한 **Pydantic**이 처리합니다. -### 보안과 인증 +### 보안과 인증 { #security-and-authentication } -보안과 인증이 통합되어 있습니다. 데이터베이스나 데이터 모델과의 타협없이 사용할 수 있습니다. +보안과 인증이 통합되어 있습니다. 데이터베이스나 데이터 모델과 타협하지 않습니다. -다음을 포함하는, 모든 보안 스키마가 OpenAPI에 정의되어 있습니다. +다음을 포함해 OpenAPI에 정의된 모든 보안 스키마: * HTTP Basic. -* **OAuth2** (**JWT tokens** 또한 포함). [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=\_blank}에 있는 자습서를 확인해 보세요. +* **OAuth2**(**JWT tokens** 또한 포함). [JWT를 사용한 OAuth2](tutorial/security/oauth2-jwt.md){.internal-link target=_blank} 자습서를 확인해 보세요. * 다음에 들어 있는 API 키: * 헤더. - * 매개변수. - * 쿠키 및 그 외. + * 쿼리 매개변수. + * 쿠키 등. -추가적으로 (**세션 쿠키**를 포함한) 모든 보안 기능은 Starlette에 있습니다. +추가로 Starlette의 모든 보안 기능(**세션 쿠키** 포함)도 제공합니다. -모두 재사용할 수 있는 도구와 컴포넌트로 만들어져 있어 여러분의 시스템, 데이터 저장소, 관계형 및 NoSQL 데이터베이스 등과 쉽게 통합할 수 있습니다. +모두 재사용 가능한 도구와 컴포넌트로 만들어져 있어, 여러분의 시스템, 데이터 저장소, 관계형 및 NoSQL 데이터베이스 등과 쉽게 통합할 수 있습니다. -### 의존성 주입 +### 의존성 주입 { #dependency-injection } -FastAPI는 사용하기 매우 간편하지만, 엄청난 <abbr title='"컴포넌트", "자원", "서비스", "제공자"로도 알려진'><strong>의존성 주입</strong></abbr>시스템을 포함하고 있습니다. +FastAPI는 사용하기 매우 쉽지만, 매우 강력한 <abbr title='또한 다음으로도 불립니다: "컴포넌트", "자원", "서비스", "제공자"'><strong>Dependency Injection</strong></abbr> 시스템을 포함하고 있습니다. -* 의존성은 의존성을 가질수도 있어, 이를 통해 의존성의 계층이나 **의존성의 "그래프"**를 형성합니다. -* 모든 것이 프레임워크에 의해 **자동적으로 처리됩니다**. -* 모든 의존성은 요청에서 데이터를 요구하여 자동 문서화와 **경로 작동 제약을 강화할 수 있습니다**. -* 의존성에서 정의된 _경로 작동_ 매개변수에 대해서도 **자동 검증**이 이루어 집니다. -* 복잡한 사용자의 인증 시스템, **데이터베이스 연결**, 등등을 지원합니다. -* 데이터베이스, 프론트엔드 등과 관련되어 **타협하지 않아도 됩니다**. 하지만 그 모든 것과 쉽게 통합이 가능합니다. +* 의존성도 의존성을 가질 수 있어, 의존성의 계층 또는 **의존성의 "그래프"**를 생성합니다. +* 모든 것이 프레임워크에 의해 **자동으로 처리됩니다**. +* 모든 의존성은 요청에서 데이터를 요구할 수 있으며, **경로 처리** 제약과 자동 문서화를 강화할 수 있습니다. +* 의존성에 정의된 *경로 처리* 매개변수에 대해서도 **자동 검증**을 합니다. +* 복잡한 사용자 인증 시스템, **데이터베이스 연결** 등을 지원합니다. +* 데이터베이스, 프론트엔드 등과 **타협하지 않습니다**. 하지만 모두와 쉽게 통합할 수 있습니다. -### 제한 없는 "플러그인" +### 제한 없는 "플러그인" { #unlimited-plug-ins } -또는 다른 방법으로, 그것들을 사용할 필요 없이 필요한 코드만 임포트할 수 있습니다. +또 다른 방식으로는, 그것들이 필요 없습니다. 필요한 코드를 임포트해서 사용하면 됩니다. -어느 통합도 (의존성과 함께) 사용하기 쉽게 설계되어 있어, *경로 작동*에 사용된 것과 동일한 구조와 문법을 사용하여 2줄의 코드로 여러분의 어플리케이션에 사용할 "플러그인"을 만들 수 있습니다. +어떤 통합이든(의존성과 함께) 사용하기 매우 간단하도록 설계되어 있어, *경로 처리*에 사용된 것과 동일한 구조와 문법을 사용해 2줄의 코드로 애플리케이션용 "플러그인"을 만들 수 있습니다. -### 테스트 결과 +### 테스트됨 { #tested } -* 100% <abbr title="자동적으로 테스트된 코드의 양">테스트 범위</abbr>. -* 100% <abbr title="파이썬의 타입 어노테이션, 이를 통해 여러분의 편집기와 외부 도구는 여러분에게 더 나은 지원을 할 수 있습니다">타입이 명시된</abbr> 코드 베이스. -* 상용 어플리케이션에서의 사용. +* 100% <abbr title="자동으로 테스트되는 코드의 양">test coverage</abbr>. +* 100% <abbr title="Python 타입 어노테이션으로, 이를 통해 편집기와 외부 도구가 더 나은 지원을 제공할 수 있습니다">type annotated</abbr> 코드 베이스. +* 프로덕션 애플리케이션에서 사용됩니다. -## Starlette 기능 +## Starlette 기능 { #starlette-features } -**FastAPI**는 <a href="https://www.starlette.dev/" class="external-link" target="_blank"><strong>Starlette</strong></a>를 기반으로 구축되었으며, 이와 완전히 호환됩니다. 따라서, 여러분이 보유하고 있는 어떤 추가적인 Starlette 코드도 작동할 것입니다. +**FastAPI**는 <a href="https://www.starlette.dev/" class="external-link" target="_blank"><strong>Starlette</strong></a>와 완전히 호환되며(또한 이를 기반으로 합니다). 따라서 추가로 가지고 있는 Starlette 코드도 모두 동작합니다. -`FastAPI`는 실제로 `Starlette`의 하위 클래스입니다. 그래서, 여러분이 이미 Starlette을 알고 있거나 사용하고 있으면, 대부분의 기능이 같은 방식으로 작동할 것입니다. +`FastAPI`는 실제로 `Starlette`의 하위 클래스입니다. 그래서 Starlette을 이미 알고 있거나 사용하고 있다면, 대부분의 기능이 같은 방식으로 동작할 것입니다. -**FastAPI**를 사용하면 여러분은 **Starlette**의 기능 대부분을 얻게 될 것입니다(FastAPI가 단순히 Starlette를 강화했기 때문입니다): +**FastAPI**를 사용하면 **Starlette**의 모든 기능을 얻게 됩니다(FastAPI는 Starlette에 강력한 기능을 더한 것입니다): -* 아주 인상적인 성능. 이는 <a href="https://github.com/encode/starlette#performance" class="external-link" target="_blank">**NodeJS**와 **Go**와 동등하게 사용 가능한 가장 빠른 파이썬 프레임워크 중 하나입니다</a>. +* 정말 인상적인 성능. <a href="https://github.com/encode/starlette#performance" class="external-link" target="_blank">**NodeJS**와 **Go**에 버금가는, 사용 가능한 가장 빠른 Python 프레임워크 중 하나입니다</a>. * **WebSocket** 지원. -* 프로세스 내의 백그라운드 작업. -* 시작과 종료 이벤트. +* 프로세스 내 백그라운드 작업. +* 시작 및 종료 이벤트. * HTTPX 기반 테스트 클라이언트. * **CORS**, GZip, 정적 파일, 스트리밍 응답. * **세션과 쿠키** 지원. -* 100% 테스트 범위. -* 100% 타입이 명시된 코드 베이스. +* 100% test coverage. +* 100% type annotated codebase. -## Pydantic 기능 +## Pydantic 기능 { #pydantic-features } -**FastAPI**는 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank"><strong>Pydantic</strong></a>을 기반으로 하며 Pydantic과 완벽하게 호환됩니다. 그래서 어느 추가적인 Pydantic 코드를 여러분이 가지고 있든 작동할 것입니다. +**FastAPI**는 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank"><strong>Pydantic</strong></a>과 완벽하게 호환되며(또한 이를 기반으로 합니다). 따라서 추가로 가지고 있는 Pydantic 코드도 모두 동작합니다. -Pydantic을 기반으로 하는, 데이터베이스를 위한 <abbr title="Object-Relational Mapper">ORM</abbr>, <abbr title="Object-Document Mapper">ODM</abbr>을 포함한 외부 라이브러리를 포함합니다. +데이터베이스를 위한 <abbr title="Object-Relational Mapper - 객체-관계 매퍼">ORM</abbr>, <abbr title="Object-Document Mapper - 객체-문서 매퍼">ODM</abbr>과 같은, Pydantic을 기반으로 하는 외부 라이브러리도 포함합니다. -이는 모든 것이 자동으로 검증되기 때문에, 많은 경우에서 요청을 통해 얻은 동일한 객체를, **직접 데이터베이스로** 넘겨줄 수 있습니다. +이는 모든 것이 자동으로 검증되기 때문에, 많은 경우 요청에서 얻은 동일한 객체를 **직접 데이터베이스로** 넘겨줄 수 있다는 의미이기도 합니다. -반대로도 마찬가지이며, 많은 경우에서 여러분은 **직접 클라이언트로** 그저 객체를 넘겨줄 수 있습니다. +반대로도 마찬가지이며, 많은 경우 데이터베이스에서 얻은 객체를 **직접 클라이언트로** 그대로 넘겨줄 수 있습니다. -**FastAPI**를 사용하면 (모든 데이터 처리를 위해 FastAPI가 Pydantic을 기반으로 하기 있기에) **Pydantic**의 모든 기능을 얻게 됩니다: +**FastAPI**를 사용하면(모든 데이터 처리를 위해 FastAPI가 Pydantic을 기반으로 하기에) **Pydantic**의 모든 기능을 얻게 됩니다: -* **어렵지 않은 언어**: - * 새로운 스키마 정의 마이크로 언어를 배우지 않아도 됩니다. - * 여러분이 파이썬 타입을 안다면, 여러분은 Pydantic을 어떻게 사용하는지 아는 겁니다. -* 여러분의 **<abbr title="통합 개발 환경, 코드 편집기와 비슷합니다">IDE</abbr>/<abbr title="코드 에러를 확인하는 프로그램">린터</abbr>/뇌**와 잘 어울립니다: - * Pydantic 데이터 구조는 단순 여러분이 정의한 클래스의 인스턴스이기 때문에, 자동 완성, 린팅, mypy 그리고 여러분의 직관까지 여러분의 검증된 데이터와 올바르게 작동합니다. +* **No brainfuck**: + * 새로운 스키마 정의 마이크로 언어를 배울 필요가 없습니다. + * Python 타입을 알고 있다면 Pydantic 사용법도 알고 있는 것입니다. +* 여러분의 **<abbr title="Integrated Development Environment - 통합 개발 환경: 코드 편집기와 비슷합니다">IDE</abbr>/<abbr title="코드 오류를 확인하는 프로그램">linter</abbr>/뇌**와 잘 어울립니다: + * pydantic 데이터 구조는 여러분이 정의한 클래스 인스턴스일 뿐이므로, 자동 완성, 린팅, mypy, 그리고 직관까지도 검증된 데이터와 함께 제대로 작동합니다. * **복잡한 구조**를 검증합니다: - * 계층적인 Pydantic 모델, 파이썬 `typing`의 `List`와 `Dict`, 그 외를 사용합니다. - * 그리고 검증자는 복잡한 데이터 스키마를 명확하고 쉽게 정의 및 확인하며 JSON 스키마로 문서화합니다. - * 여러분은 깊게 **중첩된 JSON** 객체를 가질 수 있으며, 이 객체 모두 검증하고 설명을 붙일 수 있습니다. -* **확장 가능성**: - * Pydantic은 사용자 정의 데이터 타입을 정의할 수 있게 하거나, 검증자 데코레이터가 붙은 모델의 메소드를 사용하여 검증을 확장할 수 있습니다. -* 100% 테스트 범위. + * 계층적인 Pydantic 모델, Python `typing`의 `List`와 `Dict` 등을 사용합니다. + * 그리고 validator는 복잡한 데이터 스키마를 명확하고 쉽게 정의하고, 검사하고, JSON Schema로 문서화할 수 있게 해줍니다. + * 깊게 **중첩된 JSON** 객체를 가질 수 있으며, 이를 모두 검증하고 주석을 달 수 있습니다. +* **확장 가능**: + * Pydantic은 사용자 정의 데이터 타입을 정의할 수 있게 하거나, validator decorator가 붙은 모델 메서드로 검증을 확장할 수 있습니다. +* 100% test coverage. diff --git a/docs/ko/docs/help-fastapi.md b/docs/ko/docs/help-fastapi.md index b65ef959c..a4abbe7af 100644 --- a/docs/ko/docs/help-fastapi.md +++ b/docs/ko/docs/help-fastapi.md @@ -1,4 +1,4 @@ -# FastAPI 지원 - 도움 받기 +# FastAPI 지원 - 도움 받기 { #help-fastapi-get-help } **FastAPI** 가 마음에 드시나요? @@ -10,9 +10,9 @@ FastAPI, 다른 사용자, 개발자를 응원하고 싶으신가요? 또한 도움을 받을 수 있는 방법도 몇 가지 있습니다. -## 뉴스레터 구독 +## 뉴스레터 구독 { #subscribe-to-the-newsletter } -[**FastAPI and friends** 뉴스레터](newsletter.md){.internal-link target=\_blank}를 구독하여 최신 정보를 유지할 수 있습니다: +(자주 발송되지 않는) [**FastAPI and friends** 뉴스레터](newsletter.md){.internal-link target=_blank}를 구독하여 최신 정보를 유지할 수 있습니다: * FastAPI and friends에 대한 뉴스 🚀 * 가이드 📝 @@ -20,65 +20,65 @@ FastAPI, 다른 사용자, 개발자를 응원하고 싶으신가요? * 획기적인 변화 🚨 * 팁과 요령 ✅ -## 트위터에서 FastAPI 팔로우하기 +## X(Twitter)에서 FastAPI 팔로우하기 { #follow-fastapi-on-x-twitter } <a href="https://x.com/fastapi" class="external-link" target="_blank">**X (Twitter)**의 @fastapi를 팔로우</a>하여 **FastAPI** 에 대한 최신 뉴스를 얻을 수 있습니다. 🐦 -## Star **FastAPI** in GitHub +## GitHub에서 **FastAPI**에 Star 주기 { #star-fastapi-in-github } GitHub에서 FastAPI에 "star"를 붙일 수 있습니다 (오른쪽 상단의 star 버튼을 클릭): <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>. ⭐️ 스타를 늘림으로써, 다른 사용자들이 좀 더 쉽게 찾을 수 있고, 많은 사람들에게 유용한 것임을 나타낼 수 있습니다. -## GitHub 저장소에서 릴리즈 확인 +## 릴리즈 확인을 위해 GitHub 저장소 보기 { #watch-the-github-repository-for-releases } -GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 watch 버튼을 클릭): <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>. 👀 +GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 "watch" 버튼을 클릭): <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>. 👀 -여기서 "Releases only"을 선택할 수 있습니다. +여기서 "Releases only"를 선택할 수 있습니다. -이렇게하면, **FastAPI** 의 버그 수정 및 새로운 기능의 구현 등의 새로운 자료 (최신 버전)이 있을 때마다 (이메일) 통지를 받을 수 있습니다. +이렇게하면, **FastAPI** 의 버그 수정 및 새로운 기능의 구현 등의 새로운 릴리즈(새 버전)가 있을 때마다 (이메일) 통지를 받을 수 있습니다. -## 개발자와의 연결 +## 개발자와의 연결 { #connect-with-the-author } -<a href="https://tiangolo.com" class="external-link" target="_blank">개발자(Sebastián Ramírez / `tiangolo`)</a>와 연락을 취할 수 있습니다. +개발자(작성자)인 <a href="https://tiangolo.com" class="external-link" target="_blank">저(Sebastián Ramírez / `tiangolo`)</a>와 연락을 취할 수 있습니다. 여러분은 할 수 있습니다: -* <a href="https://github.com/tiangolo" class="external-link" target="_blank">**GitHub**에서 팔로우하기.</a>. - * 당신에게 도움이 될 저의 다른 오픈소스 프로젝트를 확인하십시오. +* <a href="https://github.com/tiangolo" class="external-link" target="_blank">**GitHub**에서 팔로우하기</a>. + * 여러분에게 도움이 될 저의 다른 오픈소스 프로젝트를 확인하십시오. * 새로운 오픈소스 프로젝트를 만들었을 때 확인하려면 팔로우 하십시오. * <a href="https://x.com/tiangolo" class="external-link" target="_blank">**X (Twitter)**</a> 또는 <a href="https://fosstodon.org/@tiangolo" class="external-link" target="_blank">Mastodon</a>에서 팔로우하기. * FastAPI의 사용 용도를 알려주세요 (그것을 듣는 것을 좋아합니다). * 발표나 새로운 툴 출시 소식을 받아보십시오. - * <a href="https://x.com/fastapi" class="external-link" target="_blank">**X (Twitter)**의 @fastapi를 팔로우</a> (별도 계정에서) 할 수 있습니다. -* <a href="https://www.linkedin.com/in/tiangolo/" class="external-link" target="_blank">**LinkedIn**에서 팔로우하기.</a>. - * 새로운 툴의 발표나 출시 소식을 받아보십시오. (단, X (Twitter)를 더 자주 사용합니다 🤷‍♂). + * <a href="https://x.com/fastapi" class="external-link" target="_blank">X(Twitter)에서 @fastapi를 팔로우</a> (별도 계정에서) 할 수 있습니다. +* <a href="https://www.linkedin.com/in/tiangolo/" class="external-link" target="_blank">**LinkedIn**에서 팔로우하기</a>. + * 새로운 툴의 발표나 출시 소식을 받아보십시오 (단, X (Twitter)를 더 자주 사용합니다 🤷‍♂). * <a href="https://dev.to/tiangolo" class="external-link" target="_blank">**Dev.to**</a> 또는 <a href="https://medium.com/@tiangolo" class="external-link" target="_blank">**Medium**</a>에서 제가 작성한 내용을 읽어 보십시오 (또는 팔로우). - * 다른 기사나 아이디어들을 읽고, 제가 만들어왔던 툴에 대해서도 읽으십시오. - * 새로운 기사를 읽기 위해 팔로우 하십시오. + * 다른 아이디어와 기사들을 읽고, 제가 만들어왔던 툴에 대해서도 읽으십시오. + * 새로운 내용을 게시할 때 읽기 위해 팔로우 하십시오. -## **FastAPI**에 대한 트윗 +## **FastAPI**에 대해 트윗하기 { #tweet-about-fastapi } -<a href="https://x.com/compose/tweet?text=I'm loving @fastapi because... https://github.com/fastapi/fastapi" class="external-link" target="_blank">**FastAPI**에 대해 트윗</a> 하고 FastAPI가 마음에 드는 이유를 알려주세요. 🎉 +<a href="https://x.com/compose/tweet?text=I'm loving @fastapi because... https://github.com/fastapi/fastapi" class="external-link" target="_blank">**FastAPI**에 대해 트윗</a> 하고 저와 다른 사람들에게 FastAPI가 마음에 드는 이유를 알려주세요. 🎉 **FastAPI**가 어떻게 사용되고 있는지, 어떤 점이 마음에 들었는지, 어떤 프로젝트/회사에서 사용하고 있는지 등에 대해 듣고 싶습니다. -## FastAPI에 투표하기 +## FastAPI에 투표하기 { #vote-for-fastapi } * <a href="https://www.slant.co/options/34241/~fastapi-review" class="external-link" target="_blank">Slant에서 **FastAPI** 에 대해 투표하십시오</a>. * <a href="https://alternativeto.net/software/fastapi/about/" class="external-link" target="_blank">AlternativeTo에서 **FastAPI** 에 대해 투표하십시오</a>. -* <a href="https://stackshare.io/pypi-fastapi" class="external-link" target="_blank">StackShare에서 **FastAPI** 에 대해 투표하십시오</a>. +* <a href="https://stackshare.io/pypi-fastapi" class="external-link" target="_blank">StackShare에서 **FastAPI**를 사용한다고 표시하세요</a>. -## GitHub의 이슈로 다른사람 돕기 +## GitHub에서 질문으로 다른 사람 돕기 { #help-others-with-questions-in-github } 다른 사람들의 질문에 도움을 줄 수 있습니다: -* <a href="https://github.com/fastapi/fastapi/discussions/categories/questions?discussions_q=category%3AQuestions+is%3Aunanswered" class="external-link" target="_blank">GitHub 디스커션</a> -* <a href="https://github.com/fastapi/fastapi/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Aquestion+-label%3Aanswered+" class="external-link" target="_blank">GitHub 이슈</a> +* <a href="https://github.com/fastapi/fastapi/discussions/categories/questions?discussions_q=category%3AQuestions+is%3Aunanswered" class="external-link" target="_blank">GitHub Discussions</a> +* <a href="https://github.com/fastapi/fastapi/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Aquestion+-label%3Aanswered+" class="external-link" target="_blank">GitHub Issues</a> 많은 경우, 여러분은 이미 그 질문에 대한 답을 알고 있을 수도 있습니다. 🤓 -만약 많은 사람들의 문제를 도와준다면, 공식적인 [FastAPI 전문가](fastapi-people.md#fastapi-experts){.internal-link target=\_blank} 가 될 것입니다. 🎉 +만약 많은 사람들의 질문을 도와준다면, 공식적인 [FastAPI 전문가](fastapi-people.md#fastapi-experts){.internal-link target=_blank}가 될 것입니다. 🎉 가장 중요한 점은: 친절하려고 노력하는 것입니다. 사람들은 좌절감을 안고 오며, 많은 경우 최선의 방식으로 질문하지 않을 수도 있습니다. 하지만 최대한 친절하게 대하려고 노력하세요. 🤗 @@ -86,183 +86,170 @@ GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 watch 버 --- -다른 사람들의 질문 (디스커션 또는 이슈에서) 해결을 도울 수 있는 방법은 다음과 같습니다. +다른 사람들의 질문(디스커션 또는 이슈에서) 해결을 도울 수 있는 방법은 다음과 같습니다. -### 질문 이해하기 +### 질문 이해하기 { #understand-the-question } * 질문하는 사람이 가진 **목적**과 사용 사례를 이해할 수 있는지 확인하세요. -* 질문 (대부분은 질문입니다)이 **명확**한지 확인하세요. +* 그런 다음 질문(대부분은 질문입니다)이 **명확**한지 확인하세요. -* 많은 경우, 사용자가 가정한 해결책에 대한 질문을 하지만, 더 **좋은** 해결책이 있을 수 있습니다. 문제와 사용 사례를 더 잘 이해하면 더 나은 **대안적인 해결책**을 제안할 수 있습니다. +* 많은 경우 사용자가 상상한 해결책에 대한 질문을 하지만, 더 **좋은** 해결책이 있을 수 있습니다. 문제와 사용 사례를 더 잘 이해하면 더 나은 **대안적인 해결책**을 제안할 수 있습니다. * 질문을 이해할 수 없다면, 더 **자세한 정보**를 요청하세요. -### 문제 재현하기 +### 문제 재현하기 { #reproduce-the-problem } -대부분의 경우, 질문은 질문자의 **원본 코드**와 관련이 있습니다. +대부분의 경우 그리고 대부분의 질문에서는 질문자의 **원본 코드**와 관련된 내용이 있습니다. 많은 경우, 코드의 일부만 복사해서 올리지만, 그것만으로는 **문제를 재현**하기에 충분하지 않습니다. -* 질문자에게 <a href="https://stackoverflow.com/help/minimal-reproducible-example" class="external-link" target="_blank">최소한의 재현 가능한 예제</a>를 제공해달라고 요청하세요. 이렇게 하면 코드를 **복사-붙여넣기**하여 직접 실행하고, 동일한 오류나 동작을 확인하거나 사용 사례를 더 잘 이해할 수 있습니다. +* 질문자에게 <a href="https://stackoverflow.com/help/minimal-reproducible-example" class="external-link" target="_blank">최소한의 재현 가능한 예제</a>를 제공해달라고 요청할 수 있습니다. 이렇게 하면 코드를 **복사-붙여넣기**하여 로컬에서 실행하고, 질문자가 보고 있는 것과 동일한 오류나 동작을 확인하거나 사용 사례를 더 잘 이해할 수 있습니다. -* 너그러운 마음이 든다면, 문제 설명만을 기반으로 직접 **예제를 만들어**볼 수도 있습니다. 하지만, 이는 시간이 많이 걸릴 수 있으므로, 먼저 질문을 명확히 해달라고 요청하는 것이 좋습니다. +* 너그러운 마음이 든다면, 문제 설명만을 기반으로 직접 **예제를 만들어**볼 수도 있습니다. 다만 이는 시간이 많이 걸릴 수 있으므로, 먼저 문제를 명확히 해달라고 요청하는 것이 더 나을 수 있다는 점을 기억하세요. -### 해결책 제안하기 +### 해결책 제안하기 { #suggest-solutions } * 질문을 충분히 이해한 후에는 가능한 **답변**을 제공할 수 있습니다. -* 많은 경우, 질문자의 **근본적인 문제나 사용 사례**를 이해하는 것이 중요합니다. 그들이 시도하는 방법보다 더 나은 해결책이 있을 수 있기 때문입니다. +* 많은 경우, 질문자의 **근본적인 문제나 사용 사례**를 이해하는 것이 더 좋습니다. 그들이 시도하는 방법보다 더 나은 해결책이 있을 수 있기 때문입니다. -### 해결 요청하기 +### 종료 요청하기 { #ask-to-close } -질문자가 답변을 확인하고 나면, 당신이 문제를 해결했을 가능성이 높습니다. 축하합니다, **당신은 영웅입니다**! 🦸 +질문자가 답변을 하면, 여러분이 문제를 해결했을 가능성이 높습니다. 축하합니다, **여러분은 영웅입니다**! 🦸 * 이제 문제를 해결했다면, 질문자에게 다음을 요청할 수 있습니다. - * GitHub 디스커션에서: 댓글을 **답변**으로 표시하도록 요청하세요. - * GitHub 이슈에서: 이슈를 **닫아달라고** 요청하세요. + * GitHub Discussions에서: 댓글을 **답변**으로 표시하도록 요청하세요. + * GitHub Issues에서: 이슈를 **닫아달라고** 요청하세요. -## GitHub 저장소 보기 +## GitHub 저장소 보기 { #watch-the-github-repository } -GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 watch 버튼을 클릭): <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>. 👀 +GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 "watch" 버튼을 클릭): <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>. 👀 -"Releases only" 대신 "Watching"을 선택하면, 새로운 이슈나 질문이 생성될 때 알림을 받을 수 있습니다. 또한, 특정하게 새로운 이슈, 디스커션, PR 등만 알림 받도록 설정할 수도 있습니다. +"Releases only" 대신 "Watching"을 선택하면 누군가가 새 이슈나 질문을 만들 때 알림을 받게 됩니다. 또한 새 이슈, 디스커션, PR 등만 알림을 받도록 지정할 수도 있습니다. -그런 다음 이런 이슈들을 해결 할 수 있도록 도움을 줄 수 있습니다. +그런 다음 이런 질문들을 해결하도록 도와줄 수 있습니다. -## 이슈 생성하기 +## 질문하기 { #ask-questions } -GitHub 저장소에 <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">새로운 이슈 생성</a>을 할 수 있습니다, 예를들면 다음과 같습니다: +GitHub 저장소에서 <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">새 질문을 생성</a>할 수 있습니다. 예를 들면 다음과 같습니다: * **질문**을 하거나 **문제**에 대해 질문합니다. * 새로운 **기능**을 제안 합니다. -**참고**: 만약 이슈를 생성한다면, 저는 여러분에게 다른 사람들을 도와달라고 부탁할 것입니다. 😉 +**참고**: 만약 이렇게 한다면, 저는 여러분에게 다른 사람들도 도와달라고 요청할 것입니다. 😉 -## Pull Requests 리뷰하기 +## Pull Request 리뷰하기 { #review-pull-requests } -다른 사람들의 pull request를 리뷰하는 데 도움을 줄 수 있습니다. +다른 사람들이 만든 pull request를 리뷰하는 데 저를 도와줄 수 있습니다. 다시 한번 말하지만, 최대한 친절하게 리뷰해 주세요. 🤗 --- -Pull Rrquest를 리뷰할 때 고려해야 할 사항과 방법은 다음과 같습니다: +Pull request를 리뷰할 때 고려해야 할 사항과 방법은 다음과 같습니다: -### 문제 이해하기 +### 문제 이해하기 { #understand-the-problem } -* 먼저, 해당 pull request가 해결하려는 **문제를 이해하는지** 확인하세요. GitHub 디스커션 또는 이슈에서 더 긴 논의가 있었을 수도 있습니다. +* 먼저, 해당 pull request가 해결하려는 **문제를 이해하는지** 확인하세요. GitHub Discussion 또는 이슈에서 더 긴 논의가 있었을 수도 있습니다. -* Pull request가 필요하지 않을 가능성도 있습니다. **다른 방식**으로 문제를 해결할 수 있다면, 그 방법을 제안하거나 질문할 수 있습니다. +* Pull request가 실제로 필요하지 않을 가능성도 큽니다. 문제가 **다른 방식**으로 해결될 수 있기 때문입니다. 그런 경우 그 방법을 제안하거나 질문할 수 있습니다. -### 스타일에 너무 신경 쓰지 않기 +### 스타일에 너무 신경 쓰지 않기 { #dont-worry-about-style } -* 커밋 메시지 스타일 같은 것에 너무 신경 쓰지 않아도 됩니다. 저는 직접 커밋을 수정하여 squash and merge를 수행할 것입니다. +* 커밋 메시지 스타일 같은 것에 너무 신경 쓰지 마세요. 저는 커밋을 수동으로 조정해서 squash and merge를 할 것입니다. * 코드 스타일 규칙도 걱정할 필요 없습니다. 이미 자동화된 도구들이 이를 검사하고 있습니다. -스타일이나 일관성 관련 요청이 필요한 경우, 제가 직접 요청하거나 필요한 변경 사항을 추가 커밋으로 수정할 것입니다. +그리고 다른 스타일이나 일관성 관련 필요 사항이 있다면, 제가 직접 요청하거나 필요한 변경 사항을 위에 커밋으로 추가할 것입니다. -### 코드 확인하기 +### 코드 확인하기 { #check-the-code } -* 코드를 읽고, **논리적으로 타당**한지 확인한 후 로컬에서 실행하여 문제가 해결되는지 확인하세요. +* 코드를 확인하고 읽어서 말이 되는지 보고, **로컬에서 실행**해 실제로 문제가 해결되는지 확인하세요. -* 그런 다음, 확인했다고 **댓글**을 남겨 주세요. 그래야 제가 검토했음을 알 수 있습니다. +* 그런 다음 그렇게 했다고 **댓글**로 남겨 주세요. 그래야 제가 정말로 확인했음을 알 수 있습니다. -/// info +/// info | 정보 불행히도, 제가 단순히 여러 개의 승인만으로 PR을 신뢰할 수는 없습니다. -3개, 5개 이상의 승인이 달린 PR이 실제로는 깨져 있거나, 버그가 있거나, 주장하는 문제를 해결하지 못하는 경우가 여러 번 있었습니다. 😅 +여러 번, 설명이 그럴듯해서인지 3개, 5개 이상의 승인이 달린 PR이 있었지만, 제가 확인해보면 실제로는 깨져 있거나, 버그가 있거나, 주장하는 문제를 해결하지 못하는 경우가 있었습니다. 😅 따라서, 정말로 코드를 읽고 실행한 뒤, 댓글로 확인 내용을 남겨 주는 것이 매우 중요합니다. 🤓 /// -* PR을 더 단순하게 만들 수 있다면 그렇게 요청할 수 있지만, 너무 까다로울 필요는 없습니다. 주관적인 견해가 많이 있을 수 있기 때문입니다 (그리고 저도 제 견해가 있을 거예요 🙈). 따라서 핵심적인 부분에 집중하는 것이 좋습니다. +* PR을 더 단순하게 만들 수 있다면 그렇게 요청할 수 있지만, 너무 까다로울 필요는 없습니다. 주관적인 견해가 많이 있을 수 있기 때문입니다(그리고 저도 제 견해가 있을 거예요 🙈). 따라서 핵심적인 부분에 집중하는 것이 좋습니다. -### 테스트 +### 테스트 { #tests } * PR에 **테스트**가 포함되어 있는지 확인하는 데 도움을 주세요. -* PR을 적용하기 전에 테스트가 **실패**하는지 확인하세요. 🚨 +* PR 전에는 테스트가 **실패**하는지 확인하세요. 🚨 -* PR을 적용한 후 테스트가 **통과**하는지 확인하세요. ✅ +* 그런 다음 PR 후에는 테스트가 **통과**하는지 확인하세요. ✅ -* 많은 PR에는 테스트가 없습니다. 테스트를 추가하도록 **상기**시켜줄 수도 있고, 직접 테스트를 **제안**할 수도 있습니다. 이는 시간이 많이 소요되는 부분 중 하나이며, 그 부분을 많이 도와줄 수 있습니다. +* 많은 PR에는 테스트가 없습니다. 테스트를 추가하도록 **상기**시켜줄 수도 있고, 직접 테스트를 **제안**할 수도 있습니다. 이는 시간이 가장 많이 드는 것들 중 하나이며, 그 부분을 많이 도와줄 수 있습니다. * 그리고 시도한 내용을 댓글로 남겨주세요. 그러면 제가 확인했다는 걸 알 수 있습니다. 🤓 -## Pull Request를 만드십시오 +## Pull Request 만들기 { #create-a-pull-request } -Pull Requests를 이용하여 소스코드에 [컨트리뷰트](contributing.md){.internal-link target=\_blank} 할 수 있습니다. 예를 들면 다음과 같습니다: +Pull Requests를 이용하여 소스 코드에 [기여](contributing.md){.internal-link target=_blank}할 수 있습니다. 예를 들면 다음과 같습니다: * 문서에서 발견한 오타를 수정할 때. -* FastAPI 관련 문서, 비디오 또는 팟캐스트를 작성했거나 발견하여 <a href="https://github.com/fastapi/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">이 파일을 편집하여</a> 공유할 때. +* FastAPI에 대한 글, 비디오, 팟캐스트를 작성했거나 발견했다면 <a href="https://github.com/fastapi/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">이 파일을 편집</a>하여 공유할 때. * 해당 섹션의 시작 부분에 링크를 추가해야 합니다. -* 당신의 언어로 [문서 번역하는데](contributing.md#translations){.internal-link target=\_blank} 기여할 때. +* 여러분의 언어로 [문서 번역에](contributing.md#translations){.internal-link target=_blank} 도움을 줄 때. * 다른 사람이 작성한 번역을 검토하는 것도 도울 수 있습니다. -* 새로운 문서의 섹션을 제안할 때. -* 기존 문제/버그를 수정할 때. +* 새로운 문서 섹션을 제안할 때. +* 기존 이슈/버그를 수정할 때. * 테스트를 반드시 추가해야 합니다. -* 새로운 feature를 추가할 때. +* 새로운 기능을 추가할 때. * 테스트를 반드시 추가해야 합니다. - * 관련 문서가 필요하다면 반드시 추가해야 합니다. + * 관련 문서가 있다면 반드시 추가해야 합니다. -## FastAPI 유지 관리에 도움 주기 +## FastAPI 유지 관리 돕기 { #help-maintain-fastapi } -**FastAPI**의 유지 관리를 도와주세요! 🤓 +**FastAPI** 유지를 도와주세요! 🤓 -할 일이 많고, 그 중 대부분은 **여러분**이 할 수 있습니다. +할 일이 많고, 그중 대부분은 **여러분**이 할 수 있습니다. 지금 할 수 있는 주요 작업은: -* [GitHub에서 다른 사람들의 질문에 도움 주기](#github_1){.internal-link target=_blank} (위의 섹션을 참조하세요). -* [Pull Request 리뷰하기](#pull-requests){.internal-link target=_blank} (위의 섹션을 참조하세요). +* [GitHub에서 질문으로 다른 사람 돕기](#help-others-with-questions-in-github){.internal-link target=_blank} (위의 섹션을 참조하세요). +* [Pull Request 리뷰하기](#review-pull-requests){.internal-link target=_blank} (위의 섹션을 참조하세요). -이 두 작업이 **가장 많은 시간을 소모**하는 일입니다. 그것이 FastAPI 유지 관리의 주요 작업입니다. +이 두 작업이 **가장 많은 시간을 소모**합니다. 이것이 FastAPI를 유지 관리하는 주요 작업입니다. -이 작업을 도와주신다면, **FastAPI 유지 관리에 도움을 주는 것**이며 그것이 **더 빠르고 더 잘 발전하는 것**을 보장하는 것입니다. 🚀 +이 작업을 도와주신다면, **FastAPI 유지를 돕는 것**이며 FastAPI가 **더 빠르고 더 잘 발전하는 것**을 보장하는 것입니다. 🚀 -## 채팅에 참여하십시오 +## 채팅에 참여하기 { #join-the-chat } -👥 <a href="https://discord.gg/VQjSZaeJmf" class="external-link" target="_blank">디스코드 채팅 서버</a> 👥 에 가입하고 FastAPI 커뮤니티에서 다른 사람들과 어울리세요. +👥 <a href="https://discord.gg/VQjSZaeJmf" class="external-link" target="_blank">Discord 채팅 서버</a> 👥 에 참여해서 FastAPI 커뮤니티의 다른 사람들과 어울리세요. -/// tip +/// tip | 팁 -질문이 있는 경우, <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">GitHub 디스커션</a> 에서 질문하십시오, [FastAPI Experts](fastapi-people.md#fastapi-experts){.internal-link target=_blank} 의 도움을 받을 가능성이 높습니다. +질문은 <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">GitHub Discussions</a>에서 하세요. [FastAPI Experts](fastapi-people.md#fastapi-experts){.internal-link target=_blank}로부터 도움을 받을 가능성이 훨씬 높습니다. -다른 일반적인 대화에서만 채팅을 사용하십시오. +채팅은 다른 일반적인 대화를 위해서만 사용하세요. /// -### 질문을 위해 채팅을 사용하지 마십시오 +### 질문을 위해 채팅을 사용하지 마세요 { #dont-use-the-chat-for-questions } -채팅은 더 많은 "자유로운 대화"를 허용하기 때문에, 너무 일반적인 질문이나 대답하기 어려운 질문을 쉽게 질문을 할 수 있으므로, 답변을 받지 못할 수 있습니다. +채팅은 더 많은 "자유로운 대화"를 허용하기 때문에, 너무 일반적인 질문이나 답하기 어려운 질문을 쉽게 할 수 있어 답변을 받지 못할 수도 있다는 점을 기억하세요. -GitHub 이슈에서의 템플릿은 올바른 질문을 작성하도록 안내하여 더 쉽게 좋은 답변을 얻거나 질문하기 전에 스스로 문제를 해결할 수도 있습니다. 그리고 GitHub에서는 시간이 조금 걸리더라도 항상 모든 것에 답할 수 있습니다. 채팅 시스템에서는 개인적으로 그렇게 할 수 없습니다. 😅 +GitHub에서는 템플릿이 올바른 질문을 작성하도록 안내하여 더 쉽게 좋은 답변을 얻거나, 질문하기 전에 스스로 문제를 해결할 수도 있습니다. 그리고 GitHub에서는 시간이 조금 걸리더라도 제가 항상 모든 것에 답하도록 보장할 수 있습니다. 채팅 시스템에서는 제가 개인적으로 그렇게 할 수 없습니다. 😅 -채팅 시스템에서의 대화 또한 GitHub에서 처럼 쉽게 검색할 수 없기 때문에 대화 중에 질문과 답변이 손실될 수 있습니다. 그리고 GitHub 이슈에 있는 것만 [FastAPI Expert](fastapi-people.md#fastapi-experts){.internal-link target=_blank}가 되는 것으로 간주되므로, GitHub 이슈에서 더 많은 관심을 받을 것입니다. +채팅 시스템에서의 대화 또한 GitHub만큼 쉽게 검색할 수 없기 때문에, 질문과 답변이 대화 속에서 사라질 수 있습니다. 그리고 GitHub에 있는 것만 [FastAPI Expert](fastapi-people.md#fastapi-experts){.internal-link target=_blank}가 되는 것으로 인정되므로, GitHub에서 더 많은 관심을 받게 될 가능성이 큽니다. -반면, 채팅 시스템에는 수천 명의 사용자가 있기 때문에, 거의 항상 대화 상대를 찾을 가능성이 높습니다. 😄 +반면, 채팅 시스템에는 수천 명의 사용자가 있으므로, 거의 항상 대화 상대를 찾을 가능성이 높습니다. 😄 -## 개발자 스폰서가 되십시오 +## 개발자 스폰서 되기 { #sponsor-the-author } -<a href="https://github.com/sponsors/tiangolo" class="external-link" target="_blank">GitHub 스폰서</a> 를 통해 개발자를 경제적으로 지원할 수 있습니다. - -감사하다는 말로 커피를 ☕️ 한잔 사줄 수 있습니다. 😄 - -또한 FastAPI의 실버 또는 골드 스폰서가 될 수 있습니다. 🏅🎉 - -## FastAPI를 강화하는 도구의 스폰서가 되십시오 - -문서에서 보았듯이, FastAPI는 Starlette과 Pydantic 라는 거인의 어깨에 타고 있습니다. - -다음의 스폰서가 될 수 있습니다 - -* <a href="https://github.com/sponsors/samuelcolvin" class="external-link" target="_blank">Samuel Colvin (Pydantic)</a> -* <a href="https://github.com/sponsors/encode" class="external-link" target="_blank">Encode (Starlette, Uvicorn)</a> +여러분의 **제품/회사**가 **FastAPI**에 의존하거나 관련되어 있고, FastAPI 사용자를 대상으로 알리고 싶다면 <a href="https://github.com/sponsors/tiangolo" class="external-link" target="_blank">GitHub sponsors</a>를 통해 개발자(저)를 스폰서할 수 있습니다. 티어에 따라 문서에 배지 같은 추가 혜택을 받을 수도 있습니다. 🎁 --- diff --git a/docs/ko/docs/history-design-future.md b/docs/ko/docs/history-design-future.md index 98f01d70d..d97200121 100644 --- a/docs/ko/docs/history-design-future.md +++ b/docs/ko/docs/history-design-future.md @@ -1,81 +1,79 @@ -# 역사, 디자인 그리고 미래 +# 역사, 디자인 그리고 미래 { #history-design-and-future } -어느 날, [한 FastAPI 사용자](https://github.com/fastapi/fastapi/issues/3#issuecomment-454956920)가 이렇게 물었습니다: +얼마 전, <a href="https://github.com/fastapi/fastapi/issues/3#issuecomment-454956920" class="external-link" target="_blank">한 **FastAPI** 사용자가 이렇게 물었습니다</a>: -> 이 프로젝트의 역사를 알려 주실 수 있나요? 몇 주 만에 멋진 결과를 낸 것 같아요. [...] +> 이 프로젝트의 역사는 무엇인가요? 몇 주 만에 아무 데서도 갑자기 나타나 엄청나게 좋아진 것처럼 보이네요 [...] 여기서 그 역사에 대해 간단히 설명하겠습니다. ---- +## 대안 { #alternatives } -## 대안 +저는 여러 해 동안 복잡한 요구사항(머신러닝, 분산 시스템, 비동기 작업, NoSQL 데이터베이스 등)을 가진 API를 만들면서 여러 개발 팀을 이끌어 왔습니다. -저는 여러 해 동안 머신러닝, 분산 시스템, 비동기 작업, NoSQL 데이터베이스 같은 복잡한 요구사항을 가진 API를 개발하며 여러 팀을 이끌어 왔습니다. +그 과정에서 많은 대안을 조사하고, 테스트하고, 사용해야 했습니다. -이 과정에서 많은 대안을 조사하고, 테스트하며, 사용해야 했습니다. **FastAPI**의 역사는 그 이전에 나왔던 여러 도구의 역사와 밀접하게 연관되어 있습니다. +**FastAPI**의 역사는 상당 부분 그 이전에 있던 도구들의 역사입니다. [대안](alternatives.md){.internal-link target=_blank} 섹션에서 언급된 것처럼: -> **FastAPI**는 이전에 나왔던 많은 도구들의 노력 없이는 존재하지 않았을 것입니다. -> -> 이전에 개발된 여러 도구들이 이 프로젝트에 영감을 주었습니다. -> -> 저는 오랫동안 새로운 프레임워크를 만드는 것을 피하고자 했습니다. 처음에는 **FastAPI**가 제공하는 기능들을 다양한 프레임워크와 플러그인, 도구들을 조합해 해결하려 했습니다. -> -> 하지만 결국에는 이 모든 기능을 통합하는 도구가 필요해졌습니다. 이전 도구들로부터 최고의 아이디어들을 모으고, 이를 최적의 방식으로 조합해야만 했습니다. 이는 :term:Python 3.6+ 타입 힌트 <type hints>와 같은, 이전에는 사용할 수 없었던 언어 기능이 가능했기 때문입니다. +<blockquote markdown="1"> ---- +**FastAPI**는 다른 사람들이 이전에 해온 작업이 없었다면 존재하지 않았을 것입니다. -## 조사 +그 전에 만들어진 많은 도구들이 이것의 탄생에 영감을 주었습니다. -여러 대안을 사용해 보며 다양한 도구에서 배운 점들을 모아 저와 개발팀에게 가장 적합한 방식을 찾았습니다. +저는 여러 해 동안 새로운 프레임워크를 만드는 것을 피하고 있었습니다. 처음에는 **FastAPI**가 다루는 모든 기능을 여러 다른 프레임워크, 플러그인, 도구들을 사용해 해결하려고 했습니다. -예를 들어, 표준 :term:Python 타입 힌트 <type hints>에 기반하는 것이 이상적이라는 점이 명확했습니다. +하지만 어느 시점에는, 이전 도구들의 최고의 아이디어를 가져와 가능한 한 최선의 방식으로 조합하고, 이전에는 존재하지 않았던 언어 기능(Python 3.6+ type hints)을 사용해 이 모든 기능을 제공하는 무언가를 만드는 것 외에는 다른 선택지가 없었습니다. -또한, 이미 존재하는 표준을 활용하는 것이 가장 좋은 접근법이라 판단했습니다. +</blockquote> -그래서 **FastAPI**의 코드를 작성하기 전에 몇 달 동안 OpenAPI, JSON Schema, OAuth2 명세를 연구하며 이들의 관계와 겹치는 부분, 차이점을 이해했습니다. +## 조사 { #investigation } ---- +이전의 모든 대안을 사용해 보면서, 각 도구로부터 배울 기회를 얻었고, 아이디어를 가져와 제가 일해온 개발 팀들과 저 자신에게 가장 적합하다고 찾은 방식으로 조합할 수 있었습니다. -## 디자인 +예를 들어, 이상적으로는 표준 Python 타입 힌트에 기반해야 한다는 점이 분명했습니다. -그 후, **FastAPI** 사용자가 될 개발자로서 사용하고 싶은 개발자 "API"를 디자인했습니다. +또한, 가장 좋은 접근법은 이미 존재하는 표준을 사용하는 것이었습니다. -[Python Developer Survey](https://www.jetbrains.com/research/python-developers-survey-2018/#development-tools)에 따르면 약 80%의 Python 개발자가 PyCharm, VS Code, Jedi 기반 편집기 등에서 개발합니다. 이 과정에서 여러 아이디어를 테스트했습니다. +그래서 **FastAPI**의 코딩을 시작하기도 전에, OpenAPI, JSON Schema, OAuth2 등과 같은 명세를 몇 달 동안 공부했습니다. 이들의 관계, 겹치는 부분, 차이점을 이해하기 위해서였습니다. -대부분의 다른 편집기도 유사하게 동작하기 때문에, **FastAPI**의 이점은 거의 모든 편집기에서 누릴 수 있습니다. +## 디자인 { #design } -이 과정을 통해 코드 중복을 최소화하고, 모든 곳에서 자동 완성, 타입 검사, 에러 확인 기능이 제공되는 최적의 방식을 찾아냈습니다. +그 다음에는 (FastAPI를 사용하는 개발자로서) 사용자로서 갖고 싶었던 개발자 "API"를 디자인하는 데 시간을 썼습니다. -이 모든 것은 개발자들에게 최고의 개발 경험을 제공하기 위해 설계되었습니다. +가장 인기 있는 Python 편집기들: PyCharm, VS Code, Jedi 기반 편집기에서 여러 아이디어를 테스트했습니다. ---- +약 80%의 사용자를 포함하는 최근 <a href="https://www.jetbrains.com/research/python-developers-survey-2018/#development-tools" class="external-link" target="_blank">Python Developer Survey</a>에 따르면 그렇습니다. -## 필요조건 +즉, **FastAPI**는 Python 개발자의 80%가 사용하는 편집기들로 특별히 테스트되었습니다. 그리고 대부분의 다른 편집기도 유사하게 동작하는 경향이 있으므로, 그 모든 이점은 사실상 모든 편집기에서 동작해야 합니다. -여러 대안을 테스트한 후, [Pydantic](https://docs.pydantic.dev/)을 사용하기로 결정했습니다. +그렇게 해서 코드 중복을 가능한 한 많이 줄이고, 어디서나 자동 완성, 타입 및 에러 검사 등을 제공하는 최선의 방법을 찾을 수 있었습니다. -이후 저는 **Pydantic**이 JSON Schema와 완벽히 호환되도록 개선하고, 다양한 제약 조건 선언을 지원하며, 여러 편집기에서의 자동 완성과 타입 검사 기능을 향상하기 위해 기여했습니다. +모든 개발자에게 최고의 개발 경험을 제공하는 방식으로 말입니다. -또한, 또 다른 주요 필요조건이었던 [Starlette](https://www.starlette.dev/)에도 기여했습니다. +## 필요조건 { #requirements } ---- +여러 대안을 테스트한 후, 장점 때문에 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">**Pydantic**</a>을 사용하기로 결정했습니다. -## 개발 +그 후, JSON Schema를 완전히 준수하도록 하고, 제약 조건 선언을 정의하는 다양한 방식을 지원하며, 여러 편집기에서의 테스트를 바탕으로 편집기 지원(타입 검사, 자동 완성)을 개선하기 위해 기여했습니다. -**FastAPI**를 개발하기 시작할 즈음에는 대부분의 준비가 이미 완료된 상태였습니다. 설계가 정의되었고, 필요조건과 도구가 준비되었으며, 표준과 명세에 대한 지식도 충분했습니다. +개발 과정에서, 또 다른 핵심 필요조건인 <a href="https://www.starlette.dev/" class="external-link" target="_blank">**Starlette**</a>에도 기여했습니다. ---- +## 개발 { #development } -## 미래 +**FastAPI** 자체를 만들기 시작했을 때쯤에는, 대부분의 조각들이 이미 갖춰져 있었고, 디자인은 정의되어 있었으며, 필요조건과 도구는 준비되어 있었고, 표준과 명세에 대한 지식도 명확하고 최신 상태였습니다. -현시점에서 **FastAPI**가 많은 사람들에게 유용하다는 것이 명백해졌습니다. +## 미래 { #future } -여러 용도에 더 적합한 도구로서 기존 대안보다 선호되고 있습니다. -이미 많은 개발자와 팀들이 **FastAPI**에 의존해 프로젝트를 진행 중입니다 (저와 제 팀도 마찬가지입니다). +이 시점에는, **FastAPI**가 그 아이디어와 함께 많은 사람들에게 유용하다는 것이 이미 분명합니다. -하지만 여전히 개선해야 할 점과 추가할 기능들이 많이 남아 있습니다. +많은 사용 사례에 더 잘 맞기 때문에 이전 대안들보다 선택되고 있습니다. + +많은 개발자와 팀이 이미 자신의 프로젝트를 위해 **FastAPI**에 의존하고 있습니다(저와 제 팀도 포함해서요). + +하지만 여전히, 앞으로 나올 개선 사항과 기능들이 많이 있습니다. + +**FastAPI**의 미래는 밝습니다. -**FastAPI**는 밝은 미래로 나아가고 있습니다. 그리고 [여러분의 도움](help-fastapi.md){.internal-link target=_blank}은 큰 힘이 됩니다. From 442d007e761fdee34ec01d2365d138d0a746c971 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:16:33 +0000 Subject: [PATCH 108/243] =?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 f1321b780..cf84ae9b5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Translations +* 🌐 Update translations for ko (update outdated, found by fixer tool). PR [#14738](https://github.com/fastapi/fastapi/pull/14738) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for de (update-outdated). PR [#14690](https://github.com/fastapi/fastapi/pull/14690) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update LLM prompt for Russian translations. PR [#14733](https://github.com/fastapi/fastapi/pull/14733) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for ru (update-outdated). PR [#14693](https://github.com/fastapi/fastapi/pull/14693) by [@tiangolo](https://github.com/tiangolo). From 7a0589466c1c0129ff7291ea0b663d52859d651d Mon Sep 17 00:00:00 2001 From: JUNG SEUNGHOON <seuthootdev@gmail.com> Date: Sun, 25 Jan 2026 06:17:54 +0900 Subject: [PATCH 109/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20`llm-prompt.md`?= =?UTF-8?q?=20for=20Korean=20language=20(#14763)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(ko): refine 'burger' to '햄버거' and update glossary * Update docs/ko/llm-prompt.md Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> * Add app and command to glossary Update glossary: add 애플리케이션 (app) and 명령어 (command) --------- Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- docs/ko/llm-prompt.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ko/llm-prompt.md b/docs/ko/llm-prompt.md index 533160eab..be2f5be5d 100644 --- a/docs/ko/llm-prompt.md +++ b/docs/ko/llm-prompt.md @@ -33,6 +33,9 @@ Use the following preferred translations when they apply in documentation prose: - response (HTTP): 응답 - path operation: 경로 처리 - path operation function: 경로 처리 함수 +- app: 애플리케이션 +- command: 명령어 +- burger: 햄버거 (NOT 버거) ### `///` admonitions From 8c32e91c10cbf13da8be4d27f03d9fdaa9a6730c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:18:15 +0000 Subject: [PATCH 110/243] =?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 cf84ae9b5..1fefe908b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Translations +* 🌐 Update `llm-prompt.md` for Korean language. PR [#14763](https://github.com/fastapi/fastapi/pull/14763) by [@seuthootDev](https://github.com/seuthootDev). * 🌐 Update translations for ko (update outdated, found by fixer tool). PR [#14738](https://github.com/fastapi/fastapi/pull/14738) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for de (update-outdated). PR [#14690](https://github.com/fastapi/fastapi/pull/14690) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update LLM prompt for Russian translations. PR [#14733](https://github.com/fastapi/fastapi/pull/14733) by [@YuriiMotov](https://github.com/YuriiMotov). From a3dccaeb14ed511ed548d88254b96131299851bf Mon Sep 17 00:00:00 2001 From: Vineet Kumar <108144301+whyvineet@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:53:25 +0530 Subject: [PATCH 111/243] =?UTF-8?q?=F0=9F=93=9D=20Fix=20minor=20typos=20in?= =?UTF-8?q?=20release=20notes=20(#14780)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Fix typos in release notes --- docs/en/docs/release-notes.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1fefe908b..62dae4da7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -283,7 +283,7 @@ hide: ### Refactors -* 🔥 Remove dangling extra condiitonal no longer needed. PR [#14435](https://github.com/fastapi/fastapi/pull/14435) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove dangling extra conditional no longer needed. PR [#14435](https://github.com/fastapi/fastapi/pull/14435) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor internals, update `is_coroutine` check to reuse internal supported variants (unwrap, check class). PR [#14434](https://github.com/fastapi/fastapi/pull/14434) by [@tiangolo](https://github.com/tiangolo). ### Translations @@ -418,7 +418,7 @@ hide: ### Docs -* 📝 Upate docs for advanced dependencies with `yield`, noting the changes in 0.121.0, adding `scope`. PR [#14287](https://github.com/fastapi/fastapi/pull/14287) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update docs for advanced dependencies with `yield`, noting the changes in 0.121.0, adding `scope`. PR [#14287](https://github.com/fastapi/fastapi/pull/14287) by [@tiangolo](https://github.com/tiangolo). ### Internal @@ -2646,7 +2646,7 @@ Read more in the [advisory: Content-Type Header ReDoS](https://github.com/tiango * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/handling-errors.md`. PR [#1953](https://github.com/tiangolo/fastapi/pull/1953) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/response-status-code.md`. PR [#1942](https://github.com/tiangolo/fastapi/pull/1942) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/extra-models.md`. PR [#1941](https://github.com/tiangolo/fastapi/pull/1941) by [@SwftAlpc](https://github.com/SwftAlpc). -* 🌐 Add Japanese tranlsation for `docs/ja/docs/tutorial/schema-extra-example.md`. PR [#1931](https://github.com/tiangolo/fastapi/pull/1931) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/schema-extra-example.md`. PR [#1931](https://github.com/tiangolo/fastapi/pull/1931) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-nested-models.md`. PR [#1930](https://github.com/tiangolo/fastapi/pull/1930) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-fields.md`. PR [#1923](https://github.com/tiangolo/fastapi/pull/1923) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add German translation for `docs/de/docs/tutorial/index.md`. PR [#9502](https://github.com/tiangolo/fastapi/pull/9502) by [@fhabers21](https://github.com/fhabers21). @@ -4022,7 +4022,7 @@ You hopefully updated to a supported version of Python a while ago. If you haven ### Fixes * 🐛 Fix `RuntimeError` raised when `HTTPException` has a status code with no content. PR [#5365](https://github.com/tiangolo/fastapi/pull/5365) by [@iudeen](https://github.com/iudeen). -* 🐛 Fix empty reponse body when default `status_code` is empty but the a `Response` parameter with `response.status_code` is set. PR [#5360](https://github.com/tiangolo/fastapi/pull/5360) by [@tmeckel](https://github.com/tmeckel). +* 🐛 Fix empty response body when default `status_code` is empty but the a `Response` parameter with `response.status_code` is set. PR [#5360](https://github.com/tiangolo/fastapi/pull/5360) by [@tmeckel](https://github.com/tmeckel). ### Docs From 9348a5e2cf55440ff3da3bd4b7876acd3b3f57e5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:23:48 +0000 Subject: [PATCH 112/243] =?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 62dae4da7..975bf774e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Fix minor typos in release notes. PR [#14780](https://github.com/fastapi/fastapi/pull/14780) by [@whyvineet](https://github.com/whyvineet). * 🐛 Fix copy button in custom.js. PR [#14722](https://github.com/fastapi/fastapi/pull/14722) by [@fcharrier](https://github.com/fcharrier). * 📝 Add contribution instructions about LLM generated code and comments and automated tools for PRs. PR [#14706](https://github.com/fastapi/fastapi/pull/14706) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for management tasks. PR [#14705](https://github.com/fastapi/fastapi/pull/14705) by [@tiangolo](https://github.com/tiangolo). From c9629e0eb245836b7ecd7b12c1eb7ef12dbdb1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 31 Jan 2026 10:32:27 -0800 Subject: [PATCH 113/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20tr=20(update-outdated)=20(#14745)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- docs/tr/docs/about/index.md | 2 +- docs/tr/docs/advanced/index.md | 31 +- docs/tr/docs/advanced/security/index.md | 12 +- docs/tr/docs/advanced/testing-websockets.md | 10 +- docs/tr/docs/advanced/wsgi.md | 24 +- docs/tr/docs/benchmarks.md | 36 +- docs/tr/docs/deployment/cloud.md | 25 +- docs/tr/docs/deployment/index.md | 22 +- docs/tr/docs/how-to/general.md | 40 +- docs/tr/docs/how-to/index.md | 10 +- docs/tr/docs/index.md | 427 ++++++++++-------- docs/tr/docs/learn/index.md | 4 +- docs/tr/docs/project-generation.md | 102 +---- docs/tr/docs/python-types.md | 455 +++++++++++++------- docs/tr/docs/resources/index.md | 4 +- docs/tr/docs/tutorial/cookie-params.md | 34 +- docs/tr/docs/tutorial/first-steps.md | 333 +++++++------- docs/tr/docs/tutorial/path-params.md | 190 ++++---- docs/tr/docs/tutorial/query-params.md | 94 ++-- docs/tr/docs/tutorial/request-forms.md | 48 ++- docs/tr/docs/tutorial/static-files.md | 38 +- 21 files changed, 1089 insertions(+), 852 deletions(-) diff --git a/docs/tr/docs/about/index.md b/docs/tr/docs/about/index.md index e9dee5217..a638fb0cf 100644 --- a/docs/tr/docs/about/index.md +++ b/docs/tr/docs/about/index.md @@ -1,3 +1,3 @@ -# Hakkında +# Hakkında { #about } FastAPI, tasarımı, ilham kaynağı ve daha fazlası hakkında. 🤓 diff --git a/docs/tr/docs/advanced/index.md b/docs/tr/docs/advanced/index.md index 836e63c8a..3995109e2 100644 --- a/docs/tr/docs/advanced/index.md +++ b/docs/tr/docs/advanced/index.md @@ -1,36 +1,21 @@ -# Gelişmiş Kullanıcı Rehberi +# Gelişmiş Kullanıcı Rehberi { #advanced-user-guide } -## Ek Özellikler +## Ek Özellikler { #additional-features } -[Tutorial - User Guide](../tutorial/index.md){.internal-link target=_blank} sayfası **FastAPI**'ın tüm ana özelliklerini tanıtmaya yetecektir. +Ana [Tutorial - User Guide](../tutorial/index.md){.internal-link target=_blank} sayfası, **FastAPI**'ın tüm temel özelliklerini tanımanız için yeterli olmalıdır. -İlerleyen bölümlerde diğer seçenekler, konfigürasyonlar ve ek özellikleri göreceğiz. +Sonraki bölümlerde diğer seçenekleri, konfigürasyonları ve ek özellikleri göreceksiniz. /// tip | İpucu Sonraki bölümler **mutlaka "gelişmiş" olmak zorunda değildir**. -Kullanım şeklinize bağlı olarak, çözümünüz bu bölümlerden birinde olabilir. +Ve kullanım amacınıza bağlı olarak, çözüm bunlardan birinde olabilir. /// -## Önce Öğreticiyi Okuyun +## Önce Tutorial'ı Okuyun { #read-the-tutorial-first } -[Tutorial - User Guide](../tutorial/index.md){.internal-link target=_blank} sayfasındaki bilgilerle **FastAPI**'nın çoğu özelliğini kullanabilirsiniz. +Ana [Tutorial - User Guide](../tutorial/index.md){.internal-link target=_blank} sayfasındaki bilgilerle **FastAPI**'nın çoğu özelliğini yine de kullanabilirsiniz. -Sonraki bölümler bu sayfayı okuduğunuzu ve bu ana fikirleri bildiğinizi varsayarak hazırlanmıştır. - -## Diğer Kurslar - -[Tutorial - User Guide](../tutorial/index.md){.internal-link target=_blank} sayfası ve bu **Gelişmiş Kullanıcı Rehberi**, öğretici bir kılavuz (bir kitap gibi) şeklinde yazılmıştır ve **FastAPI'ı öğrenmek** için yeterli olsa da, ek kurslarla desteklemek isteyebilirsiniz. - -Belki de öğrenme tarzınıza daha iyi uyduğu için başka kursları tercih edebilirsiniz. - -Bazı kurs sağlayıcıları ✨ [**FastAPI destekçileridir**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, bu FastAPI ve **ekosisteminin** sürekli ve sağlıklı bir şekilde **gelişmesini** sağlar. - -Ayrıca, size **iyi bir öğrenme deneyimi** sağlamakla kalmayıp, **iyi ve sağlıklı bir framework** olan FastAPI'a ve ve **topluluğuna** (yani size) olan gerçek bağlılıklarını gösterir. - -Onların kurslarını denemek isteyebilirsiniz: - -* <a href="https://training.talkpython.fm/fastapi-courses" class="external-link" target="_blank">Talk Python Training</a> -* <a href="https://testdriven.io/courses/tdd-fastapi/" class="external-link" target="_blank">Test-Driven Development</a> +Ve sonraki bölümler, onu zaten okuduğunuzu ve bu temel fikirleri bildiğinizi varsayar. diff --git a/docs/tr/docs/advanced/security/index.md b/docs/tr/docs/advanced/security/index.md index 709f74c72..9b30781f2 100644 --- a/docs/tr/docs/advanced/security/index.md +++ b/docs/tr/docs/advanced/security/index.md @@ -1,6 +1,6 @@ -# Gelişmiş Güvenlik +# Gelişmiş Güvenlik { #advanced-security } -## Ek Özellikler +## Ek Özellikler { #additional-features } [Tutorial - User Guide: Security](../../tutorial/security/index.md){.internal-link target=_blank} sayfasında ele alınanların dışında güvenlikle ilgili bazı ek özellikler vardır. @@ -8,12 +8,12 @@ Sonraki bölümler **mutlaka "gelişmiş" olmak zorunda değildir**. -Kullanım şeklinize bağlı olarak, çözümünüz bu bölümlerden birinde olabilir. +Ve kullanım durumunuza göre, çözüm bu bölümlerden birinde olabilir. /// -## Önce Öğreticiyi Okuyun +## Önce Öğreticiyi Okuyun { #read-the-tutorial-first } -Sonraki bölümler [Tutorial - User Guide: Security](../../tutorial/security/index.md){.internal-link target=_blank} sayfasını okuduğunuzu varsayarak hazırlanmıştır. +Sonraki bölümler, ana [Tutorial - User Guide: Security](../../tutorial/security/index.md){.internal-link target=_blank} sayfasını zaten okuduğunuzu varsayar. -Bu bölümler aynı kavramlara dayanır, ancak bazı ek işlevsellikler sağlar. +Hepsi aynı kavramlara dayanır, ancak bazı ek işlevselliklere izin verir. diff --git a/docs/tr/docs/advanced/testing-websockets.md b/docs/tr/docs/advanced/testing-websockets.md index effe557d1..da12abadb 100644 --- a/docs/tr/docs/advanced/testing-websockets.md +++ b/docs/tr/docs/advanced/testing-websockets.md @@ -1,13 +1,13 @@ -# WebSockets'i Test Etmek +# WebSockets'i Test Etmek { #testing-websockets } -WebSockets testi yapmak için `TestClient`'ı kullanabilirsiniz. +WebSockets'i test etmek için aynı `TestClient`'ı kullanabilirsiniz. -Bu işlem için, `TestClient`'ı bir `with` ifadesinde kullanarak WebSocket'e bağlanabilirsiniz: +Bunun için `TestClient`'ı bir `with` ifadesinde kullanarak WebSocket'e bağlanırsınız: -{* ../../docs_src/app_testing/tutorial002.py hl[27:31] *} +{* ../../docs_src/app_testing/tutorial002_py39.py hl[27:31] *} /// note | Not -Daha fazla detay için Starlette'in <a href="https://www.starlette.dev/staticfiles/" class="external-link" target="_blank">Websockets'i Test Etmek</a> dokümantasyonunu inceleyin. +Daha fazla detay için Starlette'in <a href="https://www.starlette.dev/testclient/#testing-websocket-sessions" class="external-link" target="_blank">WebSockets'i test etme</a> dokümantasyonuna bakın. /// diff --git a/docs/tr/docs/advanced/wsgi.md b/docs/tr/docs/advanced/wsgi.md index 00815a4b2..442f83a59 100644 --- a/docs/tr/docs/advanced/wsgi.md +++ b/docs/tr/docs/advanced/wsgi.md @@ -1,32 +1,32 @@ -# WSGI - Flask, Django ve Daha Fazlasını FastAPI ile Kullanma +# WSGI'yi Dahil Etme - Flask, Django ve Diğerleri { #including-wsgi-flask-django-others } -WSGI uygulamalarını [Sub Applications - Mounts](sub-applications.md){.internal-link target=_blank}, [Behind a Proxy](behind-a-proxy.md){.internal-link target=_blank} bölümlerinde gördüğünüz gibi bağlayabilirsiniz. +WSGI uygulamalarını [Sub Applications - Mounts](sub-applications.md){.internal-link target=_blank}, [Behind a Proxy](behind-a-proxy.md){.internal-link target=_blank} bölümlerinde gördüğünüz gibi mount edebilirsiniz. -Bunun için `WSGIMiddleware` ile Flask, Django vb. WSGI uygulamanızı sarmalayabilir ve FastAPI'ya bağlayabilirsiniz. +Bunun için `WSGIMiddleware`'ı kullanabilir ve bunu WSGI uygulamanızı (örneğin Flask, Django vb.) sarmalamak için kullanabilirsiniz. -## `WSGIMiddleware` Kullanımı +## `WSGIMiddleware` Kullanımı { #using-wsgimiddleware } -`WSGIMiddleware`'ı projenize dahil edin. +`WSGIMiddleware`'ı import etmeniz gerekir. -Ardından WSGI (örneğin Flask) uygulamanızı middleware ile sarmalayın. +Ardından WSGI (örn. Flask) uygulamasını middleware ile sarmalayın. -Son olarak da bir yol altında bağlama işlemini gerçekleştirin. +Ve sonra bunu bir path'in altına mount edin. -{* ../../docs_src/wsgi/tutorial001.py hl[2:3,23] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} -## Kontrol Edelim +## Kontrol Edelim { #check-it } -Artık `/v1/` yolunun altındaki her istek Flask uygulaması tarafından işlenecektir. +Artık `/v1/` path'i altındaki her request Flask uygulaması tarafından işlenecektir. Geri kalanı ise **FastAPI** tarafından işlenecektir. -Eğer uygulamanızı çalıştırıp <a href="http://localhost:8000/v1/" class="external-link" target="_blank">http://localhost:8000/v1/</a> adresine giderseniz, Flask'tan gelen yanıtı göreceksiniz: +Eğer uygulamanızı çalıştırıp <a href="http://localhost:8000/v1/" class="external-link" target="_blank">http://localhost:8000/v1/</a> adresine giderseniz, Flask'tan gelen response'u göreceksiniz: ```txt Hello, World from Flask! ``` -Eğer <a href="http://localhost:8000/v2/" class="external-link" target="_blank">http://localhost:8000/v2/</a> adresine giderseniz, FastAPI'dan gelen yanıtı göreceksiniz: +Ve eğer <a href="http://localhost:8000/v2" class="external-link" target="_blank">http://localhost:8000/v2</a> adresine giderseniz, FastAPI'dan gelen response'u göreceksiniz: ```JSON { diff --git a/docs/tr/docs/benchmarks.md b/docs/tr/docs/benchmarks.md index eb5472869..f2b858585 100644 --- a/docs/tr/docs/benchmarks.md +++ b/docs/tr/docs/benchmarks.md @@ -1,34 +1,34 @@ -# Kıyaslamalar +# Kıyaslamalar { #benchmarks } -Bağımsız TechEmpower kıyaslamaları gösteriyor ki <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">en hızlı Python frameworklerinden birisi</a> olan Uvicorn ile çalıştırılan **FastAPI** uygulamaları, sadece Starlette ve Uvicorn'dan daha düşük sıralamada (FastAPI bu frameworklerin üzerine kurulu) yer alıyor. (*) +Bağımsız TechEmpower kıyaslamaları, Uvicorn altında çalışan **FastAPI** uygulamalarının <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">mevcut en hızlı Python frameworklerinden biri</a> olduğunu, yalnızca Starlette ve Uvicorn'un kendilerinin altında yer aldığını gösteriyor (FastAPI bunları dahili olarak kullanır). Fakat kıyaslamaları ve karşılaştırmaları incelerken şunları aklınızda bulundurmalısınız. -## Kıyaslamalar ve Hız +## Kıyaslamalar ve Hız { #benchmarks-and-speed } -Kıyaslamaları incelediğinizde, farklı özelliklere sahip araçların eşdeğer olarak karşılaştırıldığını yaygın bir şekilde görebilirsiniz. +Kıyaslamalara baktığınızda, farklı türlerdeki birkaç aracın eşdeğermiş gibi karşılaştırıldığını görmek yaygındır. Özellikle, (diğer birçok araç arasında) Uvicorn, Starlette ve FastAPI'ın birlikte karşılaştırıldığını görebilirsiniz. -Aracın çözdüğü problem ne kadar basitse, performansı o kadar iyi olacaktır. Ancak kıyaslamaların çoğu, aracın sağladığı ek özellikleri test etmez. +Aracın çözdüğü problem ne kadar basitse, elde edeceği performans o kadar iyi olur. Ayrıca kıyaslamaların çoğu, aracın sağladığı ek özellikleri test etmez. Hiyerarşi şöyledir: * **Uvicorn**: bir ASGI sunucusu - * **Starlette**: (Uvicorn'u kullanır) bir web mikroframeworkü - * **FastAPI**: (Starlette'i kullanır) veri doğrulama vb. çeşitli ek özelliklere sahip, API oluşturmak için kullanılan bir API mikroframeworkü + * **Starlette**: (Uvicorn'u kullanır) bir web mikroframework'ü + * **FastAPI**: (Starlette'i kullanır) veri doğrulama vb. ile API'lar oluşturmak için çeşitli ek özelliklere sahip bir API mikroframework'ü * **Uvicorn**: - * Sunucunun kendisi dışında ekstra bir kod içermediği için en iyi performansa sahip olacaktır. - * Doğrudan Uvicorn ile bir uygulama yazmazsınız. Bu, yazdığınız kodun en azından Starlette tarafından sağlanan tüm kodu (veya **FastAPI**) az çok içermesi gerektiği anlamına gelir. Eğer bunu yaptıysanız, son uygulamanız bir framework kullanmak ve uygulama kodlarını ve hataları en aza indirmekle aynı ek yüke sahip olacaktır. - * Eğer Uvicorn'u karşılaştırıyorsanız, Daphne, Hypercorn, uWSGI, vb. uygulama sunucuları ile karşılaştırın. + * Sunucunun kendisi dışında çok fazla ekstra kod içermediği için en iyi performansa sahip olacaktır. + * Uvicorn ile doğrudan bir uygulama yazmazsınız. Bu, kodunuzun en azından Starlette'in (veya **FastAPI**'ın) sağladığı kodun aşağı yukarı tamamını içermesi gerektiği anlamına gelir. Bunu yaparsanız, nihai uygulamanız; bir framework kullanmış olmanın ve uygulama kodunu ve bug'ları en aza indirmenin getirdiği ek yükle aynı ek yüke sahip olur. + * Uvicorn'u karşılaştırıyorsanız, Daphne, Hypercorn, uWSGI vb. application server'larla karşılaştırın. * **Starlette**: - * Uvicorn'dan sonraki en iyi performansa sahip olacaktır. İşin aslı, Starlette çalışmak için Uvicorn'u kullanıyor. Dolayısıyla, daha fazla kod çalıştırmaası gerektiğinden muhtemelen Uvicorn'dan sadece "daha yavaş" olabilir. - * Ancak yol bazlı yönlendirme vb. basit web uygulamaları oluşturmak için araçlar sağlar. - * Eğer Starlette'i karşılaştırıyorsanız, Sanic, Flask, Django, vb. frameworkler (veya mikroframeworkler) ile karşılaştırın. + * Uvicorn'dan sonra en iyi performansa sahip olacaktır. Aslında Starlette çalışmak için Uvicorn'u kullanır. Bu yüzden muhtemelen yalnızca daha fazla kod çalıştırmak zorunda kaldığı için Uvicorn'dan "daha yavaş" olabilir. + * Ancak path tabanlı routing vb. ile basit web uygulamaları oluşturmanız için araçlar sağlar. + * Starlette'i karşılaştırıyorsanız, Sanic, Flask, Django vb. web framework'lerle (veya mikroframework'lerle) karşılaştırın. * **FastAPI**: - * Starlette'in Uvicorn'u kullandığı ve ondan daha hızlı olamayacağı gibi, **FastAPI**'da Starlette'i kullanır, dolayısıyla ondan daha hızlı olamaz. - * FastAPI, Starlette'e ek olarak daha fazla özellik sunar. Bunlar veri doğrulama ve <abbr title="Dönüşüm: serialization, parsing, marshalling olarak da biliniyor">dönüşümü</abbr> gibi API'lar oluştururken neredeyse ve her zaman ihtiyaç duyduğunuz özelliklerdir. Ve bunu kullanarak, ücretsiz olarak otomatik dokümantasyon elde edersiniz (otomatik dokümantasyon çalışan uygulamalara ek yük getirmez, başlangıçta oluşturulur). - * FastAPI'ı kullanmadıysanız ve Starlette'i doğrudan kullandıysanız (veya başka bir araç, Sanic, Flask, Responder, vb.) tüm veri doğrulama ve dönüştürme araçlarını kendiniz geliştirmeniz gerekir. Dolayısıyla, son uygulamanız FastAPI kullanılarak oluşturulmuş gibi hâlâ aynı ek yüke sahip olacaktır. Çoğu durumda, uygulamalarda yazılan kodun büyük bir kısmını veri doğrulama ve dönüştürme kodları oluşturur. - * Dolayısıyla, FastAPI'ı kullanarak geliştirme süresinden, hatalardan, kod satırlarından tasarruf edersiniz ve kullanmadığınız durumda (birçok özelliği geliştirmek zorunda kalmakla birlikte) muhtemelen aynı performansı (veya daha iyisini) elde ederdiniz. - * Eğer FastAPI'ı karşılaştırıyorsanız, Flask-apispec, NestJS, Molten, vb. gibi veri doğrulama, dönüştürme ve dokümantasyon sağlayan bir web uygulaması frameworkü ile (veya araç setiyle) karşılaştırın. + * Starlette'in Uvicorn'u kullanıp ondan daha hızlı olamaması gibi, **FastAPI** da Starlette'i kullanır; dolayısıyla ondan daha hızlı olamaz. + * FastAPI, Starlette'in üzerine daha fazla özellik sağlar. API'lar oluştururken neredeyse her zaman ihtiyaç duyduğunuz veri doğrulama ve <abbr title="serialization - serileştirme">serialization</abbr> gibi özellikler. Ayrıca bunu kullanarak ücretsiz olarak otomatik dokümantasyon elde edersiniz (otomatik dokümantasyon, çalışan uygulamalara ek yük bile getirmez; startup'ta üretilir). + * FastAPI'ı kullanmayıp Starlette'i doğrudan kullansaydınız (veya Sanic, Flask, Responder vb. başka bir aracı), tüm veri doğrulama ve serialization işlemlerini kendiniz uygulamak zorunda kalırdınız. Dolayısıyla nihai uygulamanız, FastAPI kullanılarak inşa edilmiş olsaydı sahip olacağı ek yükle hâlâ aynı ek yüke sahip olurdu. Ve çoğu durumda, uygulamalarda yazılan en büyük kod miktarı veri doğrulama ve serialization kısmıdır. + * Bu nedenle FastAPI kullanarak geliştirme süresinden, bug'lardan, kod satırlarından tasarruf edersiniz; ayrıca muhtemelen, onu kullanmasaydınız (tüm bunları kodunuzda kendiniz uygulamak zorunda kalacağınız için) elde edeceğiniz performansın aynısını (veya daha iyisini) elde edersiniz. + * FastAPI'ı karşılaştırıyorsanız, Flask-apispec, NestJS, Molten vb. veri doğrulama, serialization ve dokümantasyon sağlayan bir web uygulaması framework'ü (veya araç seti) ile karşılaştırın. Entegre otomatik veri doğrulama, serialization ve dokümantasyona sahip framework'ler. diff --git a/docs/tr/docs/deployment/cloud.md b/docs/tr/docs/deployment/cloud.md index 4f82e9d0b..25ce6ca8d 100644 --- a/docs/tr/docs/deployment/cloud.md +++ b/docs/tr/docs/deployment/cloud.md @@ -1,13 +1,24 @@ -# FastAPI Uygulamasını Bulut Sağlayıcılar Üzerinde Yayınlama +# Bulut Sağlayıcılar Üzerinde FastAPI Yayınlama { #deploy-fastapi-on-cloud-providers } -FastAPI uygulamasını yayınlamak için hemen hemen **herhangi bir bulut sağlayıcıyı** kullanabilirsiniz. +FastAPI uygulamanızı yayınlamak için neredeyse **herhangi bir bulut sağlayıcıyı** kullanabilirsiniz. -Büyük bulut sağlayıcıların çoğu FastAPI uygulamasını yayınlamak için kılavuzlara sahiptir. +Çoğu durumda, ana bulut sağlayıcıların FastAPI'yi onlarla birlikte yayınlamak için kılavuzları vardır. -## Bulut Sağlayıcılar - Sponsorlar +## FastAPI Cloud { #fastapi-cloud } -Bazı bulut sağlayıcılar ✨ [**FastAPI destekçileridir**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, bu FastAPI ve **ekosisteminin** sürekli ve sağlıklı bir şekilde **gelişmesini** sağlar. +**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>**, **FastAPI**'nin arkasındaki aynı yazar ve ekip tarafından geliştirilmiştir. -Ayrıca, size **iyi servisler** sağlamakla kalmayıp, **iyi ve sağlıklı bir framework** olan FastAPI'a bağlılıklarını gösterir. +Bir API'yi minimum çabayla **oluşturma**, **yayınlama** ve **erişme** sürecini kolaylaştırır. -Bu hizmetleri denemek ve kılavuzlarını incelemek isteyebilirsiniz. +FastAPI ile uygulama geliştirirken elde edilen aynı **geliştirici deneyimini**, onları buluta **yayınlamaya** da taşır. 🎉 + +FastAPI Cloud, *FastAPI and friends* açık kaynak projelerinin birincil sponsoru ve finansman sağlayıcısıdır. ✨ + +## Bulut Sağlayıcılar - Sponsorlar { #cloud-providers-sponsors } + +Diğer bazı bulut sağlayıcılar da ✨ [**FastAPI'ye sponsor olur**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨. 🙇 + +Kılavuzlarını takip etmek ve servislerini denemek için onları da değerlendirmek isteyebilirsiniz: + +* <a href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" class="external-link" target="_blank">Render</a> +* <a href="https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi" class="external-link" target="_blank">Railway</a> diff --git a/docs/tr/docs/deployment/index.md b/docs/tr/docs/deployment/index.md index e03bb4ee0..055d99929 100644 --- a/docs/tr/docs/deployment/index.md +++ b/docs/tr/docs/deployment/index.md @@ -1,21 +1,23 @@ -# Deployment (Yayınlama) +# Deployment { #deployment } -**FastAPI** uygulamasını deploy etmek oldukça kolaydır. +**FastAPI** uygulamasını deploy etmek nispeten kolaydır. -## Deployment Ne Anlama Gelir? +## Deployment Ne Anlama Gelir? { #what-does-deployment-mean } -Bir uygulamayı **deploy** etmek (yayınlamak), uygulamayı **kullanıcılara erişilebilir hale getirmek** için gerekli adımları gerçekleştirmek anlamına gelir. +Bir uygulamayı **deploy** etmek, onu **kullanıcılara erişilebilir hale getirmek** için gerekli adımları gerçekleştirmek anlamına gelir. -Bir **Web API** için bu süreç normalde uygulamayı **uzak bir makineye** yerleştirmeyi, iyi performans, kararlılık vb. özellikler sağlayan bir **sunucu programı** ile **kullanıcılarınızın** uygulamaya etkili ve kesintisiz bir şekilde **erişebilmesini** kapsar. +Bir **web API** için bu süreç normalde uygulamayı **uzak bir makineye** yerleştirmeyi, iyi performans, kararlılık vb. özellikler sağlayan bir **sunucu programı** ile **kullanıcılarınızın** uygulamaya etkili ve kesintisiz bir şekilde, sorun yaşamadan **erişebilmesini** kapsar. -Bu, kodu sürekli olarak değiştirdiğiniz, hata alıp hata giderdiğiniz, geliştirme sunucusunu durdurup yeniden başlattığınız vb. **geliştirme** aşamalarının tam tersidir. +Bu, kodu sürekli olarak değiştirdiğiniz, bozup düzelttiğiniz, geliştirme sunucusunu durdurup yeniden başlattığınız vb. **geliştirme** aşamalarının tam tersidir. -## Deployment Stratejileri +## Deployment Stratejileri { #deployment-strategies } -Kullanım durumunuza ve kullandığınız araçlara bağlı olarak bir kaç farklı yol izleyebilirsiniz. +Kullanım durumunuza ve kullandığınız araçlara bağlı olarak bunu yapmanın birkaç yolu vardır. -Bir dizi araç kombinasyonunu kullanarak kendiniz **bir sunucu yayınlayabilirsiniz**, yayınlama sürecinin bir kısmını sizin için gerçekleştiren bir **bulut hizmeti** veya diğer olası seçenekleri kullanabilirsiniz. +Bir dizi araç kombinasyonunu kullanarak kendiniz **bir sunucu deploy edebilirsiniz**, yayınlama sürecinin bir kısmını sizin için gerçekleştiren bir **bulut hizmeti** veya diğer olası seçenekleri kullanabilirsiniz. + +Örneğin, FastAPI'nin arkasındaki ekip olarak, FastAPI uygulamalarını buluta mümkün olduğunca akıcı şekilde deploy etmeyi sağlamak için, FastAPI ile çalışmanın aynı geliştirici deneyimini sunarak <a href="https://fastapicloud.com" class="external-link" target="_blank">**FastAPI Cloud**</a>'u oluşturduk. **FastAPI** uygulamasını yayınlarken aklınızda bulundurmanız gereken ana kavramlardan bazılarını size göstereceğim (ancak bunların çoğu diğer web uygulamaları için de geçerlidir). -Sonraki bölümlerde akılda tutulması gereken diğer ayrıntıları ve yayınlama tekniklerinden bazılarını göreceksiniz. ✨ +Sonraki bölümlerde akılda tutulması gereken diğer ayrıntıları ve bunu yapmaya yönelik bazı teknikleri göreceksiniz. ✨ diff --git a/docs/tr/docs/how-to/general.md b/docs/tr/docs/how-to/general.md index cbfa7beb2..e3154921a 100644 --- a/docs/tr/docs/how-to/general.md +++ b/docs/tr/docs/how-to/general.md @@ -1,39 +1,39 @@ -# Genel - Nasıl Yapılır - Tarifler +# Genel - Nasıl Yapılır - Tarifler { #general-how-to-recipes } -Bu sayfada genel ve sıkça sorulan sorular için dokümantasyonun diğer sayfalarına yönlendirmeler bulunmaktadır. +Bu sayfada genel veya sık sorulan sorular için dokümantasyonun diğer bölümlerine çeşitli yönlendirmeler bulunmaktadır. -## Veri Filtreleme - Güvenlik +## Veri Filtreleme - Güvenlik { #filter-data-security } -Döndürmeniz gereken veriden fazlasını döndürmediğinizden emin olmak için, [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank} sayfasını okuyun. +Döndürmeniz gerekenden daha fazla veri döndürmediğinizden emin olmak için, [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank} dokümantasyonunu okuyun. -## Dokümantasyon Etiketleri - OpenAPI +## Dokümantasyon Etiketleri - OpenAPI { #documentation-tags-openapi } -*Yol operasyonlarınıza* etiketler ekleyerek dokümantasyon arayüzünde gruplar halinde görünmesini sağlamak için, [Tutorial - Path Operation Configurations - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank} sayfasını okuyun. +*path operation*'larınıza etiketler eklemek ve dokümantasyon arayüzünde gruplamak için, [Tutorial - Path Operation Configurations - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank} dokümantasyonunu okuyun. -## Dokümantasyon Özeti ve Açıklaması - OpenAPI +## Dokümantasyon Özeti ve Açıklaması - OpenAPI { #documentation-summary-and-description-openapi } -*Yol operasyonlarınıza* özet ve açıklama ekleyip dokümantasyon arayüzünde görünmesini sağlamak için, [Tutorial - Path Operation Configurations - Summary and Description](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank} sayfasını okuyun. +*path operation*'larınıza özet ve açıklama eklemek ve bunları dokümantasyon arayüzünde göstermek için, [Tutorial - Path Operation Configurations - Summary and Description](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank} dokümantasyonunu okuyun. -## Yanıt Açıklaması Dokümantasyonu - OpenAPI +## Dokümantasyon Yanıt Açıklaması - OpenAPI { #documentation-response-description-openapi } -Dokümantasyon arayüzünde yer alan yanıt açıklamasını tanımlamak için, [Tutorial - Path Operation Configurations - Response description](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank} sayfasını okuyun. +Dokümantasyon arayüzünde gösterilen response açıklamasını tanımlamak için, [Tutorial - Path Operation Configurations - Response description](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank} dokümantasyonunu okuyun. -## *Yol Operasyonunu* Kullanımdan Kaldırma - OpenAPI +## Dokümantasyonda Bir *Path Operation*'ı Kullanımdan Kaldırma - OpenAPI { #documentation-deprecate-a-path-operation-openapi } -Bir *yol işlemi*ni kullanımdan kaldırmak ve bunu dokümantasyon arayüzünde göstermek için, [Tutorial - Path Operation Configurations - Deprecation](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank} sayfasını okuyun. +Bir *path operation*'ı kullanımdan kaldırmak ve bunu dokümantasyon arayüzünde göstermek için, [Tutorial - Path Operation Configurations - Deprecation](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank} dokümantasyonunu okuyun. -## Herhangi Bir Veriyi JSON Uyumlu Hale Getirme +## Herhangi Bir Veriyi JSON Uyumlu Hale Getirme { #convert-any-data-to-json-compatible } -Herhangi bir veriyi JSON uyumlu hale getirmek için, [Tutorial - JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} sayfasını okuyun. +Herhangi bir veriyi JSON uyumlu hale getirmek için, [Tutorial - JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} dokümantasyonunu okuyun. -## OpenAPI Meta Verileri - Dokümantasyon +## OpenAPI Meta Verileri - Dokümantasyon { #openapi-metadata-docs } -OpenAPI şemanıza lisans, sürüm, iletişim vb. meta veriler eklemek için, [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md){.internal-link target=_blank} sayfasını okuyun. +Lisans, sürüm, iletişim vb. dahil olmak üzere OpenAPI şemanıza meta veriler eklemek için, [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md){.internal-link target=_blank} dokümantasyonunu okuyun. -## OpenAPI Bağlantı Özelleştirme +## OpenAPI Özel URL { #openapi-custom-url } -OpenAPI bağlantısını özelleştirmek (veya kaldırmak) için, [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md#openapi-url){.internal-link target=_blank} sayfasını okuyun. +OpenAPI URL'ini özelleştirmek (veya kaldırmak) için, [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md#openapi-url){.internal-link target=_blank} dokümantasyonunu okuyun. -## OpenAPI Dokümantasyon Bağlantıları +## OpenAPI Dokümantasyon URL'leri { #openapi-docs-urls } -Dokümantasyonu arayüzünde kullanılan bağlantıları güncellemek için, [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md#docs-urls){.internal-link target=_blank} sayfasını okuyun. +Otomatik olarak oluşturulan dokümantasyon kullanıcı arayüzlerinde kullanılan URL'leri güncellemek için, [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md#docs-urls){.internal-link target=_blank} dokümantasyonunu okuyun. diff --git a/docs/tr/docs/how-to/index.md b/docs/tr/docs/how-to/index.md index 26dd9026c..5ec2e0268 100644 --- a/docs/tr/docs/how-to/index.md +++ b/docs/tr/docs/how-to/index.md @@ -1,13 +1,13 @@ -# Nasıl Yapılır - Tarifler +# Nasıl Yapılır - Tarifler { #how-to-recipes } -Burada çeşitli konular hakkında farklı tarifler veya "nasıl yapılır" kılavuzları yer alıyor. +Burada **çeşitli konular** hakkında farklı tarifler veya "nasıl yapılır" kılavuzları göreceksiniz. -Bu fikirlerin büyük bir kısmı aşağı yukarı **bağımsız** olacaktır, çoğu durumda bunları sadece **projenize** hitap ediyorsa incelemelisiniz. +Bu fikirlerin büyük bir kısmı aşağı yukarı **bağımsız** olacaktır ve çoğu durumda bunları yalnızca doğrudan **projenize** uygulanıyorsa incelemeniz yeterli olacaktır. -Projeniz için ilginç ve yararlı görünen bir şey varsa devam edin ve inceleyin, aksi halde bunları atlayabilirsiniz. +Projeniz için ilginç ve yararlı görünen bir şey varsa devam edin ve inceleyin; aksi halde muhtemelen bunları atlayabilirsiniz. /// tip | İpucu -**FastAPI**'ı düzgün (ve önerilen) şekilde öğrenmek istiyorsanız [Öğretici - Kullanıcı Rehberi](../tutorial/index.md){.internal-link target=_blank}'ni bölüm bölüm okuyun. +**FastAPI**'ı yapılandırılmış bir şekilde (önerilir) **öğrenmek** istiyorsanız bunun yerine [Öğretici - Kullanıcı Rehberi](../tutorial/index.md){.internal-link target=_blank}'ni bölüm bölüm okuyun. /// diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index 516d5959e..9cffd4274 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -1,14 +1,14 @@ -# FastAPI +# FastAPI { #fastapi } <style> .md-content .md-typeset h1 { display: none; } </style> <p align="center"> - <a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> + <a href="https://fastapi.tiangolo.com/tr"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> </p> <p align="center"> - <em>FastAPI framework, yüksek performanslı, öğrenmesi oldukça kolay, kodlaması hızlı, kullanıma hazır</em> + <em>FastAPI framework, yüksek performanslı, öğrenmesi kolay, kodlaması hızlı, production'a hazır</em> </p> <p align="center"> <a href="https://github.com/fastapi/fastapi/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank"> @@ -27,59 +27,65 @@ --- -**Dokümantasyon**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a> +**Dokümantasyon**: <a href="https://fastapi.tiangolo.com/tr" target="_blank">https://fastapi.tiangolo.com</a> **Kaynak Kod**: <a href="https://github.com/fastapi/fastapi" target="_blank">https://github.com/fastapi/fastapi</a> --- -FastAPI, Python 'nin standart <abbr title="Tip Belirteçleri: Type Hints">tip belirteçleri</abbr>ne dayalı, modern ve hızlı (yüksek performanslı) API'lar oluşturmak için kullanılabilecek web framework'tür. +FastAPI, Python'un standart type hints'lerine dayalı olarak Python ile API'lar oluşturmak için kullanılan modern ve hızlı (yüksek performanslı) bir web framework'üdür. Temel özellikleri şunlardır: -* **Hızlı**: Çok yüksek performanslı, **NodeJS** ve **Go** ile eşit düzeyde (Starlette ve Pydantic sayesinde). [En hızlı Python framework'lerinden bir tanesidir](#performans). -* **Kodlaması Hızlı**: Geliştirme hızını yaklaşık %200 ile %300 aralığında arttırır. * +* **Hızlı**: Çok yüksek performanslı, **NodeJS** ve **Go** ile eşit düzeyde (Starlette ve Pydantic sayesinde). [Mevcut en hızlı Python framework'lerinden biri](#performance). +* **Kodlaması Hızlı**: Özellik geliştirme hızını yaklaşık %200 ile %300 aralığında artırır. * * **Daha az hata**: İnsan (geliştirici) kaynaklı hataları yaklaşık %40 azaltır. * -* **Sezgisel**: Muhteşem bir editör desteği. Her yerde <abbr title="Otomatik Tamamlama: auto-complete, autocompletion, IntelliSense">otomatik tamamlama</abbr>. Hata ayıklama ile daha az zaman harcayacaksınız. -* **Kolay**: Öğrenmesi ve kullanması kolay olacak şekilde tasarlandı. Doküman okuma ile daha az zaman harcayacaksınız. -* **Kısa**: Kod tekrarı minimize edildi. Her parametre tanımlamasında birden fazla özellik ve daha az hatayla karşılaşacaksınız. -* **Güçlü**: Otomatik ve etkileşimli dokümantasyon ile birlikte, kullanıma hazır kod elde edebilirsiniz. -* **Standard öncelikli**: API'lar için açık standartlara dayalı (ve tamamen uyumlu); <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (eski adıyla Swagger) ve <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. +* **Sezgisel**: Harika bir editör desteği. Her yerde <abbr title="auto-complete, autocompletion, IntelliSense olarak da bilinir">Completion</abbr>. Hata ayıklamaya daha az zaman. +* **Kolay**: Kullanımı ve öğrenmesi kolay olacak şekilde tasarlandı. Doküman okumaya daha az zaman. +* **Kısa**: Kod tekrarını minimize eder. Her parametre tanımından birden fazla özellik. Daha az hata. +* **Sağlam**: Production'a hazır kod elde edersiniz. Otomatik etkileşimli dokümantasyon ile birlikte. +* **Standardlara dayalı**: API'lar için açık standartlara dayalıdır (ve tamamen uyumludur); <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (önceden Swagger olarak biliniyordu) ve <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. -<small>* ilgili kanılar, dahili geliştirme ekibinin geliştirdikleri ürünlere yaptıkları testlere dayanmaktadır.</small> +<small>* tahmin, production uygulamalar geliştiren dahili bir geliştirme ekibinin yaptığı testlere dayanmaktadır.</small> -## Sponsorlar +## Sponsorlar { #sponsors } <!-- sponsors --> -{% if sponsors %} +### Keystone Sponsor { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor -%} + +### Gold and Silver Sponsors { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> {% endfor -%} {%- for sponsor in sponsors.silver -%} <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> {% endfor %} -{% endif %} <!-- /sponsors --> -<a href="https://fastapi.tiangolo.com/tr/fastapi-people/#sponsors" class="external-link" target="_blank">Diğer Sponsorlar</a> +<a href="https://fastapi.tiangolo.com/tr/fastapi-people/#sponsors" class="external-link" target="_blank">Diğer sponsorlar</a> -## Görüşler +## Görüşler { #opinions } -"_[...] Bugünlerde **FastAPI**'ı çok fazla kullanıyorum. [...] Aslında bunu ekibimin **Microsoft'taki Machine Learning servislerinin** tamamında kullanmayı planlıyorum. Bunlardan bazıları **Windows**'un ana ürünlerine ve **Office** ürünlerine entegre ediliyor._" +"_[...] Bugünlerde **FastAPI**'ı çok fazla kullanıyorum. [...] Aslında bunu ekibimin **Microsoft'taki ML servislerinin** tamamında kullanmayı planlıyorum. Bunlardan bazıları ana **Windows** ürününe ve bazı **Office** ürünlerine entegre ediliyor._" <div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/fastapi/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div> --- -"_**FastAPI**'ı **tahminlerimiz**'i sorgulanabilir hale getirecek bir **REST** sunucu oluşturmak için benimsedik/kullanmaya başladık._" +"_**predictions** almak için sorgulanabilecek bir **REST** server oluşturmak amacıyla **FastAPI** kütüphanesini benimsedik. [Ludwig için]_" <div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div> --- -"_**Netflix**, **kriz yönetiminde** orkestrasyon yapabilmek için geliştirdiği yeni framework'ü **Dispatch**'in, açık kaynak sürümünü paylaşmaktan gurur duyuyor. [**FastAPI** ile yapıldı.]_" +"_**Netflix**, **kriz yönetimi** orkestrasyon framework'ümüz: **Dispatch**'in open-source sürümünü duyurmaktan memnuniyet duyar! [**FastAPI** ile geliştirildi]_" <div style="text-align: right; margin-right: 10%;">Kevin Glisson, Marc Vilanova, Forest Monsen - <strong>Netflix</strong> <a href="https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072" target="_blank"><small>(ref)</small></a></div> @@ -91,70 +97,68 @@ Temel özellikleri şunlardır: --- -"_Dürüst olmak gerekirse, inşa ettiğiniz şey gerçekten sağlam ve profesyonel görünüyor. Birçok açıdan **Hug**'ın olmasını istediğim şey tam da bu - böyle bir şeyi inşa eden birini görmek gerçekten ilham verici._" +"_Dürüst olmak gerekirse, inşa ettiğiniz şey gerçekten sağlam ve profesyonel görünüyor. Birçok açıdan, **Hug**'ın olmasını istediğim şey tam da bu - böyle bir şeyi inşa eden birini görmek gerçekten ilham verici._" -<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="http://www.hug.rest/" target="_blank">Hug</a>'ın Yaratıcısı</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> +<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://github.com/hugapi/hug" target="_blank">Hug</a> yaratıcısı</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> --- -"_Eğer REST API geliştirmek için **modern bir framework** öğrenme arayışında isen, **FastAPI**'a bir göz at [...] Hızlı, kullanımı ve öğrenmesi kolay. [...]_" +"_REST API'lar geliştirmek için **modern bir framework** öğrenmek istiyorsanız, **FastAPI**'a bir göz atın [...] Hızlı, kullanımı ve öğrenmesi kolay [...]_" -"_**API** servislerimizi **FastAPI**'a taşıdık [...] Sizin de beğeneceğinizi düşünüyoruz. [...]_" +"_**API**'larımız için **FastAPI**'a geçtik [...] Bence hoşunuza gidecek [...]_" <div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> kurucuları - <a href="https://spacy.io" target="_blank">spaCy</a> yaratıcıları</strong> <a href="https://x.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://x.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div> --- -"_Python ile kullanıma hazır bir API oluşturmak isteyen herhangi biri için, **FastAPI**'ı şiddetle tavsiye ederim. **Harika tasarlanmış**, **kullanımı kolay** ve **yüksek ölçeklenebilir**, API odaklı geliştirme stratejimizin **ana bileşeni** haline geldi ve Virtual TAC Engineer gibi birçok otomasyon ve servisi yönetiyor._" +"_Production'da Python API geliştirmek isteyen herkese **FastAPI**'ı şiddetle tavsiye ederim. **Harika tasarlanmış**, **kullanımı kolay** ve **yüksek ölçeklenebilir**; API-first geliştirme stratejimizin **kilit bir bileşeni** haline geldi ve Virtual TAC Engineer gibi birçok otomasyon ve servise güç veriyor._" <div style="text-align: right; margin-right: 10%;">Deon Pillsbury - <strong>Cisco</strong> <a href="https://www.linkedin.com/posts/deonpillsbury_cisco-cx-python-activity-6963242628536487936-trAp/" target="_blank"><small>(ref)</small></a></div> --- -## Komut Satırı Uygulamalarının FastAPI'ı: **Typer** +## FastAPI mini belgeseli { #fastapi-mini-documentary } + +2025'in sonunda yayınlanan bir <a href="https://www.youtube.com/watch?v=mpR8ngthqiE" class="external-link" target="_blank">FastAPI mini belgeseli</a> var, online olarak izleyebilirsiniz: + +<a href="https://www.youtube.com/watch?v=mpR8ngthqiE" target="_blank"><img src="https://fastapi.tiangolo.com/img/fastapi-documentary.jpg" alt="FastAPI Mini Documentary"></a> + +## CLI'ların FastAPI'ı: **Typer** { #typer-the-fastapi-of-clis } <a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> -Eğer API yerine, terminalde kullanılmak üzere bir <abbr title="Komut Satırı: Command Line Interface">komut satırı uygulaması</abbr> geliştiriyorsanız <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>'a göz atabilirsiniz. +Web API yerine terminalde kullanılacak bir <abbr title="Command Line Interface">CLI</abbr> uygulaması geliştiriyorsanız <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>'a göz atın. -**Typer** kısaca FastAPI'ın küçük kardeşi. Ve hedefi komut satırı uygulamalarının **FastAPI'ı** olmak. ⌨️ 🚀 +**Typer**, FastAPI'ın küçük kardeşi. Ve hedefi CLI'ların **FastAPI'ı** olmak. ⌨️ 🚀 -## Gereksinimler +## Gereksinimler { #requirements } FastAPI iki devin omuzları üstünde duruyor: -* Web tarafı için <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a>. -* Data tarafı için <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a>. +* Web kısımları için <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a>. +* Data kısımları için <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a>. -## Kurulum +## Kurulum { #installation } + +Bir <a href="https://fastapi.tiangolo.com/tr/virtual-environments/" class="external-link" target="_blank">virtual environment</a> oluşturup etkinleştirelim ve ardından FastAPI'ı yükleyelim: <div class="termy"> ```console -$ pip install fastapi +$ pip install "fastapi[standard]" ---> 100% ``` </div> -Uygulamamızı kullanılabilir hale getirmek için <a href="http://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a> ya da <a href="https://github.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a> gibi bir ASGI sunucusuna ihtiyacımız olacak. +**Not**: Tüm terminallerde çalıştığından emin olmak için `"fastapi[standard]"` ifadesini tırnak içinde yazdığınızdan emin olun. -<div class="termy"> +## Örnek { #example } -```console -$ pip install "uvicorn[standard]" +### Oluşturalım { #create-it } ----> 100% -``` - -</div> - -## Örnek - -### Kodu Oluşturalım - -* `main.py` adında bir dosya oluşturup içine şu kodu yapıştıralım: +Şu içerikle `main.py` adında bir dosya oluşturalım: ```Python from typing import Union @@ -175,9 +179,9 @@ def read_item(item_id: int, q: Union[str, None] = None): ``` <details markdown="1"> -<summary>Ya da <code>async def</code>...</summary> +<summary>Ya da <code>async def</code> kullanalım...</summary> -Eğer kodunuzda `async` / `await` varsa, `async def` kullanalım: +Eğer kodunuz `async` / `await` kullanıyorsa, `async def` kullanın: ```Python hl_lines="9 14" from typing import Union @@ -199,22 +203,35 @@ async def read_item(item_id: int, q: Union[str, None] = None): **Not**: -Eğer bu konu hakkında bilginiz yoksa <a href="https://fastapi.tiangolo.com/tr/async/#in-a-hurry" target="_blank">`async` ve `await`</a> dokümantasyonundaki _"Aceleniz mi var?"_ kısmını kontrol edebilirsiniz. +Eğer bilmiyorsanız, dokümanlardaki <a href="https://fastapi.tiangolo.com/tr/async/#in-a-hurry" target="_blank">`async` ve `await`</a> hakkında _"Aceleniz mi var?"_ bölümüne bakın. </details> -### Kodu Çalıştıralım +### Çalıştıralım { #run-it } -Sunucuyu aşağıdaki komutla çalıştıralım: +Sunucuyu şu komutla çalıştıralım: <div class="termy"> ```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. ``` @@ -222,54 +239,54 @@ INFO: Application startup complete. </div> <details markdown="1"> -<summary><code>uvicorn main:app --reload</code> komutuyla ilgili...</summary> +<summary><code>fastapi dev main.py</code> komutu hakkında...</summary> -`uvicorn main:app` komutunu şu şekilde açıklayabiliriz: +`fastapi dev` komutu, `main.py` dosyanızı okur, içindeki **FastAPI** uygulamasını algılar ve <a href="https://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a> kullanarak bir server başlatır. -* `main`: dosya olan `main.py` (yani Python "modülü"). -* `app`: ise `main.py` dosyasının içerisinde `app = FastAPI()` satırında oluşturduğumuz `FastAPI` nesnesi. -* `--reload`: kod değişikliklerinin ardından sunucuyu otomatik olarak yeniden başlatır. Bu parameteyi sadece geliştirme aşamasında kullanmalıyız. +Varsayılan olarak `fastapi dev`, local geliştirme için auto-reload etkin şekilde başlar. + +Daha fazla bilgi için <a href="https://fastapi.tiangolo.com/tr/fastapi-cli/" target="_blank">FastAPI CLI dokümantasyonu</a>'nu okuyabilirsiniz. </details> -### Şimdi de Kontrol Edelim +### Kontrol Edelim { #check-it } -Tarayıcımızda şu bağlantıyı açalım <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>. +Tarayıcınızda şu bağlantıyı açın: <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>. -Aşağıdaki gibi bir JSON yanıtıyla karşılaşacağız: +Şu JSON response'unu göreceksiniz: ```JSON {"item_id": 5, "q": "somequery"} ``` -Az önce oluşturduğumuz API: +Artık şunları yapan bir API oluşturdunuz: -* `/` ve `/items/{item_id}` <abbr title="Adres / Yol: Path ">_yollarına_</abbr> HTTP isteği alabilir. -* İki _yolda_ `GET` <em>operasyonlarını</em> (HTTP _metodları_ olarak da bilinen) kabul ediyor. -* `/items/{item_id}` _yolu_ `item_id` adında bir _yol parametresine_ sahip ve bu parametre `int` değer almak zorundadır. -* `/items/{item_id}` _yolu_ `q` adında bir _yol parametresine_ sahip ve bu parametre opsiyonel olmakla birlikte, `str` değer almak zorundadır. +* `/` ve `/items/{item_id}` _path_'lerinde HTTP request'leri alır. +* Her iki _path_ de `GET` <em>operasyonlarını</em> (HTTP _method_'ları olarak da bilinir) kabul eder. +* `/items/{item_id}` _path_'i, `int` olması gereken `item_id` adlı bir _path parameter_'a sahiptir. +* `/items/{item_id}` _path_'i, opsiyonel `str` bir _query parameter_ olan `q`'ya sahiptir. -### Etkileşimli API Dokümantasyonu +### Etkileşimli API dokümantasyonu { #interactive-api-docs } -Şimdi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> bağlantısını açalım. +Şimdi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> adresine gidin. -<a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> tarafından sağlanan otomatik etkileşimli bir API dokümantasyonu göreceğiz: +Otomatik etkileşimli API dokümantasyonunu göreceksiniz (<a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> tarafından sağlanır): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternatif API Dokümantasyonu +### Alternatif API dokümantasyonu { #alternative-api-docs } -Şimdi <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> bağlantısını açalım. +Ve şimdi <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> adresine gidin. -<a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> tarafından sağlanan otomatik dokümantasyonu göreceğiz: +Alternatif otomatik dokümantasyonu göreceksiniz (<a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> tarafından sağlanır): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Örneği Güncelleyelim +## Örneği Güncelleyelim { #example-upgrade } -Şimdi `main.py` dosyasını, `PUT` isteğiyle birlikte bir gövde alacak şekilde değiştirelim. +Şimdi `main.py` dosyasını, `PUT` request'iyle gelen bir body alacak şekilde değiştirelim. -<abbr title="Gövde: Body">Gövde</abbr>yi Pydantic sayesinde standart python tiplerini kullanarak tanımlayalım. +Body'yi Pydantic sayesinde standart Python tiplerini kullanarak tanımlayalım. ```Python hl_lines="4 9-12 25-27" from typing import Union @@ -301,174 +318,248 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -Sunucu otomatik olarak yeniden başlamış olmalı (çünkü yukarıda `uvicorn` komutuyla birlikte `--reload` parametresini kullandık). +`fastapi dev` server'ı otomatik olarak yeniden yüklemelidir. -### Etkileşimli API Dokümantasyonundaki Değişimi Görelim +### Etkileşimli API dokümantasyonu güncellemesi { #interactive-api-docs-upgrade } -Şimdi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> bağlantısına tekrar gidelim. +Şimdi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> adresine gidin. -* Etkileşimli API dokümantasyonu, yeni gövdede dahil olmak üzere otomatik olarak güncellenmiş olacak: +* Etkileşimli API dokümantasyonu, yeni body dahil olacak şekilde otomatik olarak güncellenecek: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* "Try it out" butonuna tıklayalım, bu işlem API parametleri üzerinde değişiklik yapmamıza ve doğrudan API ile etkileşime geçmemize imkan sağlayacak: +* "Try it out" butonuna tıklayın; parametreleri doldurmanıza ve API ile doğrudan etkileşime girmenize olanak sağlar: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Şimdi "Execute" butonuna tıklayalım, kullanıcı arayüzü API'ımız ile bağlantı kurup parametreleri gönderecek ve sonucu ekranımıza getirecek: +* Sonra "Execute" butonuna tıklayın; kullanıcı arayüzü API'nız ile iletişim kuracak, parametreleri gönderecek, sonuçları alacak ve ekranda gösterecek: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Alternatif API Dokümantasyonundaki Değişimi Görelim +### Alternatif API dokümantasyonu güncellemesi { #alternative-api-docs-upgrade } -Şimdi ise <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> bağlantısına tekrar gidelim. +Ve şimdi <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> adresine gidin. -* Alternatif dokümantasyonda yaptığımız değişiklikler ile birlikte yeni sorgu parametresi ve gövde bilgisi ile güncelemiş olacak: +* Alternatif dokümantasyon da yeni query parameter ve body'yi yansıtacak: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Özet +### Özet { #recap } -Özetlemek gerekirse, parametrelerin, gövdenin, vb. veri tiplerini fonksiyon parametreleri olarak **bir kere** tanımlıyoruz. +Özetle, parametrelerin, body'nin vb. type'larını fonksiyon parametreleri olarak **bir kere** tanımlarsınız. -Bu işlemi standart modern Python tipleriyle yapıyoruz. +Bunu standart modern Python tipleriyle yaparsınız. -Yeni bir sözdizimi yapısını, bir kütüphane özel metod veya sınıfları öğrenmeye gerek yoktur. +Yeni bir syntax, belirli bir kütüphanenin method'larını ya da class'larını vb. öğrenmeniz gerekmez. -Hepsi sadece **Python** standartlarına dayalıdır. +Sadece standart **Python**. -Örnek olarak, `int` tanımlamak için: +Örneğin bir `int` için: ```Python item_id: int ``` -ya da daha kompleks herhangi bir python modelini tanımlayabiliriz, örneğin `Item` modeli için: +ya da daha karmaşık bir `Item` modeli için: ```Python item: Item ``` -...ve sadece kısa bir parametre tipi belirterek elde ettiklerimiz: +...ve bu tek tanımla şunları elde edersiniz: -* Editör desteğiyle birlikte: - * Otomatik tamamlama. - * Tip kontrolü. -* Veri Doğrulama: - * Veri geçerli değilse, otomatik olarak açıklayıcı hatalar gösterir. - * Çok <abbr title="Derin / İç içe: Nested">derin</abbr> JSON nesnelerinde bile doğrulama yapar. -* Gelen verinin <abbr title="Dönüşüm: serialization, parsing, marshalling olarak da biliniyor">dönüşümünü</abbr> aşağıdaki veri tiplerini kullanarak gerçekleştirir: +* Şunlar dahil editör desteği: + * Completion. + * Type kontrolleri. +* Verinin doğrulanması: + * Veri geçersiz olduğunda otomatik ve anlaşılır hatalar. + * Çok derin iç içe JSON nesneleri için bile doğrulama. +* Girdi verisinin <abbr title="serialization, parsing, marshalling olarak da bilinir">Dönüşümü</abbr>: network'ten gelen veriyi Python verisine ve type'larına çevirir. Şunlardan okur: * JSON. - * Yol parametreleri. - * Sorgu parametreleri. - * Çerezler. - * Headers. - * Formlar. - * Dosyalar. -* Giden verinin <abbr title="Dönüşüm: serialization, parsing, marshalling olarak da biliniyor">dönüşümünü</abbr> aşağıdaki veri tiplerini kullanarak gerçekleştirir (JSON olarak): - * Python tiplerinin (`str`, `int`, `float`, `bool`, `list`, vb) dönüşümü. - * `datetime` nesnesi. - * `UUID` nesnesi. + * Path parameter'lar. + * Query parameter'lar. + * Cookie'ler. + * Header'lar. + * Form'lar. + * File'lar. +* Çıktı verisinin <abbr title="serialization, parsing, marshalling olarak da bilinir">Dönüşümü</abbr>: Python verisini ve type'larını network verisine çevirir (JSON olarak): + * Python type'larını dönüştürür (`str`, `int`, `float`, `bool`, `list`, vb.). + * `datetime` nesneleri. + * `UUID` nesneleri. * Veritabanı modelleri. - * ve çok daha fazlası... -* 2 alternatif kullanıcı arayüzü dahil olmak üzere, otomatik etkileşimli API dokümantasyonu sağlar: + * ...ve daha fazlası. +* 2 alternatif kullanıcı arayüzü dahil otomatik etkileşimli API dokümantasyonu: * Swagger UI. * ReDoc. --- -Az önceki örneğe geri dönelim, **FastAPI**'ın yapacaklarına bir bakış atalım: +Önceki kod örneğine dönersek, **FastAPI** şunları yapacaktır: -* `item_id`'nin `GET` ve `PUT` istekleri için, yolda olup olmadığının kontol edecek. -* `item_id`'nin `GET` ve `PUT` istekleri için, tipinin `int` olduğunu doğrulayacak. - * Eğer değilse, sebebini belirten bir hata mesajı gösterecek. -* Opsiyonel bir `q` parametresinin `GET` isteği içinde (`http://127.0.0.1:8000/items/foo?q=somequery` gibi) olup olmadığını kontrol edecek - * `q` parametresini `= None` ile oluşturduğumuz için, opsiyonel bir parametre olacak. - * Eğer `None` olmasa zorunlu bir parametre olacaktı (`PUT` metodunun gövdesinde olduğu gibi). -* `PUT` isteği için `/items/{item_id}`'nin gövdesini, JSON olarak doğrulayıp okuyacak: - * `name` adında zorunlu bir parametre olup olmadığını ve varsa tipinin `str` olup olmadığını kontol edecek. - * `price` adında zorunlu bir parametre olup olmadığını ve varsa tipinin `float` olup olmadığını kontol edecek. - * `is_offer` adında opsiyonel bir parametre olup olmadığını ve varsa tipinin `float` olup olmadığını kontol edecek. - * Bunların hepsi en derin JSON nesnelerinde bile çalışacak. -* Verilerin JSON'a ve JSON'ın python nesnesine dönüşümü otomatik olarak yapılacak. -* Her şeyi OpenAPI ile uyumlu bir şekilde otomatik olarak dokümanlayacak ve bunlarda aşağıdaki gibi kullanılabilecek: +* `GET` ve `PUT` request'leri için path'te `item_id` olduğunu doğrular. +* `GET` ve `PUT` request'leri için `item_id`'nin type'ının `int` olduğunu doğrular. + * Değilse, client faydalı ve anlaşılır bir hata görür. +* `GET` request'leri için `q` adlı opsiyonel bir query parameter olup olmadığını kontrol eder (`http://127.0.0.1:8000/items/foo?q=somequery` örneğindeki gibi). + * `q` parametresi `= None` ile tanımlandığı için opsiyoneldir. + * `None` olmasaydı zorunlu olurdu (tıpkı `PUT` örneğindeki body gibi). +* `/items/{item_id}`'ye yapılan `PUT` request'leri için body'yi JSON olarak okur: + * `str` olması gereken, zorunlu `name` alanı olduğunu kontrol eder. + * `float` olması gereken, zorunlu `price` alanı olduğunu kontrol eder. + * Varsa, `bool` olması gereken opsiyonel `is_offer` alanını kontrol eder. + * Bunların hepsi çok derin iç içe JSON nesneleri için de çalışır. +* JSON'a ve JSON'dan dönüşümü otomatik yapar. +* Her şeyi OpenAPI ile dokümante eder; bu dokümantasyon şunlar tarafından kullanılabilir: * Etkileşimli dokümantasyon sistemleri. - * Bir çok programlama dili için otomatik istemci kodu üretim sistemleri. -* İki ayrı etkileşimli dokümantasyon arayüzünü doğrudan sağlayacak. + * Birçok dil için otomatik client kodu üretim sistemleri. +* 2 etkileşimli dokümantasyon web arayüzünü doğrudan sunar. --- -Daha yeni başladık ama çalışma mantığını çoktan anlamış oldunuz. +Daha yolun başındayız, ama bunun nasıl çalıştığı hakkında fikri kaptınız. -Şimdi aşağıdaki satırı değiştirmeyi deneyin: +Şu satırı değiştirmeyi deneyin: ```Python return {"item_name": item.name, "item_id": item_id} ``` -...bundan: +...şundan: ```Python ... "item_name": item.name ... ``` -...buna: +...şuna: ```Python ... "item_price": item.price ... ``` -...ve editörünün veri tiplerini bildiğini ve otomatik tamamladığını göreceksiniz: +...ve editörünüzün alanları otomatik tamamladığını ve type'larını bildiğini görün: ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -Daha fazal özellik içeren, daha eksiksiz bir örnek için <a href="https://fastapi.tiangolo.com/tr/tutorial/">Öğretici - Kullanıcı Rehberi</a> sayfasını ziyaret edebilirsin. +Daha fazla özellik içeren daha kapsamlı bir örnek için <a href="https://fastapi.tiangolo.com/tr/tutorial/">Öğretici - Kullanıcı Rehberi</a>'ne bakın. -**Spoiler**: Öğretici - Kullanıcı rehberi şunları içerir: +**Spoiler alert**: öğretici - kullanıcı rehberi şunları içerir: -* **Parameterlerin**, **headers**, **çerezler**, **form alanları** ve **dosyalar** olarak tanımlanması. -* `maximum_length` ya da `regex` gibi **doğrulama kısıtlamalarının** nasıl yapılabileceği. -* Çok güçlü ve kullanımı kolay **<abbr title="Bağımlılık Enjeksiyonu: components, resources, providers, services, injectables olarak da biliniyor.">Bağımlılık Enjeksiyonu</abbr>** sistemi oluşturmayı. -* Güvenlik ve kimlik doğrulama, **JWT tokenleri** ile **OAuth2** desteği, ve **HTTP Basic** doğrulaması. -* İleri seviye fakat bir o kadarda basit olan **çok derin JSON modelleri** (Pydantic sayesinde). -* **GraphQL** entegrasyonu: <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a> ve diğer kütüphaneleri kullanarak. -* Diğer ekstra özellikler (Starlette sayesinde): - * **WebSocketler** - * HTTPX ve `pytest` sayesinde aşırı kolay testler. +* **parameter**'ların farklı yerlerden: **header**'lar, **cookie**'ler, **form alanları** ve **file**'lar olarak tanımlanması. +* `maximum_length` ya da `regex` gibi **doğrulama kısıtlamalarının** nasıl ayarlanacağı. +* Çok güçlü ve kullanımı kolay bir **<abbr title="components, resources, providers, services, injectables olarak da bilinir">Dependency Injection</abbr>** sistemi. +* **JWT tokens** ve **HTTP Basic** auth ile **OAuth2** desteği dahil güvenlik ve kimlik doğrulama. +* **Çok derin iç içe JSON modelleri** tanımlamak için daha ileri (ama aynı derecede kolay) teknikler (Pydantic sayesinde). +* <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a> ve diğer kütüphaneler ile **GraphQL** entegrasyonu. +* Starlette sayesinde gelen birçok ek özellik: + * **WebSockets** + * HTTPX ve `pytest` tabanlı aşırı kolay testler * **CORS** * **Cookie Sessions** * ...ve daha fazlası. -## Performans +### Uygulamanızı deploy edin (opsiyonel) { #deploy-your-app-optional } -Bağımsız TechEmpower kıyaslamaları gösteriyor ki, Uvicorn ile çalıştırılan **FastAPI** uygulamaları <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">en hızlı Python framework'lerinden birisi</a>, sadece Starlette ve Uvicorn'dan yavaş, ki FastAPI bunların üzerine kurulu bir kütüphanedir. +İsterseniz FastAPI uygulamanızı <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>'a deploy edebilirsiniz; eğer henüz yapmadıysanız gidip bekleme listesine katılın. 🚀 -Daha fazla bilgi için, bu bölüme bir göz at <a href="https://fastapi.tiangolo.com/tr/benchmarks/" class="internal-link" target="_blank">Kıyaslamalar</a>. +Zaten bir **FastAPI Cloud** hesabınız varsa (bekleme listesinden sizi davet ettiysek 😉), uygulamanızı tek bir komutla deploy edebilirsiniz. -## Opsiyonel Gereksinimler +Deploy etmeden önce, giriş yaptığınızdan emin olun: -Pydantic tarafında kullanılan: +<div class="termy"> + +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +</div> + +Sonra uygulamanızı deploy edin: + +<div class="termy"> + +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +</div> + +Hepsi bu! Artık uygulamanıza bu URL'den erişebilirsiniz. ✨ + +#### FastAPI Cloud hakkında { #about-fastapi-cloud } + +**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>**, **FastAPI**'ın arkasındaki aynı yazar ve ekip tarafından geliştirilmiştir. + +**Bir API'ı build etmek**, **deploy etmek** ve **erişmek** süreçlerini minimum eforla kolaylaştırır. + +FastAPI ile uygulama geliştirmenin sağladığı aynı **developer experience**'ı, onları cloud'a **deploy etmeye** de taşır. 🎉 + +FastAPI Cloud, *FastAPI and friends* open source projelerinin ana sponsoru ve finansman sağlayıcısıdır. ✨ + +#### Diğer cloud sağlayıcılarına deploy { #deploy-to-other-cloud-providers } + +FastAPI open source'tur ve standartlara dayanır. FastAPI uygulamalarını seçtiğiniz herhangi bir cloud sağlayıcısına deploy edebilirsiniz. + +FastAPI uygulamalarını onlarla deploy etmek için cloud sağlayıcınızın rehberlerini takip edin. 🤓 + +## Performans { #performance } + +Bağımsız TechEmpower kıyaslamaları, Uvicorn altında çalışan **FastAPI** uygulamalarının <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">mevcut en hızlı Python framework'lerinden biri</a> olduğunu gösteriyor; sadece Starlette ve Uvicorn'un kendisinin gerisinde (FastAPI tarafından dahili olarak kullanılır). (*) + +Daha iyi anlamak için <a href="https://fastapi.tiangolo.com/tr/benchmarks/" class="internal-link" target="_blank">Kıyaslamalar</a> bölümüne bakın. + +## Bağımlılıklar { #dependencies } + +FastAPI, Pydantic ve Starlette'a bağımlıdır. + +### `standard` Bağımlılıkları { #standard-dependencies } + +FastAPI'ı `pip install "fastapi[standard]"` ile yüklediğinizde, opsiyonel bağımlılıkların `standard` grubuyla birlikte gelir: + +Pydantic tarafından kullanılanlar: * <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email-validator</code></a> - email doğrulaması için. + +Starlette tarafından kullanılanlar: + +* <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - `TestClient` kullanmak istiyorsanız gereklidir. +* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - varsayılan template yapılandırmasını kullanmak istiyorsanız gereklidir. +* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - `request.form()` ile, form <abbr title="HTTP request'inden gelen string'i Python verisine dönüştürme">"parsing"</abbr> desteği istiyorsanız gereklidir. + +FastAPI tarafından kullanılanlar: + +* <a href="https://www.uvicorn.dev" target="_blank"><code>uvicorn</code></a> - uygulamanızı yükleyen ve servis eden server için. Buna, yüksek performanslı servis için gereken bazı bağımlılıkları (örn. `uvloop`) içeren `uvicorn[standard]` dahildir. +* `fastapi-cli[standard]` - `fastapi` komutunu sağlamak için. + * Buna, FastAPI uygulamanızı <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>'a deploy etmenizi sağlayan `fastapi-cloud-cli` dahildir. + +### `standard` Bağımlılıkları Olmadan { #without-standard-dependencies } + +`standard` opsiyonel bağımlılıklarını dahil etmek istemiyorsanız, `pip install "fastapi[standard]"` yerine `pip install fastapi` ile kurabilirsiniz. + +### `fastapi-cloud-cli` Olmadan { #without-fastapi-cloud-cli } + +FastAPI'ı standard bağımlılıklarla ama `fastapi-cloud-cli` olmadan kurmak istiyorsanız, `pip install "fastapi[standard-no-fastapi-cloud-cli]"` ile yükleyebilirsiniz. + +### Ek Opsiyonel Bağımlılıklar { #additional-optional-dependencies } + +Yüklemek isteyebileceğiniz bazı ek bağımlılıklar da vardır. + +Ek opsiyonel Pydantic bağımlılıkları: + * <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - ayar yönetimi için. -* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - Pydantic ile birlikte kullanılabilecek ek tipler için. +* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - Pydantic ile kullanılacak ek type'lar için. -Starlette tarafında kullanılan: +Ek opsiyonel FastAPI bağımlılıkları: -* <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - Eğer `TestClient` yapısını kullanacaksanız gereklidir. -* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - Eğer varsayılan template konfigürasyonunu kullanacaksanız gereklidir. -* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - Eğer `request.form()` ile form <abbr title="HTTP isteği ile gelen string veriyi Python nesnesine çevirme.">dönüşümü</abbr> desteğini kullanacaksanız gereklidir. -* <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - `SessionMiddleware` desteği için gerekli. -* <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - `SchemaGenerator` desteği için gerekli (Muhtemelen FastAPI kullanırken ihtiyacınız olmaz). +* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - `ORJSONResponse` kullanmak istiyorsanız gereklidir. +* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - `UJSONResponse` kullanmak istiyorsanız gereklidir. -Hem FastAPI hem de Starlette tarafından kullanılan: +## Lisans { #license } -* <a href="https://www.uvicorn.dev" target="_blank"><code>uvicorn</code></a> - oluşturduğumuz uygulamayı servis edecek web sunucusu görevini üstlenir. -* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - `ORJSONResponse` kullanacaksanız gereklidir. -* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - `UJSONResponse` kullanacaksanız gerekli. - -Bunların hepsini `pip install fastapi[all]` ile yükleyebilirsin. - -## Lisans - -Bu proje, MIT lisansı şartları altında lisanslanmıştır. +Bu proje MIT lisansı şartları altında lisanslanmıştır. diff --git a/docs/tr/docs/learn/index.md b/docs/tr/docs/learn/index.md index 52e3aa54d..accf971aa 100644 --- a/docs/tr/docs/learn/index.md +++ b/docs/tr/docs/learn/index.md @@ -1,5 +1,5 @@ -# Öğren +# Öğren { #learn } **FastAPI** öğrenmek için giriş bölümleri ve öğreticiler burada yer alıyor. -Burayı, bir **kitap**, bir **kurs**, ve FastAPI öğrenmenin **resmi** ve önerilen yolu olarak düşünülebilirsiniz. 😎 +Burayı, bir **kitap**, bir **kurs**, FastAPI öğrenmenin **resmi** ve önerilen yolu olarak düşünebilirsiniz. 😎 diff --git a/docs/tr/docs/project-generation.md b/docs/tr/docs/project-generation.md index c9dc24acc..bdc28f0c0 100644 --- a/docs/tr/docs/project-generation.md +++ b/docs/tr/docs/project-generation.md @@ -1,84 +1,28 @@ -# Proje oluşturma - Şablonlar +# Full Stack FastAPI Şablonu { #full-stack-fastapi-template } -Başlamak için bir proje oluşturucu kullanabilirsiniz, çünkü sizin için önceden yapılmış birçok başlangıç ​​kurulumu, güvenlik, veritabanı ve temel API endpoinlerini içerir. +Şablonlar genellikle belirli bir kurulumla gelir, ancak esnek ve özelleştirilebilir olacak şekilde tasarlanırlar. Bu sayede şablonu projenizin gereksinimlerine göre değiştirip uyarlayabilir, çok iyi bir başlangıç noktası olarak kullanabilirsiniz. 🏁 -Bir proje oluşturucu, her zaman kendi ihtiyaçlarınıza göre güncellemeniz ve uyarlamanız gereken esnek bir kuruluma sahip olacaktır, ancak bu, projeniz için iyi bir başlangıç ​​noktası olabilir. +Bu şablonu başlangıç için kullanabilirsiniz; çünkü ilk kurulumun, güvenliğin, veritabanının ve bazı API endpoint'lerinin önemli bir kısmı sizin için zaten hazırlanmıştır. -## Full Stack FastAPI PostgreSQL +GitHub Repository: <a href="https://github.com/tiangolo/full-stack-fastapi-template" class="external-link" target="_blank">Full Stack FastAPI Template</a> -GitHub: <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-fastapi-postgresql</a> +## Full Stack FastAPI Şablonu - Teknoloji Yığını ve Özellikler { #full-stack-fastapi-template-technology-stack-and-features } -### Full Stack FastAPI PostgreSQL - Özellikler - -* Full **Docker** entegrasyonu (Docker based). -* Docker Swarm Mode ile deployment. -* **Docker Compose** entegrasyonu ve lokal geliştirme için optimizasyon. -* Uvicorn ve Gunicorn ile **Production ready** Python web server'ı. -* Python <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">**FastAPI**</a> backend: - * **Hızlı**: **NodeJS** ve **Go** ile eşit, çok yüksek performans (Starlette ve Pydantic'e teşekkürler). - * **Sezgisel**: Editor desteğı. <abbr title="auto-complete, IntelliSense gibi isimlerle de bilinir">Otomatik tamamlama</abbr>. Daha az debugging. - * **Kolay**: Kolay öğrenip kolay kullanmak için tasarlandı. Daha az döküman okuma daha çok iş. - * **Kısa**: Minimum kod tekrarı. Her parametre bildiriminde birden çok özellik. - * **Güçlü**: Production-ready. Otomatik interaktif dökümantasyon. - * **Standartlara dayalı**: API'ler için açık standartlara dayanır (ve tamamen uyumludur): <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> ve <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Şeması</a>. - * <a href="https://fastapi.tiangolo.com/features/" class="external-link" target="_blank">**Birçok diger özelliği**</a> dahili otomatik doğrulama, serialization, interaktif dokümantasyon, OAuth2 JWT token ile authentication, vb. -* **Güvenli şifreleme** . -* **JWT token** kimlik doğrulama. -* **SQLAlchemy** models (Flask dan bağımsızdır. Celery worker'ları ile kullanılabilir). -* Kullanıcılar için temel başlangıç ​​modeli (gerektiği gibi değiştirin ve kaldırın). -* **Alembic** migration. -* **CORS** (Cross Origin Resource Sharing). -* **Celery** worker'ları ile backend içerisinden seçilen işleri çalıştırabilirsiniz. -* **Pytest**'e dayalı, Docker ile entegre REST backend testleri ile veritabanından bağımsız olarak tam API etkileşimini test edebilirsiniz. Docker'da çalıştığı için her seferinde sıfırdan yeni bir veri deposu oluşturabilir (böylece ElasticSearch, MongoDB, CouchDB veya ne istersen kullanabilirsin ve sadece API'nin çalışıp çalışmadığını test edebilirsin). -* Atom Hydrogen veya Visual Studio Code Jupyter gibi uzantılarla uzaktan veya Docker içi geliştirme için **Jupyter Çekirdekleri** ile kolay Python entegrasyonu. -* **Vue** ile frontend: - * Vue CLI ile oluşturulmuş. - * Dahili **JWT kimlik doğrulama**. - * Dahili Login. - * Login sonrası, Kontrol paneli. - * Kullanıcı oluşturma ve düzenleme kontrol paneli - * Kendi kendine kullanıcı sürümü. - * **Vuex**. - * **Vue-router**. - * **Vuetify** güzel material design kompanentleri için. - * **TypeScript**. - * **Nginx** tabanlı Docker sunucusu (Vue-router için yapılandırılmış). - * Docker ile multi-stage yapı, böylece kodu derlemeniz, kaydetmeniz veya işlemeniz gerekmez. - * Derleme zamanında Frontend testi (devre dışı bırakılabilir). - * Mümkün olduğu kadar modüler yapılmıştır, bu nedenle kutudan çıktığı gibi çalışır, ancak Vue CLI ile yeniden oluşturabilir veya ihtiyaç duyduğunuz şekilde oluşturabilir ve istediğinizi yeniden kullanabilirsiniz. -* **PGAdmin** PostgreSQL database admin tool'u, PHPMyAdmin ve MySQL ile kolayca değiştirilebilir. -* **Flower** ile Celery job'larını monitörleme. -* **Traefik** ile backend ve frontend arasında yük dengeleme, böylece her ikisini de aynı domain altında, path ile ayrılmış, ancak farklı kapsayıcılar tarafından sunulabilirsiniz. -* Let's Encrypt **HTTPS** sertifikalarının otomatik oluşturulması dahil olmak üzere Traefik entegrasyonu. -* GitLab **CI** (sürekli entegrasyon), backend ve frontend testi dahil. - -## Full Stack FastAPI Couchbase - -GitHub: <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-fastapi-couchbase</a> - -⚠️ **UYARI** ⚠️ - -Sıfırdan bir projeye başlıyorsanız alternatiflerine bakın. - -Örneğin, <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" class="external-link" target="_blank">Full Stack FastAPI PostgreSQL</a> daha iyi bir alternatif olabilir, aktif olarak geliştiriliyor ve kullanılıyor. Ve yeni özellik ve ilerlemelere sahip. - -İsterseniz Couchbase tabanlı generator'ı kullanmakta özgürsünüz, hala iyi çalışıyor olmalı ve onunla oluşturulmuş bir projeniz varsa bu da sorun değil (ve muhtemelen zaten ihtiyaçlarınıza göre güncellediniz). - -Bununla ilgili daha fazla bilgiyi repo belgelerinde okuyabilirsiniz. - -## Full Stack FastAPI MongoDB - -... müsaitliğime ve diğer faktörlere bağlı olarak daha sonra gelebilir. 😅 🎉 - -## Machine Learning modelleri, spaCy ve FastAPI - -GitHub: <a href="https://github.com/microsoft/cookiecutter-spacy-fastapi" class="external-link" target="_blank">https://github.com/microsoft/cookiecutter-spacy-fastapi</a> - -### Machine Learning modelleri, spaCy ve FastAPI - Features - -* **spaCy** NER model entegrasyonu. -* **Azure Cognitive Search** yerleşik istek biçimi. -* Uvicorn ve Gunicorn ile **Production ready** Python web server'ı. -* Dahili **Azure DevOps** Kubernetes (AKS) CI/CD deployment. -* **Multilingual**, Proje kurulumu sırasında spaCy'nin yerleşik dillerinden birini kolayca seçin. -* **Esnetilebilir** diğer frameworkler (Pytorch, Tensorflow) ile de çalışır sadece spaCy değil. +- ⚡ Python backend API için [**FastAPI**](https://fastapi.tiangolo.com/tr). + - 🧰 Python SQL veritabanı etkileşimleri (ORM) için [SQLModel](https://sqlmodel.tiangolo.com). + - 🔍 FastAPI'nin kullandığı; veri doğrulama ve ayarlar yönetimi için [Pydantic](https://docs.pydantic.dev). + - 💾 SQL veritabanı olarak [PostgreSQL](https://www.postgresql.org). +- 🚀 frontend için [React](https://react.dev). + - 💃 TypeScript, hooks, Vite ve modern bir frontend stack'inin diğer parçalarını kullanır. + - 🎨 frontend component'leri için [Tailwind CSS](https://tailwindcss.com) ve [shadcn/ui](https://ui.shadcn.com). + - 🤖 Otomatik üretilen bir frontend client. + - 🧪 End-to-End testleri için [Playwright](https://playwright.dev). + - 🦇 Dark mode desteği. +- 🐋 Geliştirme ve production için [Docker Compose](https://www.docker.com). +- 🔒 Varsayılan olarak güvenli password hashing. +- 🔑 JWT (JSON Web Token) authentication. +- 📫 E-posta tabanlı şifre kurtarma. +- ✅ [Pytest](https://pytest.org) ile testler. +- 📞 Reverse proxy / load balancer olarak [Traefik](https://traefik.io). +- 🚢 Docker Compose kullanarak deployment talimatları; otomatik HTTPS sertifikalarını yönetmek için bir frontend Traefik proxy'sini nasıl kuracağınız dahil. +- 🏭 GitHub Actions tabanlı CI (continuous integration) ve CD (continuous deployment). diff --git a/docs/tr/docs/python-types.md b/docs/tr/docs/python-types.md index b44aa3b9d..01a3efe98 100644 --- a/docs/tr/docs/python-types.md +++ b/docs/tr/docs/python-types.md @@ -1,76 +1,74 @@ -# Python Veri Tiplerine Giriş +# Python Tiplerine Giriş { #python-types-intro } -Python isteğe bağlı olarak "tip belirteçlerini" destekler. +Python, isteğe bağlı "type hints" (diğer adıyla "type annotations") desteğine sahiptir. - **"Tip belirteçleri"** bir değişkenin <abbr title="örneğin: str, int, float, bool">tipinin</abbr> belirtilmesine olanak sağlayan özel bir sözdizimidir. +Bu **"type hints"** veya annotations, bir değişkenin <abbr title="örneğin: str, int, float, bool">type</abbr>'ını bildirmeye yarayan özel bir sözdizimidir. -Değişkenlerin tiplerini belirterek editör ve araçlardan daha fazla destek alabilirsiniz. +Değişkenleriniz için type bildirerek, editörler ve araçlar size daha iyi destek sağlayabilir. -Bu pythonda tip belirteçleri için **hızlı bir başlangıç / bilgi tazeleme** rehberidir . Bu rehber **FastAPI** kullanmak için gereken minimum konuyu kapsar ki bu da çok az bir miktardır. +Bu, Python type hints hakkında sadece **hızlı bir eğitim / bilgi tazeleme** dokümanıdır. **FastAPI** ile kullanmak için gereken minimum bilgiyi kapsar... ki aslında bu çok azdır. -**FastAPI' nin** tamamı bu tür tip belirteçleri ile donatılmıştır ve birçok avantaj sağlamaktadır. +**FastAPI** tamamen bu type hints üzerine kuruludur; bunlar ona birçok avantaj ve fayda sağlar. -**FastAPI** kullanmayacak olsanız bile tür belirteçleri hakkında bilgi edinmenizde fayda var. +Ancak hiç **FastAPI** kullanmasanız bile, bunlar hakkında biraz öğrenmeniz size fayda sağlayacaktır. /// note | Not -Python uzmanıysanız ve tip belirteçleri ilgili her şeyi zaten biliyorsanız, sonraki bölüme geçin. +Eğer bir Python uzmanıysanız ve type hints hakkında her şeyi zaten biliyorsanız, sonraki bölüme geçin. /// -## Motivasyon +## Motivasyon { #motivation } -Basit bir örnek ile başlayalım: +Basit bir örnekle başlayalım: -{* ../../docs_src/python_types/tutorial001.py *} +{* ../../docs_src/python_types/tutorial001_py39.py *} - -Programın çıktısı: +Bu programı çalıştırınca şu çıktıyı alırsınız: ``` John Doe ``` -Fonksiyon sırayla şunları yapar: +Fonksiyon şunları yapar: * `first_name` ve `last_name` değerlerini alır. -* `title()` ile değişkenlerin ilk karakterlerini büyütür. -* Değişkenleri aralarında bir boşlukla beraber <abbr title="Onları bir bütün olarak sırayla birleştirir.">Birleştirir</abbr>. +* `title()` ile her birinin ilk harfini büyük harfe çevirir. +* Ortada bir boşluk olacak şekilde <abbr title="Hepsini, tek bir bütün olacak şekilde bir araya koyar. İçerikler ardışık şekilde yer alır.">Concatenates</abbr> eder. -{* ../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} - -### Düzenle +### Düzenleyelim { #edit-it } Bu çok basit bir program. -Ama şimdi sıfırdan yazdığınızı hayal edin. +Ama şimdi bunu sıfırdan yazdığınızı hayal edin. -Bir noktada fonksiyonun tanımına başlayacaktınız, parametreleri hazır hale getirdiniz... +Bir noktada fonksiyon tanımını yazmaya başlamış olacaktınız, parametreler hazır... -Ama sonra "ilk harfi büyük harfe dönüştüren yöntemi" çağırmanız gerekir. +Ama sonra "ilk harfi büyük harfe çeviren method"u çağırmanız gerekiyor. - `upper` mıydı ? Yoksa `uppercase`' mi? `first_uppercase`? `capitalize`? +`upper` mıydı? `uppercase` miydi? `first_uppercase`? `capitalize`? -Ardından, programcıların en iyi arkadaşı olan otomatik tamamlama ile denediniz. +Sonra eski programcı dostuyla denersiniz: editör autocomplete. -'first_name', ardından bir nokta ('.') yazıp otomatik tamamlamayı tetiklemek için 'Ctrl+Space' tuşlarına bastınız. +Fonksiyonun ilk parametresi olan `first_name`'i yazarsınız, sonra bir nokta (`.`) ve ardından autocomplete'i tetiklemek için `Ctrl+Space`'e basarsınız. -Ancak, ne yazık ki, yararlı hiçbir şey elde edemediniz: +Ama ne yazık ki, işe yarar bir şey göremezsiniz: <img src="/img/python-types/image01.png"> -### Tipleri ekle +### Tipleri ekleyelim { #add-types } -Önceki sürümden sadece bir satırı değiştirelim. +Önceki sürümden tek bir satırı değiştirelim. -Tam olarak bu parçayı, işlevin parametrelerini değiştireceğiz: +Fonksiyonun parametreleri olan şu parçayı: ```Python first_name, last_name ``` -ve bu hale getireceğiz: +şuna çevireceğiz: ```Python first_name: str, last_name: str @@ -78,58 +76,55 @@ ve bu hale getireceğiz: Bu kadar. -İşte bunlar "tip belirteçleri": +Bunlar "type hints": -{* ../../docs_src/python_types/tutorial002.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} - -Bu, aşağıdaki gibi varsayılan değerleri bildirmekle aynı şey değildir: +Bu, aşağıdaki gibi default değerler bildirmekle aynı şey değildir: ```Python first_name="john", last_name="doe" ``` -Bu tamamen farklı birşey +Bu farklı bir şey. -İki nokta üst üste (`:`) kullanıyoruz , eşittir (`=`) değil. +Eşittir (`=`) değil, iki nokta (`:`) kullanıyoruz. -Normalde tip belirteçleri eklemek, kod üzerinde olacakları değiştirmez. +Ve type hints eklemek, normalde onlarsız ne oluyorsa onu değiştirmez. -Şimdi programı sıfırdan birdaha yazdığınızı hayal edin. +Ama şimdi, type hints ile o fonksiyonu oluşturmanın ortasında olduğunuzu tekrar hayal edin. -Aynı noktada, `Ctrl+Space` ile otomatik tamamlamayı tetiklediniz ve şunu görüyorsunuz: +Aynı noktada, `Ctrl+Space` ile autocomplete'i tetiklemeye çalışırsınız ve şunu görürsünüz: <img src="/img/python-types/image02.png"> -Aradığınızı bulana kadar seçenekleri kaydırabilirsiniz: +Bununla birlikte, seçenekleri görerek kaydırabilirsiniz; ta ki "tanıdık gelen" seçeneği bulana kadar: <img src="/img/python-types/image03.png"> -## Daha fazla motivasyon +## Daha fazla motivasyon { #more-motivation } -Bu fonksiyon, zaten tür belirteçlerine sahip: +Şu fonksiyona bakın, zaten type hints içeriyor: -{* ../../docs_src/python_types/tutorial003.py hl[1] *} +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} - -Editör değişkenlerin tiplerini bildiğinden, yalnızca otomatik tamamlama değil, hata kontrolleri de sağlar: +Editör değişkenlerin tiplerini bildiği için, sadece completion değil, aynı zamanda hata kontrolleri de alırsınız: <img src="/img/python-types/image04.png"> -Artık `age` değişkenini `str(age)` olarak kullanmanız gerektiğini biliyorsunuz: +Artık bunu düzeltmeniz gerektiğini, `age`'i `str(age)` ile string'e çevirmeniz gerektiğini biliyorsunuz: -{* ../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} +## Tipleri bildirmek { #declaring-types } -## Tip bildirme +Type hints bildirmek için ana yeri az önce gördünüz: fonksiyon parametreleri. -Az önce tip belirteçlerinin en çok kullanıldığı yeri gördünüz. +Bu, **FastAPI** ile kullanırken de onları en çok kullanacağınız yerdir. - **FastAPI**ile çalışırken tip belirteçlerini en çok kullanacağımız yer yine fonksiyonlardır. +### Basit tipler { #simple-types } -### Basit tipler - -Yalnızca `str` değil, tüm standart Python tiplerinin bildirebilirsiniz. +Sadece `str` değil, tüm standart Python tiplerini bildirebilirsiniz. Örneğin şunları kullanabilirsiniz: @@ -138,176 +133,332 @@ Yalnızca `str` değil, tüm standart Python tiplerinin bildirebilirsiniz. * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} +### Tip parametreleri ile Generic tipler { #generic-types-with-type-parameters } -### Tip parametreleri ile Generic tipler +`dict`, `list`, `set` ve `tuple` gibi, başka değerler içerebilen bazı veri yapıları vardır. Ve iç değerlerin kendi tipi de olabilir. -"dict", "list", "set" ve "tuple" gibi diğer değerleri içerebilen bazı veri yapıları vardır. Ve dahili değerlerinin de tip belirtecleri olabilir. +İç tipleri olan bu tiplere "**generic**" tipler denir. Ve bunları, iç tipleriyle birlikte bildirmek mümkündür. -Bu tipleri ve dahili tpileri bildirmek için standart Python modülünü "typing" kullanabilirsiniz. +Bu tipleri ve iç tipleri bildirmek için standart Python modülü `typing`'i kullanabilirsiniz. Bu modül, özellikle bu type hints desteği için vardır. -Bu tür tip belirteçlerini desteklemek için özel olarak mevcuttur. +#### Python'un daha yeni sürümleri { #newer-versions-of-python } -#### `List` +`typing` kullanan sözdizimi, Python 3.6'dan en yeni sürümlere kadar (Python 3.9, Python 3.10, vb. dahil) tüm sürümlerle **uyumludur**. -Örneğin `str` değerlerden oluşan bir `list` tanımlayalım. +Python geliştikçe, **daha yeni sürümler** bu type annotations için daha iyi destekle gelir ve çoğu durumda type annotations bildirmek için `typing` modülünü import edip kullanmanız bile gerekmez. -From `typing`, import `List` (büyük harf olan `L` ile): +Projeniz için daha yeni bir Python sürümü seçebiliyorsanız, bu ek sadelikten yararlanabilirsiniz. -{* ../../docs_src/python_types/tutorial006.py hl[1] *} +Tüm dokümanlarda her Python sürümüyle uyumlu örnekler vardır (fark olduğunda). +Örneğin "**Python 3.6+**", Python 3.6 veya üstüyle (3.7, 3.8, 3.9, 3.10, vb. dahil) uyumludur. "**Python 3.9+**" ise Python 3.9 veya üstüyle (3.10 vb. dahil) uyumludur. -Değişkenin tipini yine iki nokta üstüste (`:`) ile belirleyin. +Eğer **Python'un en güncel sürümlerini** kullanabiliyorsanız, en güncel sürüme ait örnekleri kullanın; bunlar **en iyi ve en basit sözdizimine** sahip olur, örneğin "**Python 3.10+**". -tip olarak `List` kullanın. +#### List { #list } -Liste, bazı dahili tipleri içeren bir tür olduğundan, bunları köşeli parantez içine alırsınız: +Örneğin, `str`'lerden oluşan bir `list` olan bir değişken tanımlayalım. -{* ../../docs_src/python_types/tutorial006.py hl[4] *} +Değişkeni, aynı iki nokta (`:`) sözdizimiyle bildirin. +Type olarak `list` yazın. -/// tip | Ipucu +`list`, bazı iç tipleri barındıran bir tip olduğundan, bunları köşeli parantez içine yazarsınız: -Köşeli parantez içindeki bu dahili tiplere "tip parametreleri" denir. +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} -Bu durumda `str`, `List`e iletilen tür parametresidir. +/// info | Bilgi + +Köşeli parantez içindeki bu iç tiplere "type parameters" denir. + +Bu durumda `str`, `list`'e verilen type parameter'dır. /// -Bunun anlamı şudur: "`items` değişkeni bir `list`tir ve bu listedeki öğelerin her biri bir `str`dir". +Bu şu demektir: "`items` değişkeni bir `list` ve bu listedeki her bir öğe `str`". -Bunu yaparak, düzenleyicinizin listedeki öğeleri işlerken bile destek sağlamasını sağlayabilirsiniz: +Bunu yaparak, editörünüz listeden öğeleri işlerken bile destek sağlayabilir: <img src="/img/python-types/image05.png"> -Tip belirteçleri olmadan, bunu başarmak neredeyse imkansızdır. +Tipler olmadan, bunu başarmak neredeyse imkansızdır. -`item` değişkeninin `items` listesindeki öğelerden biri olduğuna dikkat edin. +`item` değişkeninin, `items` listesindeki elemanlardan biri olduğuna dikkat edin. -Ve yine, editör bunun bir `str` ​​olduğunu biliyor ve bunun için destek sağlıyor. +Ve yine de editör bunun bir `str` olduğunu bilir ve buna göre destek sağlar. -#### `Tuple` ve `Set` +#### Tuple ve Set { #tuple-and-set } -`Tuple` ve `set`lerin tiplerini bildirmek için de aynısını yapıyoruz: - -{* ../../docs_src/python_types/tutorial007.py hl[1,4] *} - - -Bu şu anlama geliyor: - -* `items_t` değişkeni sırasıyla `int`, `int`, ve `str` tiplerinden oluşan bir `tuple` türündedir . -* `items_s` ise her öğesi `bytes` türünde olan bir `set` örneğidir. - -#### `Dict` - -Bir `dict` tanımlamak için virgülle ayrılmış iki parametre verebilirsiniz. - -İlk tip parametresi `dict` değerinin `key` değeri içindir. - -İkinci parametre ise `dict` değerinin `value` değeri içindir: - -{* ../../docs_src/python_types/tutorial008.py hl[1,4] *} +`tuple`'ları ve `set`'leri bildirmek için de aynısını yaparsınız: +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} Bu şu anlama gelir: -* `prices` değişkeni `dict` tipindedir: - * `dict` değişkeninin `key` değeri `str` tipindedir (herbir item'ın "name" değeri). - * `dict` değişkeninin `value` değeri `float` tipindedir (lherbir item'ın "price" değeri). +* `items_t` değişkeni 3 öğeli bir `tuple`'dır: bir `int`, bir başka `int` ve bir `str`. +* `items_s` değişkeni bir `set`'tir ve her bir öğesi `bytes` tipindedir. -#### `Optional` +#### Dict { #dict } -`Optional` bir değişkenin `str`gibi bir tipi olabileceğini ama isteğe bağlı olarak tipinin `None` olabileceğini belirtir: +Bir `dict` tanımlamak için, virgülle ayrılmış 2 type parameter verirsiniz. -```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} +İlk type parameter, `dict`'in key'leri içindir. + +İkinci type parameter, `dict`'in value'ları içindir: + +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} + +Bu şu anlama gelir: + +* `prices` değişkeni bir `dict`'tir: + * Bu `dict`'in key'leri `str` tipindedir (örneğin her bir öğenin adı). + * Bu `dict`'in value'ları `float` tipindedir (örneğin her bir öğenin fiyatı). + +#### Union { #union } + +Bir değişkenin **birkaç tipten herhangi biri** olabileceğini bildirebilirsiniz; örneğin bir `int` veya bir `str`. + +Python 3.6 ve üzeri sürümlerde (Python 3.10 dahil), `typing` içinden `Union` tipini kullanabilir ve köşeli parantez içine kabul edilecek olası tipleri yazabilirsiniz. + +Python 3.10'da ayrıca, olası tipleri <abbr title='also called "bitwise or operator", but that meaning is not relevant here'>vertical bar (`|`)</abbr> ile ayırabildiğiniz **yeni bir sözdizimi** de vardır. + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../docs_src/python_types/tutorial008b_py310.py!} ``` -`str` yerine `Optional[str]` kullanmak editorün bu değerin her zaman `str` tipinde değil bazen `None` tipinde de olabileceğini belirtir ve hataları tespit etmemizde yardımcı olur. +//// -#### Generic tipler +//// tab | Python 3.9+ -Köşeli parantez içinde tip parametreleri alan bu türler, örneğin: +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial008b_py39.py!} +``` -* `List` -* `Tuple` -* `Set` -* `Dict` +//// + +Her iki durumda da bu, `item`'ın `int` veya `str` olabileceği anlamına gelir. + +#### Muhtemelen `None` { #possibly-none } + +Bir değerin `str` gibi bir tipi olabileceğini ama aynı zamanda `None` da olabileceğini bildirebilirsiniz. + +Python 3.6 ve üzeri sürümlerde (Python 3.10 dahil), `typing` modülünden `Optional` import edip kullanarak bunu bildirebilirsiniz. + +```Python hl_lines="1 4" +{!../../docs_src/python_types/tutorial009_py39.py!} +``` + +Sadece `str` yerine `Optional[str]` kullanmak, aslında değer `None` olabilecekken her zaman `str` olduğunu varsaydığınız hataları editörün yakalamanıza yardımcı olmasını sağlar. + +`Optional[Something]`, aslında `Union[Something, None]` için bir kısayoldur; eşdeğerdirler. + +Bu aynı zamanda Python 3.10'da `Something | None` kullanabileceğiniz anlamına gelir: + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../docs_src/python_types/tutorial009_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009_py39.py!} +``` + +//// + +//// tab | Python 3.9+ alternatif + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009b_py39.py!} +``` + +//// + +#### `Union` veya `Optional` kullanmak { #using-union-or-optional } + +Python sürümünüz 3.10'un altındaysa, benim oldukça **öznel** bakış açıma göre küçük bir ipucu: + +* 🚨 `Optional[SomeType]` kullanmaktan kaçının +* Bunun yerine ✨ **`Union[SomeType, None]` kullanın** ✨. + +İkisi eşdeğerdir ve altta aynı şeydir; ama ben `Optional` yerine `Union` önermeyi tercih ederim. Çünkü "**optional**" kelimesi değerin optional olduğunu ima ediyor gibi durur; ama gerçekte anlamı "değer `None` olabilir"dir. Değer optional olmasa ve hâlâ required olsa bile. + +Bence `Union[SomeType, None]` ne anlama geldiğini daha açık şekilde ifade ediyor. + +Bu, tamamen kelimeler ve isimlendirmelerle ilgili. Ancak bu kelimeler, sizin ve ekip arkadaşlarınızın kod hakkında nasıl düşündüğünü etkileyebilir. + +Örnek olarak şu fonksiyonu ele alalım: + +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} + +`name` parametresi `Optional[str]` olarak tanımlanmış, ama **optional değil**; parametre olmadan fonksiyonu çağıramazsınız: + +```Python +say_hi() # Oh, no, this throws an error! 😱 +``` + +`name` parametresi **hâlâ required**'dır (*optional* değildir) çünkü bir default değeri yoktur. Yine de `name`, değer olarak `None` kabul eder: + +```Python +say_hi(name=None) # This works, None is valid 🎉 +``` + +İyi haber şu ki, Python 3.10'a geçtiğinizde bununla uğraşmanız gerekmeyecek; çünkü tiplerin union'larını tanımlamak için doğrudan `|` kullanabileceksiniz: + +{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *} + +Ve böylece `Optional` ve `Union` gibi isimlerle de uğraşmanız gerekmeyecek. 😎 + +#### Generic tipler { #generic-types } + +Köşeli parantez içinde type parameter alan bu tiplere **Generic types** veya **Generics** denir, örneğin: + +//// tab | Python 3.10+ + +Aynı builtin tipleri generics olarak kullanabilirsiniz (köşeli parantez ve içindeki tiplerle): + +* `list` +* `tuple` +* `set` +* `dict` + +Ve önceki Python sürümlerinde olduğu gibi `typing` modülünden: + +* `Union` * `Optional` * ...and others. -**Generic types** yada **Generics** olarak adlandırılır. +Python 3.10'da, `Union` ve `Optional` generics'lerini kullanmaya alternatif olarak, tip union'larını bildirmek için <abbr title='also called "bitwise or operator", but that meaning is not relevant here'>vertical bar (`|`)</abbr> kullanabilirsiniz; bu çok daha iyi ve daha basittir. -### Tip olarak Sınıflar +//// -Bir değişkenin tipini bir sınıf ile bildirebilirsiniz. +//// tab | Python 3.9+ -Diyelim ki `name` değerine sahip `Person` sınıfınız var: +Aynı builtin tipleri generics olarak kullanabilirsiniz (köşeli parantez ve içindeki tiplerle): -{* ../../docs_src/python_types/tutorial010.py hl[1:3] *} +* `list` +* `tuple` +* `set` +* `dict` +Ve `typing` modülünden gelen generics: -Sonra bir değişkeni 'Person' tipinde tanımlayabilirsiniz: +* `Union` +* `Optional` +* ...and others. -{* ../../docs_src/python_types/tutorial010.py hl[6] *} +//// +### Tip olarak sınıflar { #classes-as-types } -Ve yine bütün editör desteğini alırsınız: +Bir sınıfı da bir değişkenin tipi olarak bildirebilirsiniz. + +Örneğin, adı olan bir `Person` sınıfınız olsun: + +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} + +Sonra bir değişkeni `Person` tipinde olacak şekilde bildirebilirsiniz: + +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} + +Ve sonra, yine tüm editör desteğini alırsınız: <img src="/img/python-types/image06.png"> -## Pydantic modelleri +Bunun "`one_person`, `Person` sınıfının bir **instance**'ıdır" anlamına geldiğine dikkat edin. -<a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> veri doğrulaması yapmak için bir Python kütüphanesidir. +"`one_person`, `Person` adlı **class**'tır" anlamına gelmez. -Verilerin "biçimini" niteliklere sahip sınıflar olarak düzenlersiniz. +## Pydantic modelleri { #pydantic-models } -Ve her niteliğin bir türü vardır. +<a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a>, data validation yapmak için bir Python kütüphanesidir. -Sınıfın bazı değerlerle bir örneğini oluşturursunuz ve değerleri doğrular, bunları uygun türe dönüştürür ve size tüm verileri içeren bir nesne verir. +Verinin "shape"'ini attribute'lara sahip sınıflar olarak tanımlarsınız. -Ve ortaya çıkan nesne üzerindeki bütün editör desteğini alırsınız. +Ve her attribute'un bir tipi vardır. -Resmi Pydantic dokümanlarından alınmıştır: +Ardından o sınıfın bir instance'ını bazı değerlerle oluşturursunuz; bu değerleri doğrular, uygun tipe dönüştürür (gerekliyse) ve size tüm veriyi içeren bir nesne verir. -{* ../../docs_src/python_types/tutorial011.py *} +Ve bu ortaya çıkan nesne ile tüm editör desteğini alırsınız. +Resmî Pydantic dokümanlarından bir örnek: -/// info +{* ../../docs_src/python_types/tutorial011_py310.py *} -Daha fazla şey öğrenmek için <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic'i takip edin</a>. +/// info | Bilgi + +Daha fazlasını öğrenmek için <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic'in dokümanlarına bakın</a>. /// -**FastAPI** tamamen Pydantic'e dayanmaktadır. +**FastAPI** tamamen Pydantic üzerine kuruludur. -Daha fazlasini görmek için [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. +Bunların pratikte nasıl çalıştığını [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank} içinde çok daha fazla göreceksiniz. -## **FastAPI** tip belirteçleri +/// tip | İpucu -**FastAPI** birkaç şey yapmak için bu tür tip belirteçlerinden faydalanır. - -**FastAPI** ile parametre tiplerini bildirirsiniz ve şunları elde edersiniz: - -* **Editor desteği**. -* **Tip kontrolü**. - -...ve **FastAPI** aynı belirteçleri şunlar için de kullanıyor: - -* **Gereksinimleri tanımlama**: request path parameters, query parameters, headers, bodies, dependencies, ve benzeri gereksinimlerden -* **Verileri çevirme**: Gönderilen veri tipinden istenilen veri tipine çevirme. -* **Verileri doğrulama**: Her gönderilen verinin: - * doğrulanması ve geçersiz olduğunda **otomatik hata** oluşturma. -* OpenAPI kullanarak apinizi **Belgeleyin** : - * bu daha sonra otomatik etkileşimli dokümantasyon kullanıcı arayüzü tarafından kullanılır. - -Bütün bunlar kulağa soyut gelebilir. Merak etme. Tüm bunları çalışırken göreceksiniz. [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. - -Önemli olan, standart Python türlerini tek bir yerde kullanarak (daha fazla sınıf, dekoratör vb. eklemek yerine), **FastAPI**'nin bizim için işi yapmasını sağlamak. - -/// info - -Tüm öğreticiyi zaten okuduysanız ve türler hakkında daha fazla bilgi için geri döndüyseniz, iyi bir kaynak:<a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank"> the "cheat sheet" from `mypy`</a>. +Pydantic, default value olmadan `Optional` veya `Union[Something, None]` kullandığınızda özel bir davranışa sahiptir; bununla ilgili daha fazla bilgiyi Pydantic dokümanlarında <a href="https://docs.pydantic.dev/2.3/usage/models/#required-fields" class="external-link" target="_blank">Required Optional fields</a> bölümünde okuyabilirsiniz. + +/// + +## Metadata Annotations ile Type Hints { #type-hints-with-metadata-annotations } + +Python'da ayrıca, `Annotated` kullanarak bu type hints içine **ek <abbr title="Veri hakkında veri; bu durumda type hakkında bilgi, örneğin bir açıklama.">metadata</abbr>** koymayı sağlayan bir özellik de vardır. + +Python 3.9'dan itibaren `Annotated`, standart kütüphanenin bir parçasıdır; bu yüzden `typing` içinden import edebilirsiniz. + +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} + +Python'un kendisi bu `Annotated` ile bir şey yapmaz. Editörler ve diğer araçlar için tip hâlâ `str`'dir. + +Ama **FastAPI**'ye uygulamanızın nasıl davranmasını istediğinize dair ek metadata sağlamak için `Annotated` içindeki bu alanı kullanabilirsiniz. + +Hatırlanması gereken önemli nokta: `Annotated`'a verdiğiniz **ilk *type parameter***, **gerçek tip**tir. Geri kalanı ise diğer araçlar için metadatadır. + +Şimdilik, sadece `Annotated`'ın var olduğunu ve bunun standart Python olduğunu bilmeniz yeterli. 😎 + +İleride bunun ne kadar **güçlü** olabildiğini göreceksiniz. + +/// tip | İpucu + +Bunun **standart Python** olması, editörünüzde mümkün olan **en iyi developer experience**'ı almaya devam edeceğiniz anlamına gelir; kodu analiz etmek ve refactor etmek için kullandığınız araçlarla da, vb. ✨ + +Ayrıca kodunuzun pek çok başka Python aracı ve kütüphanesiyle çok uyumlu olacağı anlamına gelir. 🚀 + +/// + +## **FastAPI**'de type hints { #type-hints-in-fastapi } + +**FastAPI**, birkaç şey yapmak için bu type hints'ten faydalanır. + +**FastAPI** ile type hints kullanarak parametreleri bildirirsiniz ve şunları elde edersiniz: + +* **Editör desteği**. +* **Tip kontrolleri**. + +...ve **FastAPI** aynı bildirimleri şunlar için de kullanır: + +* **Gereksinimleri tanımlamak**: request path parameters, query parameters, headers, bodies, dependencies, vb. +* **Veriyi dönüştürmek**: request'ten gerekli tipe. +* **Veriyi doğrulamak**: her request'ten gelen veriyi: + * Veri geçersiz olduğunda client'a dönen **otomatik hatalar** üretmek. +* OpenAPI kullanarak API'yi **dokümante etmek**: + * bu, daha sonra otomatik etkileşimli dokümantasyon kullanıcı arayüzleri tarafından kullanılır. + +Bunların hepsi kulağa soyut gelebilir. Merak etmeyin. Tüm bunları [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank} içinde çalışırken göreceksiniz. + +Önemli olan, standart Python tiplerini tek bir yerde kullanarak (daha fazla sınıf, decorator vb. eklemek yerine), **FastAPI**'nin sizin için işin büyük kısmını yapmasıdır. + +/// info | Bilgi + +Tüm tutorial'ı zaten bitirdiyseniz ve tipler hakkında daha fazlasını görmek için geri döndüyseniz, iyi bir kaynak: <a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank">`mypy`'nin "cheat sheet"i</a>. /// diff --git a/docs/tr/docs/resources/index.md b/docs/tr/docs/resources/index.md index fc71a9ca1..884052f79 100644 --- a/docs/tr/docs/resources/index.md +++ b/docs/tr/docs/resources/index.md @@ -1,3 +1,3 @@ -# Kaynaklar +# Kaynaklar { #resources } -Ek kaynaklar, dış bağlantılar, makaleler ve daha fazlası. ✈️ +Ek kaynaklar, dış bağlantılar ve daha fazlası. ✈️ diff --git a/docs/tr/docs/tutorial/cookie-params.md b/docs/tr/docs/tutorial/cookie-params.md index f07508c2f..18eedab7f 100644 --- a/docs/tr/docs/tutorial/cookie-params.md +++ b/docs/tr/docs/tutorial/cookie-params.md @@ -1,35 +1,45 @@ -# Çerez (Cookie) Parametreleri +# Çerez (Cookie) Parametreleri { #cookie-parameters } -`Query` (Sorgu) ve `Path` (Yol) parametrelerini tanımladığınız şekilde çerez parametreleri tanımlayabilirsiniz. +`Query` ve `Path` parametrelerini tanımladığınız şekilde Cookie parametreleri tanımlayabilirsiniz. -## Import `Cookie` +## `Cookie`'yi Import Edin { #import-cookie } -Öncelikle, `Cookie`'yi projenize dahil edin: +Öncelikle, `Cookie`'yi import edin: {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[3] *} -## `Cookie` Parametrelerini Tanımlayın +## `Cookie` Parametrelerini Tanımlayın { #declare-cookie-parameters } -Çerez parametrelerini `Path` veya `Query` tanımlaması yapar gibi tanımlayın. +Ardından, `Path` ve `Query` ile aynı yapıyı kullanarak Cookie parametrelerini tanımlayın. -İlk değer varsayılan değerdir; tüm ekstra doğrulama veya belirteç parametrelerini kullanabilirsiniz: +Varsayılan değeri ve tüm ekstra doğrulama veya annotation parametrelerini tanımlayabilirsiniz: {* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[9] *} /// note | Teknik Detaylar -`Cookie` sınıfı `Path` ve `Query` sınıflarının kardeşidir. Diğerleri gibi `Param` sınıfını miras alan bir sınıftır. +`Cookie`, `Path` ve `Query`'nin "kardeş" sınıfıdır. O da aynı ortak `Param` sınıfından miras alır. -Ancak `fastapi`'dan projenize dahil ettiğiniz `Query`, `Path`, `Cookie` ve diğerleri aslında özel sınıflar döndüren birer fonksiyondur. +Ancak `fastapi`'dan `Query`, `Path`, `Cookie` ve diğerlerini import ettiğinizde, bunlar aslında özel sınıflar döndüren fonksiyonlardır, bunu unutmayın. /// /// info | Bilgi -Çerez tanımlamak için `Cookie` sınıfını kullanmanız gerekmektedir, aksi taktirde parametreler sorgu parametreleri olarak yorumlanır. +Çerezleri tanımlamak için `Cookie` kullanmanız gerekir, aksi halde parametreler query parametreleri olarak yorumlanır. /// -## Özet +/// info | Bilgi -Çerez tanımlamalarını `Cookie` sınıfını kullanarak `Query` ve `Path` tanımlar gibi tanımlayın. +**Tarayıcılar çerezleri** özel şekillerde ve arka planda işlediği için, **JavaScript**'in onlara dokunmasına kolayca izin **vermezler**. + +`/docs` adresindeki **API docs UI**'a giderseniz, *path operation*'larınız için çerezlerin **dokümantasyonunu** görebilirsiniz. + +Ancak **veriyi doldurup** "Execute" düğmesine tıklasanız bile, docs UI **JavaScript** ile çalıştığı için çerezler gönderilmez ve herhangi bir değer yazmamışsınız gibi bir **hata** mesajı görürsünüz. + +/// + +## Özet { #recap } + +`Query` ve `Path` ile aynı ortak deseni kullanarak, çerezleri `Cookie` ile tanımlayın. diff --git a/docs/tr/docs/tutorial/first-steps.md b/docs/tr/docs/tutorial/first-steps.md index 9a8ef762d..332f5c559 100644 --- a/docs/tr/docs/tutorial/first-steps.md +++ b/docs/tr/docs/tutorial/first-steps.md @@ -1,102 +1,118 @@ -# İlk Adımlar +# İlk Adımlar { #first-steps } En sade FastAPI dosyası şu şekilde görünür: -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} -Yukarıdaki içeriği bir `main.py` dosyasına kopyalayalım. +Yukarıdakini `main.py` adlı bir dosyaya kopyalayın. -Uygulamayı çalıştıralım: +Canlı sunucuyu çalıştırın: <div class="termy"> ```console -$ uvicorn main:app --reload +$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:solid">main.py</u> -<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -<span style="color: green;">INFO</span>: Started reloader process [28720] -<span style="color: green;">INFO</span>: Started server process [28722] -<span style="color: green;">INFO</span>: Waiting for application startup. -<span style="color: green;">INFO</span>: Application startup complete. + <span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting development server 🚀 + + Searching for package file structure from directories + with <font color="#3465A4">__init__.py</font> files + Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py + + <span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with + the following code: + + <u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> + + <span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000/docs</u></font> + + <span style="background-color:#007166"><font color="#D3D7CF"> tip </font></span> Running in development mode, for production use: + <b>fastapi run</b> + + Logs: + + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Will watch for changes in these directories: + <b>[</b><font color="#4E9A06">'/home/user/code/awesomeapp'</font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> <b>(</b>Press CTRL+C + to quit<b>)</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started reloader process <b>[</b><font color="#34E2E2"><b>383138</b></font><b>]</b> using WatchFiles + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>383153</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. ``` </div> -/// note | Not - -`uvicorn main:app` komutunu şu şekilde açıklayabiliriz: - -* `main`: dosya olan `main.py` (yani Python "modülü"). -* `app`: ise `main.py` dosyasının içerisinde `app = FastAPI()` satırında oluşturduğumuz `FastAPI` nesnesi. -* `--reload`: kod değişikliklerinin ardından sunucuyu otomatik olarak yeniden başlatır. Bu parameteyi sadece geliştirme aşamasında kullanmalıyız. - -/// - -Çıktı olarak şöyle bir satır ile karşılaşacaksınız: +Çıktıda, şuna benzer bir satır göreceksiniz: ```hl_lines="4" INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` -Bu satır, yerel makinenizde uygulamanızın çalıştığı bağlantıyı gösterir. +Bu satır, uygulamanızın yerel makinenizde hangi URL'de sunulduğunu gösterir. -### Kontrol Edelim +### Kontrol Edelim { #check-it } -Tarayıcınızı açıp <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a> bağlantısına gidin. +Tarayıcınızı açıp <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a> adresine gidin. -Şu şekilde bir JSON yanıtı ile karşılaşacağız: +Şu şekilde bir JSON response göreceksiniz: ```JSON {"message": "Hello World"} ``` -### Etkileşimli API Dokümantasyonu +### Etkileşimli API Dokümantasyonu { #interactive-api-docs } -Şimdi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> bağlantısını açalım. +Şimdi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> adresine gidin. -<a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> tarafından sağlanan otomatik etkileşimli bir API dokümantasyonu göreceğiz: +Otomatik etkileşimli API dokümantasyonunu ( <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> tarafından sağlanan) göreceksiniz: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternatif API Dokümantasyonu +### Alternatif API Dokümantasyonu { #alternative-api-docs } -Şimdi <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> bağlantısını açalım. +Ve şimdi <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> adresine gidin. -<a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> tarafından sağlanan otomatik dokümantasyonu göreceğiz: +Alternatif otomatik dokümantasyonu ( <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> tarafından sağlanan) göreceksiniz: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -### OpenAPI +### OpenAPI { #openapi } -**FastAPI**, **OpenAPI** standardını kullanarak tüm API'ınızın tamamını tanımlayan bir "şema" oluşturur. +**FastAPI**, API'ları tanımlamak için **OpenAPI** standardını kullanarak tüm API'nızın tamamını içeren bir "şema" üretir. -#### "Şema" +#### "Şema" { #schema } -"Şema", bir şeyin tanımı veya açıklamasıdır. Geliştirilen koddan ziyade soyut bir açıklamadır. +"Şema", bir şeyin tanımı veya açıklamasıdır. Onu uygulayan kod değil, sadece soyut bir açıklamadır. -#### API "Şeması" +#### API "şeması" { #api-schema } -Bu durumda, <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a>, API şemasını nasıl tanımlayacağınızı belirten bir şartnamedir. +Bu durumda, <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a>, API'nızın şemasını nasıl tanımlayacağınızı belirleyen bir şartnamedir. -Bu şema tanımı, API yollarınızla birlikte yollarınızın aldığı olası parametreler gibi tanımlamaları içerir. +Bu şema tanımı, API path'leriniz, alabilecekleri olası parametreler vb. şeyleri içerir. -#### Veri "Şeması" +#### Veri "şeması" { #data-schema } "Şema" terimi, JSON içeriği gibi bazı verilerin şeklini de ifade edebilir. -Bu durumda, JSON özellikleri ve sahip oldukları veri türleri gibi anlamlarına gelir. +Bu durumda, JSON attribute'ları ve sahip oldukları veri türleri vb. anlamına gelir. -#### OpenAPI ve JSON Şema +#### OpenAPI ve JSON Schema { #openapi-and-json-schema } -OpenAPI, API'niz için bir API şeması tanımlar. Ve bu şema, JSON veri şemaları standardı olan **JSON Şema** kullanılarak API'niz tarafından gönderilen ve alınan verilerin tanımlarını (veya "şemalarını") içerir. +OpenAPI, API'nız için bir API şeması tanımlar. Ve bu şema, JSON veri şemaları standardı olan **JSON Schema** kullanılarak API'nız tarafından gönderilen ve alınan verilerin tanımlarını (veya "şemalarını") içerir. -#### `openapi.json` Dosyasına Göz At +#### `openapi.json` Dosyasına Göz At { #check-the-openapi-json } -Ham OpenAPI şemasının nasıl göründüğünü merak ediyorsanız, FastAPI otomatik olarak tüm API'ınızın tanımlamalarını içeren bir JSON (şeması) oluşturur. +Ham OpenAPI şemasının nasıl göründüğünü merak ediyorsanız, FastAPI otomatik olarak tüm API'nızın açıklamalarını içeren bir JSON (şema) üretir. -Bu şemayı direkt olarak <a href="http://127.0.0.1:8000/openapi.json" class="external-link" target="_blank">http://127.0.0.1:8000/openapi.json</a> bağlantısından görüntüleyebilirsiniz. +Bunu doğrudan şuradan görebilirsiniz: <a href="http://127.0.0.1:8000/openapi.json" class="external-link" target="_blank">http://127.0.0.1:8000/openapi.json</a>. -Aşağıdaki gibi başlayan bir JSON ile karşılaşacaksınız: +Şuna benzer bir şekilde başlayan bir JSON gösterecektir: ```JSON { @@ -119,79 +135,87 @@ Aşağıdaki gibi başlayan bir JSON ile karşılaşacaksınız: ... ``` -#### OpenAPI Ne İşe Yarar? +#### OpenAPI Ne İşe Yarar? { #what-is-openapi-for } -OpenAPI şeması, FastAPI projesinde bulunan iki etkileşimli dokümantasyon sistemine güç veren şeydir. +OpenAPI şeması, dahil edilen iki etkileşimli dokümantasyon sistemine güç veren şeydir. -OpenAPI'ya dayalı düzinelerce alternatif etkileşimli dokümantasyon aracı mevcuttur. **FastAPI** ile oluşturulmuş uygulamanıza bu alternatiflerden herhangi birini kolayca ekleyebilirsiniz. +Ve OpenAPI tabanlı düzinelerce alternatif vardır. **FastAPI** ile oluşturulmuş uygulamanıza bu alternatiflerden herhangi birini kolayca ekleyebilirsiniz. -Ayrıca, API'ınızla iletişim kuracak önyüz, mobil veya IoT uygulamaları gibi istemciler için otomatik olarak kod oluşturabilirsiniz. +Ayrıca, API'nızla iletişim kuran istemciler için otomatik olarak kod üretmekte de kullanabilirsiniz. Örneğin frontend, mobil veya IoT uygulamaları. -## Adım Adım Özetleyelim +### Uygulamanızı Yayınlayın (opsiyonel) { #deploy-your-app-optional } -### Adım 1: `FastAPI`yı Projemize Dahil Edelim +İsterseniz FastAPI uygulamanızı <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>'a deploy edebilirsiniz; henüz katılmadıysanız gidip bekleme listesine yazılın. 🚀 -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +Zaten bir **FastAPI Cloud** hesabınız varsa (bekleme listesinden sizi davet ettiysek 😉), uygulamanızı tek komutla deploy edebilirsiniz. -`FastAPI`, API'niz için tüm işlevselliği sağlayan bir Python sınıfıdır. +Deploy etmeden önce giriş yaptığınızdan emin olun: + +<div class="termy"> + +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +</div> + +Ardından uygulamanızı deploy edin: + +<div class="termy"> + +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +</div> + +Bu kadar! Artık uygulamanıza o URL üzerinden erişebilirsiniz. ✨ + +## Adım Adım Özetleyelim { #recap-step-by-step } + +### Adım 1: `FastAPI` import edin { #step-1-import-fastapi } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} + +`FastAPI`, API'nız için tüm işlevselliği sağlayan bir Python class'ıdır. /// note | Teknik Detaylar -`FastAPI` doğrudan `Starlette`'i miras alan bir sınıftır. +`FastAPI`, doğrudan `Starlette`'ten miras alan bir class'tır. <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a>'in tüm işlevselliğini `FastAPI` ile de kullanabilirsiniz. /// -### Adım 2: Bir `FastAPI` "Örneği" Oluşturalım +### Adım 2: bir `FastAPI` "instance"ı oluşturun { #step-2-create-a-fastapi-instance } -{* ../../docs_src/first_steps/tutorial001.py hl[3] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[3] *} -Burada `app` değişkeni `FastAPI` sınıfının bir örneği olacaktır. +Burada `app` değişkeni `FastAPI` class'ının bir "instance"ı olacaktır. -Bu, tüm API'yı oluşturmak için ana etkileşim noktası olacaktır. +Bu, tüm API'nızı oluşturmak için ana etkileşim noktası olacaktır. -Bu `app` değişkeni, `uvicorn` komutunda atıfta bulunulan değişkenin ta kendisidir. +### Adım 3: bir *path operation* oluşturun { #step-3-create-a-path-operation } -<div class="termy"> +#### Path { #path } -```console -$ uvicorn main:app --reload +Buradaki "Path", URL'in ilk `/` işaretinden başlayarak son kısmını ifade eder. -<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -</div> - -Uygulamanızı aşağıdaki gibi oluşturursanız: - -{* ../../docs_src/first_steps/tutorial002.py hl[3] *} - -Ve bunu `main.py` dosyasına yerleştirirseniz eğer `uvicorn` komutunu şu şekilde çalıştırabilirsiniz: - -<div class="termy"> - -```console -$ uvicorn main:my_awesome_api --reload - -<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -</div> - -### Adım 3: Bir *Yol Operasyonu* Oluşturalım - -#### <abbr title="Yol: Path">Yol</abbr> - -Burada "yol" bağlantıda bulunan ilk `/` ile başlayan ve sonrasında gelen kısmı ifade eder. - -Yani, şu şekilde bir bağlantıda: +Yani, şu şekilde bir URL'de: ``` https://example.com/items/foo ``` -... yol şöyle olur: +...path şöyle olur: ``` /items/foo @@ -199,77 +223,77 @@ https://example.com/items/foo /// info | Bilgi -"Yol" genellikle "<abbr title="Endpoint: Bitim Noktası">endpoint</abbr>" veya "<abbr title="Route: Yönlendirme/Yön">route</abbr>" olarak adlandırılır. +Bir "path" genellikle "endpoint" veya "route" olarak da adlandırılır. /// -Bir API oluştururken, "yol", "kaynaklar" ile "endişeleri" ayırmanın ana yöntemidir. +Bir API oluştururken, "path", "concerns" ve "resources" ayrımını yapmanın ana yoludur. -#### Operasyonlar +#### Operation { #operation } -Burada "operasyon" HTTP "metodlarından" birini ifade eder. +Burada "Operation", HTTP "method"larından birini ifade eder. -Bunlardan biri: +Şunlardan biri: * `POST` * `GET` * `PUT` * `DELETE` -...veya daha az kullanılan diğerleri: +...ve daha egzotik olanlar: * `OPTIONS` * `HEAD` * `PATCH` * `TRACE` -HTTP protokolünde, bu "metodlardan" birini (veya daha fazlasını) kullanarak her bir yol ile iletişim kurabilirsiniz. +HTTP protokolünde, her bir path ile bu "method"lardan biri (veya birden fazlası) ile iletişim kurabilirsiniz. --- -API oluştururkan, belirli bir amaca hizmet eden belirli HTTP metodlarını kullanırsınız. +API oluştururken, normalde belirli bir aksiyon için bu spesifik HTTP method'larını kullanırsınız. -Normalde kullanılan: +Normalde şunları kullanırsınız: -* `POST`: veri oluşturmak. -* `GET`: veri okumak. -* `PUT`: veriyi güncellemek. -* `DELETE`: veriyi silmek. +* `POST`: veri oluşturmak için. +* `GET`: veri okumak için. +* `PUT`: veriyi güncellemek için. +* `DELETE`: veriyi silmek için. -Bu nedenle, OpenAPI'da HTTP metodlarından her birine "operasyon" denir. +Bu nedenle, OpenAPI'da HTTP method'larının her birine "operation" denir. -Biz de onları "**operasyonlar**" olarak adlandıracağız. +Biz de bunlara "**operation**" diyeceğiz. -#### Bir *Yol Operasyonu Dekoratörü* Tanımlayalım +#### Bir *path operation decorator* tanımlayın { #define-a-path-operation-decorator } -{* ../../docs_src/first_steps/tutorial001.py hl[6] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *} -`@app.get("/")` dekoratörü, **FastAPI**'a hemen altındaki fonksiyonun aşağıdaki durumlardan sorumlu olduğunu söyler: +`@app.get("/")`, **FastAPI**'a hemen altındaki fonksiyonun şuraya giden request'leri ele almakla sorumlu olduğunu söyler: -* <abbr title="Bir HTTP GET metodu"><code>get</code> operasyonu</abbr> ile -* `/` yoluna gelen istekler +* path `/` +* <abbr title="bir HTTP GET method'u"><code>get</code> operation</abbr> kullanarak /// info | `@decorator` Bilgisi -Python'da `@something` sözdizimi "<abbr title="Decorator">dekoratör</abbr>" olarak adlandırılır. +Python'daki `@something` söz dizimi "decorator" olarak adlandırılır. -Dekoratörler, dekoratif bir şapka gibi (sanırım terim buradan geliyor) fonksiyonların üzerlerine yerleştirilirler. +Onu bir fonksiyonun üstüne koyarsınız. Güzel, dekoratif bir şapka gibi (sanırım terim de buradan geliyor). -Bir "dekoratör" hemen altında bulunan fonksiyonu alır ve o fonksiyon ile bazı işlemler gerçekleştirir. +Bir "decorator", altındaki fonksiyonu alır ve onunla bir şey yapar. -Bizim durumumuzda, kullandığımız dekoratör, **FastAPI**'a altındaki fonksiyonun `/` yoluna gelen `get` metodlu isteklerden sorumlu olduğunu söyler. +Bizim durumumuzda bu decorator, **FastAPI**'a altındaki fonksiyonun **path** `/` ile **operation** `get`'e karşılık geldiğini söyler. -Bu bir **yol operasyonu dekoratörüdür**. +Bu, "**path operation decorator**"dır. /// -Ayrıca diğer operasyonları da kullanabilirsiniz: +Diğer operation'ları da kullanabilirsiniz: * `@app.post()` * `@app.put()` * `@app.delete()` -Daha az kullanılanları da kullanabilirsiniz: +Ve daha egzotik olanları: * `@app.options()` * `@app.head()` @@ -278,58 +302,79 @@ Daha az kullanılanları da kullanabilirsiniz: /// tip | İpucu -Her işlemi (HTTP metod) istediğiniz gibi kullanmakta özgürsünüz. +Her bir operation'ı (HTTP method'unu) istediğiniz gibi kullanmakta özgürsünüz. -**FastAPI** herhangi bir özel amacı veya anlamı olması konusunda ısrarcı olmaz. +**FastAPI** herhangi bir özel anlamı zorunlu kılmaz. Buradaki bilgiler bir gereklilik değil, bir kılavuz olarak sunulmaktadır. -Mesela GraphQL kullanırkan genelde tüm işlemleri yalnızca `POST` operasyonunu kullanarak gerçekleştirirsiniz. +Örneğin GraphQL kullanırken, normalde tüm aksiyonları yalnızca `POST` operation'ları kullanarak gerçekleştirirsiniz. /// -### Adım 4: **Yol Operasyonu Fonksiyonunu** Tanımlayın +### Adım 4: **path operation function**'ı tanımlayın { #step-4-define-the-path-operation-function } -Aşağıdaki, bizim **yol operasyonu fonksiyonumuzdur**: +Bu bizim "**path operation function**"ımız: -* **yol**: `/` -* **operasyon**: `get` -* **fonksiyon**: "dekoratör"ün (`@app.get("/")`'in) altındaki fonksiyondur. +* **path**: `/`. +* **operation**: `get`. +* **function**: "decorator"ün altındaki fonksiyondur (`@app.get("/")`'in altındaki). -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} Bu bir Python fonksiyonudur. -Bu fonksiyon bir `GET` işlemi kullanılarak "`/`" bağlantısına bir istek geldiğinde **FastAPI** tarafından çağrılır. +**FastAPI**, "`/`" URL'ine `GET` operation kullanarak bir request aldığında bu fonksiyonu çağıracaktır. -Bu durumda bu fonksiyon bir `async` fonksiyondur. +Bu durumda, bu bir `async` fonksiyondur. --- -Bu fonksiyonu `async def` yerine normal bir fonksiyon olarak da tanımlayabilirsiniz. +Bunu `async def` yerine normal bir fonksiyon olarak da tanımlayabilirsiniz: -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note | Not -Eğer farkı bilmiyorsanız, [Async: *"Aceleniz mi var?"*](../async.md#in-a-hurry){.internal-link target=_blank} sayfasını kontrol edebilirsiniz. +Eğer farkı bilmiyorsanız, [Async: *"Aceleniz mi var?"*](../async.md#in-a-hurry){.internal-link target=_blank} sayfasına bakın. /// -### Adım 5: İçeriği Geri Döndürün +### Adım 5: içeriği döndürün { #step-5-return-the-content } -{* ../../docs_src/first_steps/tutorial001.py hl[8] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *} -Bir `dict`, `list` veya `str`, `int` gibi tekil değerler döndürebilirsiniz. +Bir `dict`, `list`, `str`, `int` vb. tekil değerler döndürebilirsiniz. -Ayrıca, Pydantic modelleri de döndürebilirsiniz (bu konu ileriki aşamalarda irdelenecektir). +Ayrıca Pydantic modelleri de döndürebilirsiniz (bununla ilgili daha fazlasını ileride göreceksiniz). -Otomatik olarak JSON'a dönüştürülecek (ORM'ler vb. dahil) başka birçok nesne ve model vardır. En beğendiklerinizi kullanmayı deneyin, yüksek ihtimalle destekleniyordur. +Otomatik olarak JSON'a dönüştürülecek (ORM'ler vb. dahil) başka birçok nesne ve model vardır. En sevdiğiniz nesne/model'leri kullanmayı deneyin; büyük ihtimalle zaten destekleniyordur. -## Özet +### Adım 6: Deploy edin { #step-6-deploy-it } -* `FastAPI`'yı projemize dahil ettik. -* Bir `app` örneği oluşturduk. -* Bir **yol operasyonu dekoratörü** (`@app.get("/")` gibi) yazdık. -* Bir **yol operasyonu fonksiyonu** (`def root(): ...` gibi) yazdık. -* Geliştirme sunucumuzu (`uvicorn main:app --reload` gibi) çalıştırdık. +Uygulamanızı tek komutla **<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>**'a deploy edin: `fastapi deploy`. 🎉 + +#### FastAPI Cloud Hakkında { #about-fastapi-cloud } + +**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>**, **FastAPI**'ın arkasındaki aynı yazar ve ekip tarafından geliştirilmiştir. + +Minimum eforla bir API'ı **oluşturma**, **deploy etme** ve **erişme** sürecini sadeleştirir. + +FastAPI ile uygulama geliştirirken yaşadığınız aynı **developer experience**'ı, onları buluta **deploy etme** aşamasına da taşır. 🎉 + +FastAPI Cloud, *FastAPI and friends* açık kaynak projelerinin birincil sponsoru ve finansman sağlayıcısıdır. ✨ + +#### Diğer cloud sağlayıcılarına deploy edin { #deploy-to-other-cloud-providers } + +FastAPI açık kaynaklıdır ve standartlara dayanır. FastAPI uygulamalarını seçtiğiniz herhangi bir cloud sağlayıcısına deploy edebilirsiniz. + +FastAPI uygulamalarını onlarla deploy etmek için cloud sağlayıcınızın kılavuzlarını takip edin. 🤓 + +## Özet { #recap } + +* `FastAPI` import edin. +* Bir `app` instance'ı oluşturun. +* `@app.get("/")` gibi decorator'ları kullanarak bir **path operation decorator** yazın. +* Bir **path operation function** tanımlayın; örneğin `def root(): ...`. +* `fastapi dev` komutunu kullanarak geliştirme sunucusunu çalıştırın. +* İsterseniz `fastapi deploy` ile uygulamanızı deploy edin. diff --git a/docs/tr/docs/tutorial/path-params.md b/docs/tr/docs/tutorial/path-params.md index 408dd25ca..db676f1ee 100644 --- a/docs/tr/docs/tutorial/path-params.md +++ b/docs/tr/docs/tutorial/path-params.md @@ -1,34 +1,34 @@ -# Yol Parametreleri +# Yol Parametreleri { #path-parameters } -Yol "parametrelerini" veya "değişkenlerini" Python <abbr title="String Biçimleme: Format String">string biçimlemede</abbr> kullanılan sözdizimi ile tanımlayabilirsiniz. +Python <abbr title="String Biçimleme: Format String">string biçimlemede</abbr> kullanılan sözdizimiyle path "parametreleri"ni veya "değişkenleri"ni tanımlayabilirsiniz: -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} -Yol parametresi olan `item_id`'nin değeri, fonksiyonunuza `item_id` argümanı olarak aktarılacaktır. +Path parametresi `item_id`'nin değeri, fonksiyonunuza `item_id` argümanı olarak aktarılacaktır. -Eğer bu örneği çalıştırıp <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a> sayfasına giderseniz, şöyle bir çıktı ile karşılaşırsınız: +Yani, bu örneği çalıştırıp <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a> adresine giderseniz, şöyle bir response görürsünüz: ```JSON {"item_id":"foo"} ``` -## Tip İçeren Yol Parametreleri +## Tip İçeren Yol Parametreleri { #path-parameters-with-types } -Standart Python tip belirteçlerini kullanarak yol parametresinin tipini fonksiyonun içerisinde tanımlayabilirsiniz. +Standart Python tip belirteçlerini kullanarak path parametresinin tipini fonksiyonun içinde tanımlayabilirsiniz: -{* ../../docs_src/path_params/tutorial002.py hl[7] *} +{* ../../docs_src/path_params/tutorial002_py39.py hl[7] *} -Bu durumda, `item_id` bir `int` olarak tanımlanacaktır. +Bu durumda, `item_id` bir `int` olarak tanımlanır. /// check | Ek bilgi -Bu sayede, fonksiyon içerisinde hata denetimi, kod tamamlama gibi konularda editör desteğine kavuşacaksınız. +Bu sayede, fonksiyon içinde hata denetimi, kod tamamlama vb. konularda editör desteğine kavuşursunuz. /// -## Veri <abbr title="Dönüşüm: serialization, parsing ve marshalling olarak da biliniyor">Dönüşümü</abbr> +## Veri <abbr title="also known as: endpoints, routes">conversion</abbr> { #data-conversion } -Eğer bu örneği çalıştırıp tarayıcınızda <a href="http://127.0.0.1:8000/items/3" class="external-link" target="_blank">http://127.0.0.1:8000/items/3</a> sayfasını açarsanız, şöyle bir yanıt ile karşılaşırsınız: +Bu örneği çalıştırıp tarayıcınızda <a href="http://127.0.0.1:8000/items/3" class="external-link" target="_blank">http://127.0.0.1:8000/items/3</a> adresini açarsanız, şöyle bir response görürsünüz: ```JSON {"item_id":3} @@ -36,15 +36,15 @@ Eğer bu örneği çalıştırıp tarayıcınızda <a href="http://127.0.0.1:800 /// check | Ek bilgi -Dikkatinizi çekerim ki, fonksiyonunuzun aldığı (ve döndürdüğü) değer olan `3` bir string `"3"` değil aksine bir Python `int`'idir. +Dikkat edin: fonksiyonunuzun aldığı (ve döndürdüğü) değer olan `3`, string `"3"` değil, bir Python `int`'idir. -Bu tanımlamayla birlikte, **FastAPI** size otomatik istek <abbr title="HTTP isteği ile birlikte gelen string'i Python verisine dönüştürme">"ayrıştırma"</abbr> özelliği sağlar. +Yani, bu tip tanımıyla birlikte **FastAPI** size otomatik request <abbr title="HTTP isteği ile birlikte gelen string'i Python verisine dönüştürme">"parsing"</abbr> sağlar. /// -## Veri Doğrulama +## Veri Doğrulama { #data-validation } -Eğer tarayıcınızda <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a> sayfasını açarsanız, şuna benzer güzel bir HTTP hatası ile karşılaşırsınız: +Ancak tarayıcınızda <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a> adresine giderseniz, şuna benzer güzel bir HTTP hatası görürsünüz: ```JSON { @@ -62,141 +62,135 @@ Eğer tarayıcınızda <a href="http://127.0.0.1:8000/items/foo" class="external } ``` -Çünkü burada `item_id` yol parametresi `int` tipinde bir değer beklerken `"foo"` yani `string` tipinde bir değer almıştı. +çünkü path parametresi `item_id`, `int` olmayan `"foo"` değerine sahipti. -Aynı hata <a href="http://127.0.0.1:8000/items/4.2" class="external-link" target="_blank">http://127.0.0.1:8000/items/4.2</a> sayfasında olduğu gibi `int` yerine `float` bir değer verseydik de ortaya çıkardı. +Aynı hata, şu örnekte olduğu gibi `int` yerine `float` verirseniz de ortaya çıkar: <a href="http://127.0.0.1:8000/items/4.2" class="external-link" target="_blank">http://127.0.0.1:8000/items/4.2</a> /// check | Ek bilgi -Böylece, aynı Python tip tanımlaması ile birlikte, **FastAPI** veri doğrulama özelliği sağlar. +Yani, aynı Python tip tanımıyla birlikte **FastAPI** size veri doğrulama sağlar. -Dikkatinizi çekerim ki, karşılaştığınız hata, doğrulamanın geçersiz olduğu mutlak noktayı da açık bir şekilde belirtiyor. +Dikkat edin: hata ayrıca doğrulamanın geçmediği noktayı da açıkça belirtir. -Bu özellik, API'ınızla iletişime geçen kodu geliştirirken ve ayıklarken inanılmaz derecede yararlı olacaktır. +Bu, API'ınızla etkileşime giren kodu geliştirirken ve debug ederken inanılmaz derecede faydalıdır. /// -## Dokümantasyon +## Dokümantasyon { #documentation } -Ayrıca, tarayıcınızı <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> adresinde açarsanız, aşağıdaki gibi otomatik ve interaktif bir API dökümantasyonu ile karşılaşırsınız: +Tarayıcınızı <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> adresinde açtığınızda, aşağıdaki gibi otomatik ve interaktif bir API dokümantasyonu görürsünüz: <img src="/img/tutorial/path-params/image01.png"> /// check | Ek bilgi -Üstelik, sadece aynı Python tip tanımlaması ile, **FastAPI** size otomatik ve interaktif (Swagger UI ile entegre) bir dokümantasyon sağlar. +Yine, sadece aynı Python tip tanımıyla **FastAPI** size otomatik ve interaktif dokümantasyon (Swagger UI entegrasyonuyla) sağlar. -Dikkatinizi çekerim ki, yol parametresi integer olarak tanımlanmıştır. +Dikkat edin: path parametresi integer olarak tanımlanmıştır. /// -## Standartlara Dayalı Avantajlar, Alternatif Dokümantasyon +## Standartlara Dayalı Avantajlar, Alternatif Dokümantasyon { #standards-based-benefits-alternative-documentation } -Oluşturulan şema <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md" class="external-link" target="_blank">OpenAPI</a> standardına uygun olduğu için birçok uyumlu araç mevcuttur. +Üretilen şema <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md" class="external-link" target="_blank">OpenAPI</a> standardından geldiği için birçok uyumlu araç vardır. -Bu sayede, **FastAPI**'ın bizzat kendisi <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> sayfasından erişebileceğiniz alternatif (ReDoc kullanan) bir API dokümantasyonu sağlar: +Bu nedenle **FastAPI**'ın kendisi, <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> adresinden erişebileceğiniz alternatif bir API dokümantasyonu (ReDoc kullanarak) sağlar: <img src="/img/tutorial/path-params/image02.png"> -Aynı şekilde, farklı diller için kod türetme araçları da dahil olmak üzere çok sayıda uyumlu araç bulunur. +Aynı şekilde, birçok uyumlu araç vardır. Birçok dil için kod üretme araçları da buna dahildir. -## Pydantic +## Pydantic { #pydantic } -Tüm veri doğrulamaları <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> tarafından arka planda gerçekleştirilir, bu sayede tüm avantajlardan faydalanabilirsiniz. Böylece, emin ellerde olduğunuzu hissedebilirsiniz. +Tüm veri doğrulamaları, arka planda <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> tarafından gerçekleştirilir; böylece onun tüm avantajlarından faydalanırsınız. Ve emin ellerde olduğunuzu bilirsiniz. -Aynı tip tanımlamalarını `str`, `float`, `bool` ve diğer karmaşık veri tipleri ile kullanma imkanınız vardır. +Aynı tip tanımlarını `str`, `float`, `bool` ve daha birçok karmaşık veri tipiyle kullanabilirsiniz. -Bunlardan birkaçı, bu eğitimin ileriki bölümlerinde irdelenmiştir. +Bunların birkaçı, eğitimin sonraki bölümlerinde ele alınacaktır. -## Sıralama Önem Arz Eder +## Sıralama Önemlidir { #order-matters } -*Yol operasyonları* tasarlarken sabit yol barındıran durumlar ile karşılaşabilirsiniz. +*Path operation*'lar oluştururken sabit bir path'e sahip olduğunuz durumlarla karşılaşabilirsiniz. -Farz edelim ki `/users/me` yolu geçerli kullanıcı hakkında bilgi almak için kullanılıyor olsun. +Örneğin `/users/me`'nin, geçerli kullanıcı hakkında veri almak için kullanıldığını varsayalım. -Benzer şekilde `/users/{user_id}` gibi tanımlanmış ve belirli bir kullanıcı hakkında veri almak için kullanıcının ID bilgisini kullanan bir yolunuz da mevcut olabilir. +Sonra belirli bir kullanıcı hakkında, kullanıcı ID'si ile veri almak için `/users/{user_id}` şeklinde bir path'iniz de olabilir. -*Yol operasyonları* sıralı bir şekilde gözden geçirildiğinden dolayı `/users/me` yolunun `/users/{user_id}` yolundan önce tanımlanmış olmasından emin olmanız gerekmektedir: +*Path operation*'lar sırayla değerlendirildiği için, `/users/me` için olan path'in `/users/{user_id}` olandan önce tanımlandığından emin olmanız gerekir: -{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} +{* ../../docs_src/path_params/tutorial003_py39.py hl[6,11] *} -Aksi halde, `/users/{user_id}` yolu `"me"` değerinin `user_id` parametresi için gönderildiğini "düşünerek" `/users/me` ile de eşleşir. +Aksi halde, `/users/{user_id}` için olan path, `"me"` değerini `user_id` parametresi olarak aldığını "düşünerek" `/users/me` için de eşleşir. -Benzer şekilde, bir yol operasyonunu yeniden tanımlamanız mümkün değildir: +Benzer şekilde, bir path operation'ı yeniden tanımlayamazsınız: -{* ../../docs_src/path_params/tutorial003b.py hl[6,11] *} +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} -Yol, ilk kısım ile eşleştiğinden dolayı her koşulda ilk yol operasyonu kullanılacaktır. +Path önce eşleştiği için her zaman ilk olan kullanılır. -## Ön Tanımlı Değerler +## Ön Tanımlı Değerler { #predefined-values } -Eğer *yol parametresi* alan bir *yol operasyonunuz* varsa ve alabileceği *yol parametresi* değerlerinin ön tanımlı olmasını istiyorsanız, standart Python <abbr title="Enumeration">`Enum`</abbr> tipini kullanabilirsiniz. +Bir *path operation*'ınız *path parameter* alıyorsa ama olası geçerli *path parameter* değerlerinin önceden tanımlı olmasını istiyorsanız, standart bir Python <abbr title="Enumeration">`Enum`</abbr> kullanabilirsiniz. -### Bir `Enum` Sınıfı Oluşturalım +### Bir `Enum` Sınıfı Oluşturalım { #create-an-enum-class } -`Enum` sınıfını projemize dahil edip `str` ile `Enum` sınıflarını miras alan bir alt sınıf yaratalım. +`Enum`'u import edin ve `str` ile `Enum`'dan miras alan bir alt sınıf oluşturun. -`str` sınıfı miras alındığından dolayı, API dokümanı, değerlerin `string` tipinde olması gerektiğini anlayabilecek ve doğru bir şekilde işlenecektir. +`str`'den miras aldığınızda API dokümanları değerlerin `string` tipinde olması gerektiğini anlayabilir ve doğru şekilde render edebilir. -Sonrasında, sınıf içerisinde, mevcut ve geçerli değerler olacak olan sabit değerli özelliklerini oluşturalım: +Sonra, kullanılabilir geçerli değerler olacak sabit değerli class attribute'ları oluşturun: -{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} - -/// info | Bilgi - -3.4 sürümünden beri <a href="https://docs.python.org/3/library/enum.html" class="external-link" target="_blank">enumerationlar (ya da enumlar) Python'da mevcuttur</a>. - -/// +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | İpucu -Merak ediyorsanız söyleyeyim, "AlexNet", "ResNet" ve "LeNet" isimleri Makine Öğrenmesi <abbr title="Teknik olarak, Derin Öğrenme model mimarileri">modellerini</abbr> temsil eder. +Merak ediyorsanız: "AlexNet", "ResNet" ve "LeNet", Makine Öğrenmesi <abbr title="Technically, Deep Learning model architectures">modelleri</abbr>nin sadece isimleridir. /// -### Bir *Yol Parametresi* Tanımlayalım +### Bir *Path Parameter* Tanımlayalım { #declare-a-path-parameter } -Sonrasında, yarattığımız enum sınıfını (`ModelName`) kullanarak tip belirteci aracılığıyla bir *yol parametresi* oluşturalım: +Ardından oluşturduğunuz enum sınıfını (`ModelName`) kullanarak tip belirteciyle bir *path parameter* oluşturun: -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} -### Dokümana Göz Atalım +### Dokümana Göz Atalım { #check-the-docs } -*Yol parametresi* için mevcut değerler ön tanımlı olduğundan dolayı, interaktif döküman onları güzel bir şekilde gösterebilir: +*Path parameter* için kullanılabilir değerler ön tanımlı olduğu için, interaktif dokümanlar bunları güzelce gösterebilir: <img src="/img/tutorial/path-params/image03.png"> -### Python *Enumerationları* ile Çalışmak +### Python *Enumeration*'ları ile Çalışmak { #working-with-python-enumerations } -*Yol parametresinin* değeri bir *enumeration üyesi* olacaktır. +*Path parameter*'ın değeri bir *enumeration member* olacaktır. -#### *Enumeration Üyelerini* Karşılaştıralım +#### *Enumeration Member*'ları Karşılaştıralım { #compare-enumeration-members } -Parametreyi, yarattığınız enum olan `ModelName` içerisindeki *enumeration üyesi* ile karşılaştırabilirsiniz: +Bunu, oluşturduğunuz enum `ModelName` içindeki *enumeration member* ile karşılaştırabilirsiniz: -{* ../../docs_src/path_params/tutorial005.py hl[17] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *} -#### *Enumeration Değerini* Edinelim +#### *Enumeration Value*'yu Alalım { #get-the-enumeration-value } -`model_name.value` veya genel olarak `your_enum_member.value` tanımlarını kullanarak (bu durumda bir `str` olan) gerçek değere ulaşabilirsiniz: +Gerçek değeri (bu durumda bir `str`) `model_name.value` ile veya genel olarak `your_enum_member.value` ile alabilirsiniz: -{* ../../docs_src/path_params/tutorial005.py hl[20] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *} /// tip | İpucu -`"lenet"` değerine `ModelName.lenet.value` tanımı ile de ulaşabilirsiniz. +`"lenet"` değerine `ModelName.lenet.value` ile de erişebilirsiniz. /// -#### *Enumeration Üyelerini* Döndürelim +#### *Enumeration Member*'ları Döndürelim { #return-enumeration-members } -JSON gövdesine (örneğin bir `dict`) gömülü olsalar bile *yol operasyonundaki* *enum üyelerini* döndürebilirsiniz. +*Path operation*'ınızdan, bir JSON body'nin içine gömülü olsalar bile (ör. bir `dict`) *enum member*'ları döndürebilirsiniz. -Bu üyeler istemciye iletilmeden önce kendilerine karşılık gelen değerlerine (bu durumda string) dönüştürüleceklerdir: +İstemciye dönmeden önce, karşılık gelen değerlerine (bu durumda string) dönüştürülürler: -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} -İstemci tarafında şuna benzer bir JSON yanıtı ile karşılaşırsınız: +İstemcinizde şöyle bir JSON response alırsınız: ```JSON { @@ -205,53 +199,53 @@ Bu üyeler istemciye iletilmeden önce kendilerine karşılık gelen değerlerin } ``` -## Yol İçeren Yol Parametreleri +## Path İçeren Path Parametreleri { #path-parameters-containing-paths } -Farz edelim ki elinizde `/files/{file_path}` isminde bir *yol operasyonu* var. +Diyelim ki `/files/{file_path}` path'ine sahip bir *path operation*'ınız var. -Fakat `file_path` değerinin `home/johndoe/myfile.txt` gibi bir *yol* barındırmasını istiyorsunuz. +Ama `file_path`'in kendisinin `home/johndoe/myfile.txt` gibi bir *path* içermesi gerekiyor. -Sonuç olarak, oluşturmak istediğin URL `/files/home/johndoe/myfile.txt` gibi bir şey olacaktır. +Böylece, o dosyanın URL'si şu şekilde olur: `/files/home/johndoe/myfile.txt`. -### OpenAPI Desteği +### OpenAPI Desteği { #openapi-support } -Test etmesi ve tanımlaması zor senaryolara sebebiyet vereceğinden dolayı OpenAPI, *yol* barındıran *yol parametrelerini* tanımlayacak bir çözüm sunmuyor. +OpenAPI, içinde bir *path* barındıracak bir *path parameter* tanımlamak için bir yöntem desteklemez; çünkü bu, test etmesi ve tanımlaması zor senaryolara yol açabilir. -Ancak bunu, Starlette kütüphanesinin dahili araçlarından birini kullanarak **FastAPI**'da gerçekleştirebilirsiniz. +Yine de, Starlette'in dahili araçlarından birini kullanarak bunu **FastAPI**'da yapabilirsiniz. -Parametrenin bir yol içermesi gerektiğini belirten herhangi bir doküman eklemememize rağmen dokümanlar yine de çalışacaktır. +Ve dokümanlar, parametrenin bir path içermesi gerektiğini söyleyen herhangi bir dokümantasyon eklemese bile çalışmaya devam eder. -### Yol Dönüştürücü +### Path Dönüştürücü { #path-convertor } -Direkt olarak Starlette kütüphanesinden gelen bir opsiyon sayesinde aşağıdaki gibi *yol* içeren bir *yol parametresi* bağlantısı tanımlayabilirsiniz: +Starlette'ten doğrudan gelen bir seçenekle, *path* içeren bir *path parameter*'ı şu URL ile tanımlayabilirsiniz: ``` /files/{file_path:path} ``` -Bu durumda, parametrenin adı `file_path` olacaktır ve son kısım olan `:path` kısmı, parametrenin herhangi bir *yol* ile eşleşmesi gerektiğini belirtecektir. +Bu durumda parametrenin adı `file_path`'tir ve son kısım olan `:path`, parametrenin herhangi bir *path* ile eşleşmesi gerektiğini söyler. -Böylece şunun gibi bir kullanım yapabilirsiniz: +Yani şununla kullanabilirsiniz: -{* ../../docs_src/path_params/tutorial004.py hl[6] *} +{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *} /// tip | İpucu -Parametrenin başında `/home/johndoe/myfile.txt` yolunda olduğu gibi (`/`) işareti ile birlikte kullanmanız gerektiği durumlar olabilir. +Parametrenin başında `/home/johndoe/myfile.txt` örneğinde olduğu gibi bir eğik çizgi (`/`) ile başlaması gerekebilir. -Bu durumda, URL, `files` ile `home` arasında iki eğik çizgiye (`//`) sahip olup `/files//home/johndoe/myfile.txt` gibi gözükecektir. +Bu durumda URL, `files` ile `home` arasında çift eğik çizgi (`//`) olacak şekilde `/files//home/johndoe/myfile.txt` olur. /// -## Özet +## Özet { #recap } -**FastAPI** ile kısa, sezgisel ve standart Python tip tanımlamaları kullanarak şunları elde edersiniz: +**FastAPI** ile kısa, sezgisel ve standart Python tip tanımlarını kullanarak şunları elde edersiniz: -* Editör desteği: hata denetimi, otomatik tamamlama, vb. -* Veri "<abbr title="HTTP isteği ile birlikte gelen string'i Python verisine dönüştürme">dönüştürme</abbr>" +* Editör desteği: hata denetimleri, otomatik tamamlama vb. +* Veri "<abbr title="HTTP isteği ile birlikte gelen string'i Python verisine dönüştürme">parsing</abbr>" * Veri doğrulama -* API tanımlamaları ve otomatik dokümantasyon +* API annotation ve otomatik dokümantasyon -Ve sadece, bunları bir kez tanımlamanız yeterli. +Ve bunları sadece bir kez tanımlamanız yeterlidir. -Diğer frameworkler ile karşılaştırıldığında (ham performans dışında), üstte anlatılan durum muhtemelen **FastAPI**'ın göze çarpan başlıca avantajıdır. +Bu, (ham performans dışında) **FastAPI**'ın alternatif framework'lere kıyasla muhtemelen en görünür ana avantajıdır. diff --git a/docs/tr/docs/tutorial/query-params.md b/docs/tr/docs/tutorial/query-params.md index a8ba883ed..89cfa3fb3 100644 --- a/docs/tr/docs/tutorial/query-params.md +++ b/docs/tr/docs/tutorial/query-params.md @@ -1,83 +1,83 @@ -# Sorgu Parametreleri +# Sorgu Parametreleri { #query-parameters } -Fonksiyonda yol parametrelerinin parçası olmayan diğer tanımlamalar otomatik olarak "sorgu" parametresi olarak yorumlanır. +Fonksiyonda path parametrelerinin parçası olmayan diğer parametreleri tanımladığınızda, bunlar otomatik olarak "query" parametreleri olarak yorumlanır. -{* ../../docs_src/query_params/tutorial001.py hl[9] *} +{* ../../docs_src/query_params/tutorial001_py39.py hl[9] *} -Sorgu, bağlantıdaki `?` kısmından sonra gelen ve `&` işareti ile ayrılan anahtar-değer çiftlerinin oluşturduğu bir kümedir. +Query, bir URL'de `?` işaretinden sonra gelen ve `&` karakterleriyle ayrılan anahtar-değer çiftlerinin kümesidir. -Örneğin, aşağıdaki bağlantıda: +Örneğin, şu URL'de: ``` http://127.0.0.1:8000/items/?skip=0&limit=10 ``` -...sorgu parametreleri şunlardır: +...query parametreleri şunlardır: -* `skip`: değeri `0`'dır -* `limit`: değeri `10`'dır +* `skip`: değeri `0` +* `limit`: değeri `10` -Parametreler bağlantının bir parçası oldukları için doğal olarak string olarak değerlendirilirler. +URL'nin bir parçası oldukları için "doğal olarak" string'tirler. -Fakat, Python tipleri ile tanımlandıkları zaman (yukarıdaki örnekte `int` oldukları gibi), parametreler o tiplere dönüştürülür ve o tipler çerçevesinde doğrulanırlar. +Ancak, bunları Python tipleriyle (yukarıdaki örnekte `int` olarak) tanımladığınızda, o tipe dönüştürülürler ve o tipe göre doğrulanırlar. -Yol parametreleri için geçerli olan her türlü işlem aynı şekilde sorgu parametreleri için de geçerlidir: +Path parametreleri için geçerli olan aynı süreç query parametreleri için de geçerlidir: -* Editör desteği (şüphesiz) -* Veri "<abbr title="HTTP isteği ile birlikte gelen string'i Python verisine dönüştürme">ayrıştırma</abbr>" +* Editör desteği (tabii ki) +* Veri <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr> * Veri doğrulama * Otomatik dokümantasyon -## Varsayılanlar +## Varsayılanlar { #defaults } -Sorgu parametreleri, adres yolunun sabit bir parçası olmadıklarından dolayı isteğe bağlı ve varsayılan değere sahip olabilirler. +Query parametreleri path'in sabit bir parçası olmadığından, opsiyonel olabilir ve varsayılan değerlere sahip olabilir. -Yukarıdaki örnekte `skip=0` ve `limit=10` varsayılan değere sahiplerdir. +Yukarıdaki örnekte varsayılan değerleri `skip=0` ve `limit=10`'dur. -Yani, aşağıdaki bağlantıya gitmek: +Yani şu URL'ye gitmek: ``` http://127.0.0.1:8000/items/ ``` -şu adrese gitmek ile aynı etkiye sahiptir: +şuraya gitmekle aynı olur: ``` http://127.0.0.1:8000/items/?skip=0&limit=10 ``` -Ancak, mesela şöyle bir adresi ziyaret ederseniz: +Ancak örneğin şuraya giderseniz: ``` http://127.0.0.1:8000/items/?skip=20 ``` -Fonksiyonunuzdaki parametre değerleri aşağıdaki gibi olacaktır: +Fonksiyonunuzdaki parametre değerleri şöyle olacaktır: -* `skip=20`: çünkü bağlantıda böyle tanımlandı. -* `limit=10`: çünkü varsayılan değer buydu. +* `skip=20`: çünkü URL'de siz ayarladınız +* `limit=10`: çünkü varsayılan değer oydu -## İsteğe Bağlı Parametreler +## İsteğe bağlı parametreler { #optional-parameters } -Aynı şekilde, varsayılan değerlerini `None` olarak atayarak isteğe bağlı parametreler tanımlayabilirsiniz: +Aynı şekilde, varsayılan değerlerini `None` yaparak isteğe bağlı query parametreleri tanımlayabilirsiniz: {* ../../docs_src/query_params/tutorial002_py310.py hl[7] *} -Bu durumda, `q` fonksiyon parametresi isteğe bağlı olacak ve varsayılan değer olarak `None` alacaktır. +Bu durumda, fonksiyon parametresi `q` isteğe bağlı olur ve varsayılan olarak `None` olur. /// check | Ek bilgi -Ayrıca, dikkatinizi çekerim ki; **FastAPI**, `item_id` parametresinin bir yol parametresi olduğunu ve `q` parametresinin yol değil bir sorgu parametresi olduğunu fark edecek kadar beceriklidir. +Ayrıca, **FastAPI** path parametresi olan `item_id`'nin bir path parametresi olduğunu ve `q`'nun path olmadığını fark edecek kadar akıllıdır; dolayısıyla bu bir query parametresidir. /// -## Sorgu Parametresi Tip Dönüşümü +## Sorgu parametresi tip dönüşümü { #query-parameter-type-conversion } -Aşağıda görüldüğü gibi dönüştürülmek üzere `bool` tipleri de tanımlayabilirsiniz: +`bool` tipleri de tanımlayabilirsiniz, ve bunlar dönüştürülür: {* ../../docs_src/query_params/tutorial003_py310.py hl[7] *} -Bu durumda, eğer şu adrese giderseniz: +Bu durumda, şuraya giderseniz: ``` http://127.0.0.1:8000/items/foo?short=1 @@ -107,38 +107,38 @@ veya http://127.0.0.1:8000/items/foo?short=yes ``` -veya adres, herhangi farklı bir harf varyasyonu içermesi durumuna rağmen (büyük harf, sadece baş harfi büyük kelime, vb.) fonksiyonunuz, `bool` tipli `short` parametresini `True` olarak algılayacaktır. Aksi halde `False` olarak algılanacaktır. +veya başka herhangi bir büyük/küçük harf varyasyonunda (tamamı büyük, ilk harf büyük, vb.), fonksiyonunuz `short` parametresini `bool` değeri `True` olarak görecektir. Aksi halde `False` olarak görür. -## Çoklu Yol ve Sorgu Parametreleri +## Çoklu path ve query parametreleri { #multiple-path-and-query-parameters } -**FastAPI** neyin ne olduğunu ayırt edebileceğinden dolayı aynı anda birden fazla yol ve sorgu parametresi tanımlayabilirsiniz. +Aynı anda birden fazla path parametresi ve query parametresi tanımlayabilirsiniz; **FastAPI** hangisinin hangisi olduğunu bilir. -Ve parametreleri, herhangi bir sıraya koymanıza da gerek yoktur. +Ayrıca bunları belirli bir sırayla tanımlamanız gerekmez. -İsimlerine göre belirleneceklerdir: +İsme göre tespit edilirler: {* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## Zorunlu Sorgu Parametreleri +## Zorunlu query parametreleri { #required-query-parameters } -Türü yol olmayan bir parametre (şu ana kadar sadece sorgu parametrelerini gördük) için varsayılan değer tanımlarsanız o parametre zorunlu olmayacaktır. +Path olmayan parametreler (şimdilik sadece query parametrelerini gördük) için varsayılan değer tanımladığınızda, bu parametre zorunlu olmaz. -Parametre için belirli bir değer atamak istemeyip parametrenin sadece isteğe bağlı olmasını istiyorsanız değerini `None` olarak atayabilirsiniz. +Belirli bir değer eklemek istemiyor ama sadece opsiyonel olmasını istiyorsanız, varsayılanı `None` olarak ayarlayın. -Fakat, bir sorgu parametresini zorunlu yapmak istiyorsanız varsayılan bir değer atamamanız yeterli olacaktır: +Ancak bir query parametresini zorunlu yapmak istediğinizde, herhangi bir varsayılan değer tanımlamamanız yeterlidir: -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} -Burada `needy` parametresi `str` tipinden oluşan zorunlu bir sorgu parametresidir. +Burada query parametresi `needy`, `str` tipinde zorunlu bir query parametresidir. -Eğer tarayıcınızda şu bağlantıyı: +Tarayıcınızda şöyle bir URL açarsanız: ``` http://127.0.0.1:8000/items/foo-item ``` -...`needy` parametresini eklemeden açarsanız şuna benzer bir hata ile karşılaşırsınız: +...zorunlu `needy` parametresini eklemeden, şuna benzer bir hata görürsünüz: ```JSON { @@ -156,13 +156,13 @@ http://127.0.0.1:8000/items/foo-item } ``` -`needy` zorunlu bir parametre olduğundan dolayı bağlantıda tanımlanması gerekir: +`needy` zorunlu bir parametre olduğundan, URL'de ayarlamanız gerekir: ``` http://127.0.0.1:8000/items/foo-item?needy=sooooneedy ``` -...bu iş görür: +...bu çalışır: ```JSON { @@ -171,11 +171,11 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy } ``` -Ve elbette, bazı parametreleri zorunlu, bazılarını varsayılan değerli ve bazılarını tamamen opsiyonel olarak tanımlayabilirsiniz: +Ve elbette, bazı parametreleri zorunlu, bazılarını varsayılan değerli, bazılarını da tamamen isteğe bağlı olarak tanımlayabilirsiniz: {* ../../docs_src/query_params/tutorial006_py310.py hl[8] *} -Bu durumda, 3 tane sorgu parametresi var olacaktır: +Bu durumda, 3 tane query parametresi vardır: * `needy`, zorunlu bir `str`. * `skip`, varsayılan değeri `0` olan bir `int`. @@ -183,6 +183,6 @@ Bu durumda, 3 tane sorgu parametresi var olacaktır: /// tip | İpucu -Ayrıca, [Yol Parametrelerinde](path-params.md#on-tanml-degerler){.internal-link target=_blank} de kullanıldığı şekilde `Enum` sınıfından faydalanabilirsiniz. +[Path Parametreleri](path-params.md#predefined-values){.internal-link target=_blank} ile aynı şekilde `Enum`'ları da kullanabilirsiniz. /// diff --git a/docs/tr/docs/tutorial/request-forms.md b/docs/tr/docs/tutorial/request-forms.md index e4e04f5f9..4608a6b79 100644 --- a/docs/tr/docs/tutorial/request-forms.md +++ b/docs/tr/docs/tutorial/request-forms.md @@ -1,69 +1,73 @@ -# Form Verisi +# Form Verisi { #form-data } -İstek gövdesinde JSON verisi yerine form alanlarını karşılamanız gerketiğinde `Form` sınıfını kullanabilirsiniz. +JSON yerine form alanlarını almanız gerektiğinde `Form` kullanabilirsiniz. /// info | Bilgi -Formları kullanmak için öncelikle <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a> paketini indirmeniz gerekmektedir. +Formları kullanmak için önce <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a> paketini kurun. -Örneğin `pip install python-multipart`. +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu etkinleştirdiğinizden emin olun ve ardından örneğin şöyle kurun: + +```console +$ pip install python-multipart +``` /// -## `Form` Sınıfını Projenize Dahil Edin +## `Form`'u Import Edin { #import-form } -`Form` sınıfını `fastapi`'den projenize dahil edin: +`Form`'u `fastapi`'den import edin: {* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} -## `Form` Parametrelerini Tanımlayın +## `Form` Parametrelerini Tanımlayın { #define-form-parameters } Form parametrelerini `Body` veya `Query` için yaptığınız gibi oluşturun: {* ../../docs_src/request_forms/tutorial001_an_py39.py hl[9] *} -Örneğin, OAuth2 spesifikasyonunun kullanılabileceği ("şifre akışı" olarak adlandırılan) yollardan birinde, form alanları olarak <abbr title="Kullanıcı Adı: Username">"username"</abbr> ve <abbr title="Şifre: Password">"password"</abbr> gönderilmesi gerekir. +Örneğin OAuth2 spesifikasyonunun kullanılabileceği ("password flow" olarak adlandırılan) yollardan birinde, form alanları olarak bir `username` ve `password` göndermek zorunludur. -Bu <abbr title="Spesifikasyon: Specification">spesifikasyon</abbr> form alanlarını adlandırırken isimlerinin birebir `username` ve `password` olmasını ve JSON verisi yerine form verisi olarak gönderilmesini gerektirir. +<abbr title="specification - spesifikasyon">spec</abbr>, alanların adının tam olarak `username` ve `password` olmasını ve JSON değil form alanları olarak gönderilmesini gerektirir. -`Form` sınıfıyla tanımlama yaparken `Body`, `Query`, `Path` ve `Cookie` sınıflarında kullandığınız aynı validasyon, örnekler, isimlendirme (örneğin `username` yerine `user-name` kullanımı) ve daha fazla konfigurasyonu kullanabilirsiniz. +`Form` ile `Body` (ve `Query`, `Path`, `Cookie`) ile yaptığınız aynı konfigürasyonları tanımlayabilirsiniz; validasyon, örnekler, alias (örn. `username` yerine `user-name`) vb. dahil. /// info | Bilgi -`Form` doğrudan `Body` sınıfını miras alan bir sınıftır. +`Form`, doğrudan `Body`'den miras alan bir sınıftır. /// /// tip | İpucu -Form gövdelerini tanımlamak için `Form` sınıfını kullanmanız gerekir; çünkü bu olmadan parametreler sorgu parametreleri veya gövde (JSON) parametreleri olarak yorumlanır. +Form gövdelerini tanımlamak için `Form`'u açıkça kullanmanız gerekir; çünkü bunu yapmazsanız parametreler query parametreleri veya body (JSON) parametreleri olarak yorumlanır. /// -## "Form Alanları" Hakkında +## "Form Alanları" Hakkında { #about-form-fields } -HTML formlarının (`<form></form>`) verileri sunucuya gönderirken JSON'dan farklı özel bir kodlama kullanır. +HTML formlarının (`<form></form>`) verileri sunucuya gönderme şekli normalde bu veri için JSON'dan farklı "özel" bir encoding kullanır. -**FastAPI** bu verilerin JSON yerine doğru şekilde okunmasını sağlayacaktır. +**FastAPI** bu veriyi JSON yerine doğru yerden okuyacaktır. /// note | Teknik Detaylar -Form verileri normalde `application/x-www-form-urlencoded` medya tipiyle kodlanır. +Formlardan gelen veri normalde "media type" `application/x-www-form-urlencoded` kullanılarak encode edilir. -Ancak form içerisinde dosyalar yer aldığında `multipart/form-data` olarak kodlanır. Bir sonraki bölümde dosyaların işlenmesi hakkında bilgi edineceksiniz. +Ancak form dosyalar içerdiğinde `multipart/form-data` olarak encode edilir. Dosyaları ele almayı bir sonraki bölümde okuyacaksınız. -Form kodlama türleri ve form alanları hakkında daha fazla bilgi edinmek istiyorsanız <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> web docs for <code>POST</code></a> sayfasını ziyaret edebilirsiniz. +Bu encoding'ler ve form alanları hakkında daha fazla okumak isterseniz, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> web docs for <code>POST</code></a> sayfasına gidin. /// /// warning | Uyarı -*Yol operasyonları* içerisinde birden fazla `Form` parametresi tanımlayabilirsiniz ancak bunlarla birlikte JSON verisi kabul eden `Body` alanları tanımlayamazsınız çünkü bu durumda istek gövdesi `application/json` yerine `application/x-www-form-urlencoded` ile kodlanmış olur. +Bir *path operation* içinde birden fazla `Form` parametresi tanımlayabilirsiniz, ancak JSON olarak almayı beklediğiniz `Body` alanlarını da ayrıca tanımlayamazsınız; çünkü bu durumda request'in body'si `application/json` yerine `application/x-www-form-urlencoded` ile encode edilmiş olur. -Bu **FastAPI**'ın getirdiği bir kısıtlama değildir, HTTP protokolünün bir parçasıdır. +Bu **FastAPI**'ın bir kısıtlaması değildir, HTTP protokolünün bir parçasıdır. /// -## Özet +## Özet { #recap } -Form verisi girdi parametreleri tanımlamak için `Form` sınıfını kullanın. +Form verisi girdi parametrelerini tanımlamak için `Form` kullanın. diff --git a/docs/tr/docs/tutorial/static-files.md b/docs/tr/docs/tutorial/static-files.md index 4542aca77..d30b4389d 100644 --- a/docs/tr/docs/tutorial/static-files.md +++ b/docs/tr/docs/tutorial/static-files.md @@ -1,40 +1,40 @@ -# Statik Dosyalar +# Statik Dosyalar { #static-files } -`StaticFiles`'ı kullanarak statik dosyaları bir yol altında sunabilirsiniz. +`StaticFiles` kullanarak bir dizindeki statik dosyaları otomatik olarak sunabilirsiniz. -## `StaticFiles` Kullanımı +## `StaticFiles` Kullanımı { #use-staticfiles } -* `StaticFiles` sınıfını projenize dahil edin. -* Bir `StaticFiles()` örneğini belirli bir yola bağlayın. +* `StaticFiles`'ı import edin. +* Belirli bir path'te bir `StaticFiles()` örneğini "mount" edin. -{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} +{* ../../docs_src/static_files/tutorial001_py39.py hl[2,6] *} /// note | Teknik Detaylar -Projenize dahil etmek için `from starlette.staticfiles import StaticFiles` kullanabilirsiniz. +`from starlette.staticfiles import StaticFiles` da kullanabilirsiniz. -**FastAPI**, geliştiricilere kolaylık sağlamak amacıyla `starlette.staticfiles`'ı `fastapi.staticfiles` olarak sağlar. Ancak `StaticFiles` sınıfı aslında doğrudan Starlette'den gelir. +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.staticfiles`'ı `fastapi.staticfiles` olarak da sağlar. Ancak aslında doğrudan Starlette'den gelir. /// -### Bağlama (Mounting) Nedir? +### "Mounting" Nedir { #what-is-mounting } -"Bağlamak", belirli bir yola tamamen "bağımsız" bir uygulama eklemek anlamına gelir ve ardından tüm alt yollara gelen istekler bu uygulama tarafından işlenir. +"Mounting", belirli bir path'te tamamen "bağımsız" bir uygulama eklemek ve sonrasında tüm alt path'leri handle etmesini sağlamak demektir. -Bu, bir `APIRouter` kullanmaktan farklıdır çünkü bağlanmış bir uygulama tamamen bağımsızdır. Ana uygulamanızın OpenAPI ve dokümanlar, bağlanmış uygulamadan hiçbir şey içermez, vb. +Bu, bir `APIRouter` kullanmaktan farklıdır; çünkü mount edilen uygulama tamamen bağımsızdır. Ana uygulamanızın OpenAPI ve docs'ları, mount edilen uygulamadan hiçbir şey içermez, vb. -[Advanced User Guide](../advanced/index.md){.internal-link target=_blank} bölümünde daha fazla bilgi edinebilirsiniz. +Bununla ilgili daha fazla bilgiyi [Advanced User Guide](../advanced/index.md){.internal-link target=_blank} içinde okuyabilirsiniz. -## Detaylar +## Detaylar { #details } -`"/static"` ifadesi, bu "alt uygulamanın" "bağlanacağı" alt yolu belirtir. Bu nedenle, `"/static"` ile başlayan her yol, bu uygulama tarafından işlenir. +İlk `"/static"`, bu "alt uygulamanın" "mount" edileceği alt path'i ifade eder. Dolayısıyla `"/static"` ile başlayan herhangi bir path bunun tarafından handle edilir. -`directory="static"` ifadesi, statik dosyalarınızı içeren dizinin adını belirtir. +`directory="static"`, statik dosyalarınızı içeren dizinin adını ifade eder. -`name="static"` ifadesi, alt uygulamanın **FastAPI** tarafından kullanılacak ismini belirtir. +`name="static"`, **FastAPI**'nin dahili olarak kullanabileceği bir isim verir. -Bu parametrelerin hepsi "`static`"den farklı olabilir, bunları kendi uygulamanızın ihtiyaçlarına göre belirleyebilirsiniz. +Bu parametrelerin hepsi "`static`" ile aynı olmak zorunda değildir; kendi uygulamanızın ihtiyaçlarına ve özel detaylarına göre ayarlayın. -## Daha Fazla Bilgi +## Daha Fazla Bilgi { #more-info } -Daha fazla detay ve seçenek için <a href="https://www.starlette.dev/staticfiles/" class="external-link" target="_blank">Starlette'in Statik Dosyalar hakkındaki dokümantasyonunu</a> incelleyin. +Daha fazla detay ve seçenek için <a href="https://www.starlette.dev/staticfiles/" class="external-link" target="_blank">Starlette'in Statik Dosyalar hakkındaki dokümanlarını</a> inceleyin. From 08924400c2c75533685148b029defcf6913f1def Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 18:32:52 +0000 Subject: [PATCH 114/243] =?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 975bf774e..eb72d559d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,6 +19,7 @@ hide: ### Translations +* 🌐 Update translations for tr (update-outdated). PR [#14745](https://github.com/fastapi/fastapi/pull/14745) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update `llm-prompt.md` for Korean language. PR [#14763](https://github.com/fastapi/fastapi/pull/14763) by [@seuthootDev](https://github.com/seuthootDev). * 🌐 Update translations for ko (update outdated, found by fixer tool). PR [#14738](https://github.com/fastapi/fastapi/pull/14738) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for de (update-outdated). PR [#14690](https://github.com/fastapi/fastapi/pull/14690) by [@tiangolo](https://github.com/tiangolo). From df6530e0020f0b26951fff58b02d0a7e9ab834d3 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:44:39 +0300 Subject: [PATCH 115/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20uk=20(update=20outdated,=20found=20by=20fixer=20tool)=20(?= =?UTF-8?q?#14739)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/uk/docs/alternatives.md | 80 ++++++++++---------- docs/uk/docs/fastapi-cli.md | 96 +++++++++++------------- docs/uk/docs/features.md | 138 +++++++++++++++++++---------------- 3 files changed, 160 insertions(+), 154 deletions(-) diff --git a/docs/uk/docs/alternatives.md b/docs/uk/docs/alternatives.md index 786df45c5..d44ca794f 100644 --- a/docs/uk/docs/alternatives.md +++ b/docs/uk/docs/alternatives.md @@ -1,8 +1,8 @@ -# Альтернативи, натхнення та порівняння +# Альтернативи, натхнення та порівняння { #alternatives-inspiration-and-comparisons } -Що надихнуло на створення **FastAPI**, який він у порінянні з іншими альтернативами та чого він у них навчився. +Що надихнуло **FastAPI**, як він порівнюється з альтернативами та чого він у них навчився. -## Вступ +## Вступ { #intro } **FastAPI** не існувало б, якби не попередні роботи інших. @@ -12,17 +12,17 @@ Але в якийсь момент не було іншого виходу, окрім створення чогось, що надавало б усі ці функції, взявши найкращі ідеї з попередніх інструментів і поєднавши їх найкращим чином, використовуючи мовні функції, які навіть не були доступні раніше (Python 3.6+ підказки типів). -## Попередні інструменти +## Попередні інструменти { #previous-tools } -### <a href="https://www.djangoproject.com/" class="external-link" target="_blank">Django</a> +### <a href="https://www.djangoproject.com/" class="external-link" target="_blank">Django</a> { #django } Це найпопулярніший фреймворк Python, який користується широкою довірою. Він використовується для створення таких систем, як Instagram. Він відносно тісно пов’язаний з реляційними базами даних (наприклад, MySQL або PostgreSQL), тому мати базу даних NoSQL (наприклад, Couchbase, MongoDB, Cassandra тощо) як основний механізм зберігання не дуже просто. -Він був створений для створення HTML у серверній частині, а не для створення API, які використовуються сучасним інтерфейсом (як-от React, Vue.js і Angular) або іншими системами (як-от <abbr title="Internet of Things">IoT</abbr > пристрої), які спілкуються з ним. +Він був створений для створення HTML у серверній частині, а не для створення API, які використовуються сучасним інтерфейсом (як-от React, Vue.js і Angular) або іншими системами (як-от <abbr title="Internet of Things - Інтернет речей">IoT</abbr> пристрої), які спілкуються з ним. -### <a href="https://www.django-rest-framework.org/" class="external-link" target="_blank">Django REST Framework</a> +### <a href="https://www.django-rest-framework.org/" class="external-link" target="_blank">Django REST Framework</a> { #django-rest-framework } Фреймворк Django REST був створений як гнучкий інструментарій для створення веб-інтерфейсів API використовуючи Django в основі, щоб покращити його можливості API. @@ -42,7 +42,7 @@ Django REST Framework створив Том Крісті. Той самий тв /// -### <a href="https://flask.palletsprojects.com" class="external-link" target="_blank">Flask</a> +### <a href="https://flask.palletsprojects.com" class="external-link" target="_blank">Flask</a> { #flask } Flask — це «мікрофреймворк», він не включає інтеграцію бази даних, а також багато речей, які за замовчуванням є в Django. @@ -64,7 +64,7 @@ Flask — це «мікрофреймворк», він не включає ін /// -### <a href="https://requests.readthedocs.io" class="external-link" target="_blank">Requests</a> +### <a href="https://requests.readthedocs.io" class="external-link" target="_blank">Requests</a> { #requests } **FastAPI** насправді не є альтернативою **Requests**. Сфера їх застосування дуже різна. @@ -88,12 +88,12 @@ Requests мають дуже простий та інтуїтивно зрозу response = requests.get("http://example.com/some/url") ``` -Відповідна операція *роуту* API FastAPI може виглядати так: +Відповідна операція шляху API FastAPI може виглядати так: ```Python hl_lines="1" @app.get("/some/url") def read_url(): - return {"message": "Hello World"} + return {"message": "Hello World"} ``` Зверніть увагу на схожість у `requests.get(...)` і `@app.get(...)`. @@ -101,12 +101,12 @@ def read_url(): /// check | Надихнуло **FastAPI** на * Майте простий та інтуїтивно зрозумілий API. - * Використовуйте імена (операції) методів HTTP безпосередньо, простим та інтуїтивно зрозумілим способом. - * Розумні параметри за замовчуванням, але потужні налаштування. +* Використовуйте імена (операції) методів HTTP безпосередньо, простим та інтуїтивно зрозумілим способом. +* Розумні параметри за замовчуванням, але потужні налаштування. /// -### <a href="https://swagger.io/" class="external-link" target="_blank">Swagger</a> / <a href="https://github.com/OAI /OpenAPI-Specification/" class="external-link" target="_blank">OpenAPI</a> +### <a href="https://swagger.io/" class="external-link" target="_blank">Swagger</a> / <a href="https://github.com/OAI/OpenAPI-Specification/" class="external-link" target="_blank">OpenAPI</a> { #swagger-openapi } Головною функцією, яку я хотів від Django REST Framework, була автоматична API документація. @@ -124,18 +124,18 @@ def read_url(): Інтегрувати інструменти інтерфейсу на основі стандартів: - * <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Інтерфейс Swagger</a> - * <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> +* <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Інтерфейс Swagger</a> +* <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> Ці два було обрано через те, що вони досить популярні та стабільні, але, виконавши швидкий пошук, ви можете знайти десятки додаткових альтернативних інтерфейсів для OpenAPI (які можна використовувати з **FastAPI**). /// -### Фреймворки REST для Flask +### Фреймворки REST для Flask { #flask-rest-frameworks } Існує кілька фреймворків Flask REST, але, витративши час і роботу на їх дослідження, я виявив, що багато з них припинено або залишено, з кількома постійними проблемами, які зробили їх непридатними. -### <a href="https://marshmallow.readthedocs.io/en/stable/" class="external-link" target="_blank">Marshmallow</a> +### <a href="https://marshmallow.readthedocs.io/en/stable/" class="external-link" target="_blank">Marshmallow</a> { #marshmallow } Однією з головних функцій, необхідних для систем API, є "<abbr title="також звана marshalling, conversion">серіалізація</abbr>", яка бере дані з коду (Python) і перетворює їх на щось, що можна надіслати через мережу. Наприклад, перетворення об’єкта, що містить дані з бази даних, на об’єкт JSON. Перетворення об’єктів `datetime` на строки тощо. @@ -153,7 +153,7 @@ Marshmallow створено для забезпечення цих функці /// -### <a href="https://webargs.readthedocs.io/en/latest/" class="external-link" target="_blank">Webargs</a> +### <a href="https://webargs.readthedocs.io/en/latest/" class="external-link" target="_blank">Webargs</a> { #webargs } Іншою важливою функцією, необхідною для API, є <abbr title="читання та перетворення даних Python">аналіз</abbr> даних із вхідних запитів. @@ -175,7 +175,7 @@ Webargs був створений тими ж розробниками Marshmall /// -### <a href="https://apispec.readthedocs.io/en/stable/" class="external-link" target="_blank">APISpec</a> +### <a href="https://apispec.readthedocs.io/en/stable/" class="external-link" target="_blank">APISpec</a> { #apispec } Marshmallow і Webargs забезпечують перевірку, аналіз і серіалізацію як плагіни. @@ -205,7 +205,7 @@ APISpec був створений тими ж розробниками Marshmall /// -### <a href="https://flask-apispec.readthedocs.io/en/latest/" class="external-link" target="_blank">Flask-apispec</a> +### <a href="https://flask-apispec.readthedocs.io/en/latest/" class="external-link" target="_blank">Flask-apispec</a> { #flask-apispec } Це плагін Flask, який об’єднує Webargs, Marshmallow і APISpec. @@ -237,13 +237,13 @@ Flask-apispec був створений тими ж розробниками Mar /// -### <a href="https://nestjs.com/" class="external-link" target="_blank">NestJS</a> (та <a href="https://angular.io/ " class="external-link" target="_blank">Angular</a>) +### <a href="https://nestjs.com/" class="external-link" target="_blank">NestJS</a> (та <a href="https://angular.io/" class="external-link" target="_blank">Angular</a>) { #nestjs-and-angular } Це навіть не Python, NestJS — це фреймворк NodeJS JavaScript (TypeScript), натхненний Angular. Це досягає чогось подібного до того, що можна зробити з Flask-apispec. -Він має інтегровану систему впровадження залежностей, натхненну Angular two. Він потребує попередньої реєстрації «injectables» (як і всі інші системи впровадження залежностей, які я знаю), тому це збільшує багатослівність та повторення коду. +Він має інтегровану систему впровадження залежностей, натхненну Angular 2. Він потребує попередньої реєстрації «injectables» (як і всі інші системи впровадження залежностей, які я знаю), тому це збільшує багатослівність та повторення коду. Оскільки параметри описані за допомогою типів TypeScript (подібно до підказок типу Python), підтримка редактора досить хороша. @@ -259,7 +259,7 @@ Flask-apispec був створений тими ж розробниками Mar /// -### <a href="https://sanic.readthedocs.io/en/latest/" class="external-link" target="_blank">Sanic</a> +### <a href="https://sanic.readthedocs.io/en/latest/" class="external-link" target="_blank">Sanic</a> { #sanic } Це був один із перших надзвичайно швидких фреймворків Python на основі `asyncio`. Він був дуже схожий на Flask. @@ -279,7 +279,7 @@ Flask-apispec був створений тими ж розробниками Mar /// -### <a href="https://falconframework.org/" class="external-link" target="_blank">Falcon</a> +### <a href="https://falconframework.org/" class="external-link" target="_blank">Falcon</a> { #falcon } Falcon — ще один високопродуктивний фреймворк Python, він розроблений як мінімальний і працює як основа інших фреймворків, таких як Hug. @@ -297,7 +297,7 @@ Falcon — ще один високопродуктивний фреймворк /// -### <a href="https://moltenframework.com/" class="external-link" target="_blank">Molten</a> +### <a href="https://moltenframework.com/" class="external-link" target="_blank">Molten</a> { #molten } Я відкрив для себе Molten на перших етапах створення **FastAPI**. І він має досить схожі ідеї: @@ -321,7 +321,7 @@ Falcon — ще один високопродуктивний фреймворк /// -### <a href="https://github.com/hugapi/hug" class="external-link" target="_blank">Hug</a> +### <a href="https://github.com/hugapi/hug" class="external-link" target="_blank">Hug</a> { #hug } Hug був одним із перших фреймворків, який реалізував оголошення типів параметрів API за допомогою підказок типу Python. Це була чудова ідея, яка надихнула інші інструменти зробити те саме. @@ -351,7 +351,7 @@ Hug надихнув частину APIStar і був одним із найбі /// -### <a href="https://github.com/encode/apistar" class="external-link" target="_blank">APIStar</a> (<= 0,5) +### <a href="https://github.com/encode/apistar" class="external-link" target="_blank">APIStar</a> (<= 0,5) { #apistar-0-5 } Безпосередньо перед тим, як вирішити створити **FastAPI**, я знайшов сервер **APIStar**. Він мав майже все, що я шукав, і мав чудовий дизайн. @@ -379,9 +379,9 @@ Hug надихнув частину APIStar і був одним із найбі APIStar створив Том Крісті. Той самий хлопець, який створив: - * Django REST Framework - * Starlette (на якому базується **FastAPI**) - * Uvicorn (використовується Starlette і **FastAPI**) +* Django REST Framework +* Starlette (на якому базується **FastAPI**) +* Uvicorn (використовується Starlette і **FastAPI**) /// @@ -393,13 +393,15 @@ APIStar створив Том Крісті. Той самий хлопець, я І після тривалого пошуку подібної структури та тестування багатьох різних альтернатив, APIStar став найкращим доступним варіантом. - Потім APIStar перестав існувати як сервер, і було створено Starlette, який став новою кращою основою для такої системи. Це стало останнім джерелом натхнення для створення **FastAPI**. Я вважаю **FastAPI** «духовним спадкоємцем» APIStar, удосконалюючи та розширюючи функції, систему введення тексту та інші частини на основі досвіду, отриманого від усіх цих попередніх інструментів. + Потім APIStar перестав існувати як сервер, і було створено Starlette, який став новою кращою основою для такої системи. Це стало останнім джерелом натхнення для створення **FastAPI**. + + Я вважаю **FastAPI** «духовним спадкоємцем» APIStar, удосконалюючи та розширюючи функції, систему типізації та інші частини на основі досвіду, отриманого від усіх цих попередніх інструментів. /// -## Використовується **FastAPI** +## Використовується **FastAPI** { #used-by-fastapi } -### <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> +### <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> { #pydantic } Pydantic — це бібліотека для визначення перевірки даних, серіалізації та документації (за допомогою схеми JSON) на основі підказок типу Python. @@ -415,9 +417,9 @@ Pydantic — це бібліотека для визначення переві /// -### <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a> +### <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a> { #starlette } -Starlette — це легкий фреймворк/набір інструментів <abbr title="The new standard for build asynchronous Python web">ASGI</abbr>, який ідеально підходить для створення високопродуктивних asyncio сервісів. +Starlette — це легкий фреймворк/набір інструментів <abbr title="Новий стандарт для створення асинхронних Python вебзастосунків">ASGI</abbr>, який ідеально підходить для створення високопродуктивних asyncio сервісів. Він дуже простий та інтуїтивно зрозумілий. Його розроблено таким чином, щоб його можна було легко розширювати та мати модульні компоненти. @@ -460,7 +462,7 @@ ASGI — це новий «стандарт», який розробляєтьс /// -### <a href="https://www.uvicorn.dev/" class="external-link" target="_blank">Uvicorn</a> +### <a href="https://www.uvicorn.dev/" class="external-link" target="_blank">Uvicorn</a> { #uvicorn } Uvicorn — це блискавичний сервер ASGI, побудований на uvloop і httptools. @@ -472,12 +474,12 @@ Uvicorn — це блискавичний сервер ASGI, побудован Основний веб-сервер для запуску програм **FastAPI**. - Ви можете поєднати його з Gunicorn, щоб мати асинхронний багатопроцесний сервер. + Ви також можете використати параметр командного рядка `--workers`, щоб мати асинхронний багатопроцесний сервер. Додаткову інформацію див. у розділі [Розгортання](deployment/index.md){.internal-link target=_blank}. /// -## Орієнтири та швидкість +## Орієнтири та швидкість { #benchmarks-and-speed } Щоб зрозуміти, порівняти та побачити різницю між Uvicorn, Starlette і FastAPI, перегляньте розділ про [Бенчмарки](benchmarks.md){.internal-link target=_blank}. diff --git a/docs/uk/docs/fastapi-cli.md b/docs/uk/docs/fastapi-cli.md index f18b10471..eb5538230 100644 --- a/docs/uk/docs/fastapi-cli.md +++ b/docs/uk/docs/fastapi-cli.md @@ -1,83 +1,75 @@ -# FastAPI CLI +# FastAPI CLI { #fastapi-cli } -**FastAPI CLI** це програма командного рядка, яку Ви можете використовувати, щоб обслуговувати Ваш додаток FastAPI, керувати Вашими FastApi проектами, тощо. +**FastAPI CLI** — це програма командного рядка, яку ви можете використовувати, щоб обслуговувати ваш застосунок FastAPI, керувати вашим проєктом FastAPI тощо. -Коли Ви встановлюєте FastApi (тобто виконуєте `pip install "fastapi[standard]"`), Ви також встановлюєте пакунок `fastapi-cli`, цей пакунок надає команду `fastapi` в терміналі. +Коли ви встановлюєте FastAPI (наприклад, за допомогою `pip install "fastapi[standard]"`), він включає пакет під назвою `fastapi-cli`, цей пакет надає команду `fastapi` у терміналі. -Для запуску Вашого FastAPI проекту для розробки, Ви можете скористатись командою `fastapi dev`: +Щоб запустити ваш застосунок FastAPI для розробки, ви можете використати команду `fastapi dev`: <div class="termy"> ```console -$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:single">main.py</u> -<font color="#3465A4">INFO </font> Using path <font color="#3465A4">main.py</font> -<font color="#3465A4">INFO </font> Resolved absolute path <font color="#75507B">/home/user/code/awesomeapp/</font><font color="#AD7FA8">main.py</font> -<font color="#3465A4">INFO </font> Searching for package file structure from directories with <font color="#3465A4">__init__.py</font> files -<font color="#3465A4">INFO </font> Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> +$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:solid">main.py</u> - ╭─ <font color="#8AE234"><b>Python module file</b></font> ─╮ - │ │ - │ 🐍 main.py │ - │ │ - ╰──────────────────────╯ + <span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting development server 🚀 -<font color="#3465A4">INFO </font> Importing module <font color="#4E9A06">main</font> -<font color="#3465A4">INFO </font> Found importable FastAPI app + Searching for package file structure from directories with + <font color="#3465A4">__init__.py</font> files + Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> - ╭─ <font color="#8AE234"><b>Importable FastAPI app</b></font> ─╮ - │ │ - │ <span style="background-color:#272822"><font color="#FF4689">from</font></span><span style="background-color:#272822"><font color="#F8F8F2"> main </font></span><span style="background-color:#272822"><font color="#FF4689">import</font></span><span style="background-color:#272822"><font color="#F8F8F2"> app</font></span><span style="background-color:#272822"> </span> │ - │ │ - ╰──────────────────────────╯ + <span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py -<font color="#3465A4">INFO </font> Using import string <font color="#8AE234"><b>main:app</b></font> + <span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with the + following code: - <span style="background-color:#C4A000"><font color="#2E3436">╭────────── FastAPI CLI - Development mode ───────────╮</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ Serving at: http://127.0.0.1:8000 │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ API docs: http://127.0.0.1:8000/docs │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ Running in development mode, for production use: │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ </font></span><span style="background-color:#C4A000"><font color="#555753"><b>fastapi run</b></font></span><span style="background-color:#C4A000"><font color="#2E3436"> │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> - <span style="background-color:#C4A000"><font color="#2E3436">╰─────────────────────────────────────────────────────╯</font></span> + <u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> -<font color="#4E9A06">INFO</font>: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] -<font color="#4E9A06">INFO</font>: Uvicorn running on <b>http://127.0.0.1:8000</b> (Press CTRL+C to quit) -<font color="#4E9A06">INFO</font>: Started reloader process [<font color="#34E2E2"><b>2265862</b></font>] using <font color="#34E2E2"><b>WatchFiles</b></font> -<font color="#4E9A06">INFO</font>: Started server process [<font color="#06989A">2265873</font>] -<font color="#4E9A06">INFO</font>: Waiting for application startup. -<font color="#4E9A06">INFO</font>: Application startup complete. + <span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> + + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> + <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000/docs</u></font> + + <span style="background-color:#007166"><font color="#D3D7CF"> tip </font></span> Running in development mode, for production use: + <b>fastapi run</b> + + Logs: + + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Will watch for changes in these directories: + <b>[</b><font color="#4E9A06">'/home/user/code/awesomeapp'</font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> <b>(</b>Press CTRL+C to + quit<b>)</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started reloader process <b>[</b><font color="#34E2E2"><b>383138</b></font><b>]</b> using WatchFiles + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>383153</b></font><b>]</b> + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. + <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. ``` </div> -Програма командного рядка `fastapi` це **FastAPI CLI**. +Програма командного рядка під назвою `fastapi` — це **FastAPI CLI**. -FastAPI CLI приймає шлях до Вашої Python програми (напр. `main.py`) і автоматично виявляє екземпляр `FastAPI` (зазвичай названий `app`), обирає коректний процес імпорту, а потім обслуговує його. +FastAPI CLI бере шлях до вашої Python-програми (наприклад, `main.py`) і автоматично виявляє екземпляр `FastAPI` (зазвичай з назвою `app`), визначає правильний процес імпорту, а потім обслуговує його. -Натомість, для запуску у продакшн використовуйте `fastapi run`. 🚀 +Натомість, для продакшн ви використали б `fastapi run`. 🚀 -Всередині **FastAPI CLI** використовує <a href="https://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a>, високопродуктивний, production-ready, ASGI cервер. 😎 +Внутрішньо **FastAPI CLI** використовує <a href="https://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a>, високопродуктивний, production-ready, ASGI сервер. 😎 -## `fastapi dev` +## `fastapi dev` { #fastapi-dev } -Використання `fastapi dev` ініціює режим розробки. +Запуск `fastapi dev` ініціює режим розробки. -За замовчуванням, **автоматичне перезавантаження** увімкнене, автоматично перезавантажуючи сервер кожного разу, коли Ви змінюєте Ваш код. Це ресурсо-затратно, та може бути менш стабільним, ніж коли воно вимкнене. Ви повинні використовувати його тільки під час розробки. Воно також слухає IP-адресу `127.0.0.1`, що є IP Вашого девайсу для самостійної комунікації з самим собою (`localhost`). +За замовчуванням **auto-reload** увімкнено, і сервер автоматично перезавантажується, коли ви вносите зміни у ваш код. Це ресурсоємно та може бути менш стабільним, ніж коли його вимкнено. Вам слід використовувати це лише для розробки. Також він слухає IP-адресу `127.0.0.1`, яка є IP-адресою для того, щоб ваша машина могла взаємодіяти лише сама з собою (`localhost`). -## `fastapi run` +## `fastapi run` { #fastapi-run } -Виконання `fastapi run` запустить FastAPI у продакшн-режимі за замовчуванням. +Виконання `fastapi run` за замовчуванням запускає FastAPI у продакшн-режимі. -За замовчуванням, **автоматичне перезавантаження** вимкнене. Воно також прослуховує IP-адресу `0.0.0.0`, що означає всі доступні IP адреси, тим самим даючи змогу будь-кому комунікувати з девайсом. Так Ви зазвичай будете запускати його у продакшн, наприклад у контейнері. +За замовчуванням **auto-reload** вимкнено. Також він слухає IP-адресу `0.0.0.0`, що означає всі доступні IP-адреси, таким чином він буде публічно доступним для будь-кого, хто може взаємодіяти з машиною. Зазвичай саме так ви запускатимете його в продакшн, наприклад у контейнері. -В більшості випадків Ви можете (і маєте) мати "termination proxy", який обробляє HTTPS для Вас, це залежить від способу розгортання вашого додатку, Ваш провайдер може зробити це для Вас, або Вам потрібно налаштувати його самостійно. +У більшості випадків ви (і вам слід) матимете «termination proxy», який обробляє HTTPS для вас зверху; це залежатиме від того, як ви розгортаєте ваш застосунок: ваш провайдер може зробити це за вас, або вам може знадобитися налаштувати це самостійно. -/// tip +/// tip | Порада -Ви можете дізнатись більше про це у [документації про розгортування](deployment/index.md){.internal-link target=_blank}. +Ви можете дізнатися більше про це в [документації з розгортання](deployment/index.md){.internal-link target=_blank}. /// diff --git a/docs/uk/docs/features.md b/docs/uk/docs/features.md index aa0ef7c79..d8233115f 100644 --- a/docs/uk/docs/features.md +++ b/docs/uk/docs/features.md @@ -1,21 +1,21 @@ -# Функціональні можливості +# Функціональні можливості { #features } -## Функціональні можливості FastAPI +## Функціональні можливості FastAPI { #fastapi-features } **FastAPI** надає вам такі можливості: -### Використання відкритих стандартів +### На основі відкритих стандартів { #based-on-open-standards } * <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank"><strong>OpenAPI</strong></a> для створення API, включаючи оголошення <abbr title="також відомі як: endpoints, маршрути">шляхів</abbr>, <abbr title="також відомі як HTTP-методи, наприклад, POST, GET, PUT, DELETE">операцій</abbr>, параметрів, тіл запитів, безпеки тощо. * Автоматична документація моделей даних за допомогою <a href="https://json-schema.org/" class="external-link" target="_blank"><strong>JSON Schema</strong></a> (оскільки OpenAPI базується саме на JSON Schema). * Розроблено на основі цих стандартів після ретельного аналізу, а не як додатковий рівень поверх основної архітектури. -* Це також дає змогу автоматично **генерувати код клієнта** багатьма мовами. +* Це також дає змогу використовувати автоматичну **генерацію клієнтського коду** багатьма мовами. -### Автоматична генерація документації +### Автоматична документація { #automatic-docs } -Інтерактивна документація API та вебінтерфейс для його дослідження. Оскільки фреймворк базується на OpenAPI, є кілька варіантів, два з яких включені за замовчуванням. +Інтерактивна документація API та вебінтерфейси для його дослідження. Оскільки фреймворк базується на OpenAPI, є кілька варіантів, 2 з яких включені за замовчуванням. -* <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank"><strong>Swagger UI</strong></a> — дозволяє інтерактивно переглядати API, викликати та тестувати його прямо у браузері. +* <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank"><strong>Swagger UI</strong></a> — з інтерактивним дослідженням, викликом і тестуванням вашого API прямо з браузера. ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) @@ -23,23 +23,25 @@ ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Тільки сучасний Python +### Лише сучасний Python { #just-modern-python } -FastAPI використовує стандартні **типи Python** (завдяки Pydantic). Вам не потрібно вивчати новий синтаксис — лише стандартний сучасний Python. +Усе базується на стандартних оголошеннях **типів Python** (завдяки Pydantic). Жодного нового синтаксису для вивчення. Лише стандартний сучасний Python. -Якщо вам потрібне коротке нагадування про використання типів у Python (навіть якщо ви не використовуєте FastAPI), перегляньте короткий підручник: [Вступ до типів Python](python-types.md){.internal-link target=_blank}. +Якщо вам потрібно 2-хвилинне нагадування про те, як використовувати типи Python (навіть якщо ви не використовуєте FastAPI), перегляньте короткий підручник: [Типи Python](python-types.md){.internal-link target=_blank}. -Ось приклад стандартного Python-коду з типами: +Ви пишете стандартний Python з типами: ```Python from datetime import date + from pydantic import BaseModel -# Оголошення змінної як str -# з підтримкою автодоповнення у редакторі +# Оголосіть змінну як str +# та отримайте підтримку редактора всередині функції def main(user_id: str): return user_id + # Модель Pydantic class User(BaseModel): id: int @@ -47,7 +49,7 @@ class User(BaseModel): joined: date ``` -Приклад використання цієї моделі: +Далі це можна використовувати так: ```Python my_user: User = User(id=3, name="John Doe", joined="2018-07-19") @@ -65,19 +67,21 @@ my_second_user: User = User(**second_user_data) `**second_user_data` означає: -Передати ключі та значення словника `second_user_data` як аргументи у вигляді "ключ-значення", еквівалентно `User(id=4, name="Mary", joined="2018-11-30")`. +Передати ключі та значення словника `second_user_data` безпосередньо як аргументи у вигляді «ключ-значення», еквівалентно: `User(id=4, name="Mary", joined="2018-11-30")` /// -### Підтримка редакторів (IDE) +### Підтримка редакторів (IDE) { #editor-support } -Фреймворк спроєктований так, щоб бути легким і інтуїтивно зрозумілим. Усі рішення тестувалися у різних редакторах ще до початку розробки, щоб забезпечити найкращий досвід програмування. +Увесь фреймворк спроєктовано так, щоб ним було легко та інтуїтивно користуватися; усі рішення тестувалися у кількох редакторах ще до початку розробки, щоб забезпечити найкращий досвід розробки. -За результатами опитувань розробників Python <a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank">однією з найпопулярніших функцій є "автодоповнення"</a>. +З опитувань розробників Python зрозуміло <a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank">що однією з найуживаніших функцій є «автодоповнення»</a>. -**FastAPI** повністю підтримує автодоповнення у всіх місцях, тому вам рідко доведеться повертатися до документації. +Увесь фреймворк **FastAPI** побудований так, щоб це забезпечити. Автодоповнення працює всюди. -Приклад автодоповнення у редакторах: +Вам рідко доведеться повертатися до документації. + +Ось як ваш редактор може вам допомогти: * у <a href="https://code.visualstudio.com/" class="external-link" target="_blank">Visual Studio Code</a>: @@ -87,17 +91,25 @@ my_second_user: User = User(**second_user_data) ![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) -### Короткий код -FastAPI має розумні налаштування **за замовчуванням**, але всі параметри можна налаштовувати відповідно до ваших потреб. Однак за замовчуванням все "просто працює". +Ви отримаєте автодоповнення в коді, який раніше могли вважати навіть неможливим. Наприклад, для ключа `price` всередині JSON body (який міг бути вкладеним), що надходить із запиту. + +Більше не доведеться вводити неправильні назви ключів, постійно повертатися до документації або прокручувати вгору-вниз, щоб знайти, чи ви зрештою використали `username` чи `user_name`. + +### Короткий код { #short } + +FastAPI має розумні **налаштування за замовчуванням** для всього, з можливістю конфігурації всюди. Усі параметри можна точно налаштувати під ваші потреби та визначити потрібний вам API. + +Але за замовчуванням усе **«просто працює»**. + +### Валідація { #validation } -### Валідація * Підтримка валідації для більшості (або всіх?) **типів даних Python**, зокрема: * JSON-об'єктів (`dict`). - * JSON-списків (`list`) з визначенням типів елементів. - * Рядків (`str`) із мінімальною та максимальною довжиною. - * Чисел (`int`, `float`) з обмеженнями мінімальних та максимальних значень тощо. + * JSON-масивів (`list`) із визначенням типів елементів. + * Полів-рядків (`str`) із визначенням мінімальної та максимальної довжини. + * Чисел (`int`, `float`) з мінімальними та максимальними значеннями тощо. -* Валідація складніших типів, таких як: +* Валідація для більш екзотичних типів, як-от: * URL. * Email. * UUID. @@ -105,55 +117,55 @@ FastAPI має розумні налаштування **за замовчува Уся валідація виконується через надійний та перевірений **Pydantic**. -### Безпека та автентифікація +### Безпека та автентифікація { #security-and-authentication } -**FastAPI** підтримує вбудовану автентифікацію та авторизацію, без прив’язки до конкретних баз даних чи моделей даних. +Інтегровані безпека та автентифікація. Без жодних компромісів із базами даних чи моделями даних. -Підтримуються всі схеми безпеки OpenAPI, включаючи: +Підтримуються всі схеми безпеки, визначені в OpenAPI, включно з: * HTTP Basic. -* **OAuth2** (також із підтримкою **JWT-токенів**). Див. підручник: [OAuth2 із JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* **OAuth2** (також із підтримкою **JWT tokens**). Перегляньте підручник: [OAuth2 із JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. * Ключі API в: * Заголовках. * Параметрах запиту. * Cookies тощо. -А також усі можливості безпеки від Starlette (зокрема **сесійні cookies**). +А також усі можливості безпеки від Starlette (зокрема **session cookies**). -Усі вони створені як багаторазові інструменти та компоненти, які легко інтегруються з вашими системами, сховищами даних, реляційними та NoSQL базами даних тощо. +Усе це зроблено як багаторазові інструменти та компоненти, які легко інтегруються з вашими системами, сховищами даних, реляційними та NoSQL базами даних тощо. -### Впровадження залежностей +### Впровадження залежностей { #dependency-injection } -**FastAPI** містить надзвичайно просту у використанні, але потужну систему впровадження залежностей. +FastAPI містить надзвичайно просту у використанні, але надзвичайно потужну систему <abbr title='також відоме як: "components", "resources", "services", "providers"'><strong>Dependency Injection</strong></abbr>. -* Залежності можуть мати власні залежності, утворюючи ієрархію або **"граф залежностей"**. -* Усі залежності автоматично керуються фреймворком. -* Усі залежності можуть отримувати дані з запитів і розширювати **обмеження операції за шляхом** та автоматичну документацію. -* **Автоматична валідація** навіть для параметрів *операцій шляху*, визначених у залежностях. -* Підтримка складних систем автентифікації користувачів, **з'єднань із базами даних** тощо. -* **Жодних обмежень** щодо використання баз даних, фронтендів тощо, але водночас проста інтеграція з усіма ними. +* Навіть залежності можуть мати власні залежності, утворюючи ієрархію або **«граф» залежностей**. +* Усе **автоматично обробляється** фреймворком. +* Усі залежності можуть вимагати дані із запитів і **розширювати обмеження операції шляху** та автоматичну документацію. +* **Автоматична валідація** навіть для параметрів *операції шляху*, визначених у залежностях. +* Підтримка складних систем автентифікації користувачів, **підключень до баз даних** тощо. +* **Жодних компромісів** із базами даних, фронтендами тощо. Але проста інтеграція з усіма ними. -### Немає обмежень на "плагіни" +### Необмежені «плагіни» { #unlimited-plug-ins } -Або іншими словами, вони не потрібні – просто імпортуйте та використовуйте необхідний код. +Інакше кажучи, вони не потрібні — імпортуйте та використовуйте код, який вам потрібен. -Будь-яка інтеграція спроєктована настільки просто (з використанням залежностей), що ви можете створити "плагін" для свого застосунку всього у 2 рядках коду, використовуючи ту саму структуру та синтаксис, що й для ваших *операцій шляху*. +Будь-яка інтеграція спроєктована так, щоб її було дуже просто використовувати (із залежностями), тож ви можете створити «плагін» для свого застосунку у 2 рядках коду, використовуючи ту саму структуру та синтаксис, що й для ваших *операцій шляху*. -### Протестовано +### Протестовано { #tested } * 100% <abbr title="Обсяг коду, що автоматично тестується">покриття тестами</abbr>. * 100% <abbr title="Анотації типів у Python, завдяки яким ваш редактор і зовнішні інструменти можуть надавати кращу підтримку">анотована типами</abbr> кодова база. -* Використовується у робочих середовищах. +* Використовується в production-застосунках. -## Можливості Starlette +## Можливості Starlette { #starlette-features } **FastAPI** повністю сумісний із (та побудований на основі) <a href="https://www.starlette.dev/" class="external-link" target="_blank"><strong>Starlette</strong></a>. Тому будь-який додатковий код Starlette, який ви маєте, також працюватиме. -**FastAPI** фактично є підкласом **Starlette**. Тому, якщо ви вже знайомі зі Starlette або використовуєте його, більшість функціональності працюватиме так само. +`FastAPI` фактично є підкласом `Starlette`. Тому, якщо ви вже знайомі зі Starlette або використовуєте його, більшість функціональності працюватиме так само. -З **FastAPI** ви отримуєте всі можливості **Starlette** (адже FastAPI — це, по суті, Starlette на стероїдах): +З **FastAPI** ви отримуєте всі можливості **Starlette** (адже FastAPI — це просто Starlette на стероїдах): -* Разюча продуктивність. Це <a href="https://github.com/encode/starlette#performance" class="external-link" target="_blank">один із найшвидших фреймворків на Python</a>, на рівні з **NodeJS** і **Go**. +* Разюча продуктивність. Це <a href="https://github.com/encode/starlette#performance" class="external-link" target="_blank">один із найшвидших доступних Python-фреймворків, на рівні з **NodeJS** і **Go**</a>. * Підтримка **WebSocket**. * Фонові задачі у процесі. * Події запуску та завершення роботи. @@ -163,27 +175,27 @@ FastAPI має розумні налаштування **за замовчува * 100% покриття тестами. * 100% анотована типами кодова база. -## Можливості Pydantic +## Можливості Pydantic { #pydantic-features } **FastAPI** повністю сумісний із (та побудований на основі) <a href="https://docs.pydantic.dev/" class="external-link" target="_blank"><strong>Pydantic</strong></a>. Тому будь-який додатковий код Pydantic, який ви маєте, також працюватиме. -Включаючи зовнішні бібліотеки, побудовані також на Pydantic, такі як <abbr title="Object-Relational Mapper">ORM</abbr>, <abbr title="Object-Document Mapper">ODM</abbr> для баз даних. +Включно із зовнішніми бібліотеками, які також базуються на Pydantic, як-от <abbr title="Object-Relational Mapper - Об'єктно-реляційний відображувач">ORM</abbr>-и, <abbr title="Object-Document Mapper - Об'єктно-документний відображувач">ODM</abbr>-и для баз даних. -Це також означає, що в багатьох випадках ви можете передати той самий об'єкт, який отримуєте з запиту, **безпосередньо в базу даних**, оскільки все автоматично перевіряється. +Це також означає, що в багатьох випадках ви можете передати той самий об'єкт, який отримуєте із запиту, **безпосередньо в базу даних**, оскільки все автоматично перевіряється. -Те ж саме відбувається й у зворотному напрямку — у багатьох випадках ви можете просто передати об'єкт, який отримуєте з бази даних, **безпосередньо клієнту**. +Те саме застосовується й у зворотному напрямку — у багатьох випадках ви можете просто передати об'єкт, який отримуєте з бази даних, **безпосередньо клієнту**. З **FastAPI** ви отримуєте всі можливості **Pydantic** (адже FastAPI базується на Pydantic для обробки всіх даних): -* **Ніякої плутанини** : - * Не потрібно вчити нову мову для визначення схем. +* **Ніякої плутанини**: + * Не потрібно вчити нову мікромову для визначення схем. * Якщо ви знаєте типи Python, ви знаєте, як використовувати Pydantic. -* Легко працює з вашим **<abbr title="Інтегроване середовище розробки, схоже на редактор коду">IDE</abbr>/<abbr title="Програма, яка перевіряє помилки в коді">лінтером</abbr>/мозком**: - * Оскільки структури даних Pydantic є просто екземплярами класів, які ви визначаєте; автодоповнення, лінтинг, mypy і ваша інтуїція повинні добре працювати з вашими перевіреними даними. -* Валідація **складних структур**: - * Використання ієрархічних моделей Pydantic. Python `typing`, `List` і `Dict` тощо. - * Валідатори дозволяють чітко і просто визначати, перевіряти й документувати складні схеми даних у вигляді JSON-схеми. - * Ви можете мати глибоко **вкладені JSON об'єкти** та перевірити та анотувати їх всі. +* Легко працює з вашим **<abbr title="Integrated Development Environment - Інтегроване середовище розробки: схоже на код-редактор">IDE</abbr>/<abbr title="Програма, що перевіряє код на помилки">linter</abbr>/мозком**: + * Оскільки структури даних pydantic є просто екземплярами класів, які ви визначаєте; автодоповнення, лінтинг, mypy і ваша інтуїція повинні добре працювати з вашими перевіреними даними. +* Валідує **складні структури**: + * Використання ієрархічних моделей Pydantic, Python `typing`’s `List` і `Dict` тощо. + * Валідатори дають змогу складні схеми даних чітко й просто визначати, перевіряти й документувати як JSON Schema. + * Ви можете мати глибоко **вкладені JSON** об'єкти, і всі вони будуть валідовані та анотовані. * **Розширюваність**: - * Pydantic дозволяє визначати користувацькі типи даних або розширювати валідацію методами в моделі декоратором `validator`. + * Pydantic дозволяє визначати користувацькі типи даних або ви можете розширити валідацію методами в моделі, позначеними декоратором validator. * 100% покриття тестами. From 07f08fc79a021cbc06d194f0a32f521253c6ed8c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 09:44:59 +0000 Subject: [PATCH 116/243] =?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 eb72d559d..699b96696 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,6 +19,7 @@ hide: ### Translations +* 🌐 Update translations for uk (update outdated, found by fixer tool). PR [#14739](https://github.com/fastapi/fastapi/pull/14739) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for tr (update-outdated). PR [#14745](https://github.com/fastapi/fastapi/pull/14745) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update `llm-prompt.md` for Korean language. PR [#14763](https://github.com/fastapi/fastapi/pull/14763) by [@seuthootDev](https://github.com/seuthootDev). * 🌐 Update translations for ko (update outdated, found by fixer tool). PR [#14738](https://github.com/fastapi/fastapi/pull/14738) by [@YuriiMotov](https://github.com/YuriiMotov). From 3626d764c13cb12414b2b1a254af075f2799c716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 2 Feb 2026 02:24:23 -0800 Subject: [PATCH 117/243] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Contributors=20and=20Translators=20(#14796)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- docs/en/data/contributors.yml | 18 ++-- docs/en/data/translation_reviewers.yml | 114 ++++++++++++++----------- docs/en/data/translators.yml | 43 +++++----- 3 files changed, 95 insertions(+), 80 deletions(-) diff --git a/docs/en/data/contributors.yml b/docs/en/data/contributors.yml index 0c144cd4c..41115ccbd 100644 --- a/docs/en/data/contributors.yml +++ b/docs/en/data/contributors.yml @@ -1,17 +1,17 @@ tiangolo: login: tiangolo - count: 857 + count: 871 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo dependabot: login: dependabot - count: 130 + count: 133 avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 url: https://github.com/apps/dependabot alejsdev: login: alejsdev count: 53 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=0facffe3abf87f57a1f05fa773d1119cc5c2f6a5&v=4 url: https://github.com/alejsdev pre-commit-ci: login: pre-commit-ci @@ -20,8 +20,8 @@ pre-commit-ci: url: https://github.com/apps/pre-commit-ci YuriiMotov: login: YuriiMotov - count: 36 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + count: 38 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 url: https://github.com/YuriiMotov github-actions: login: github-actions @@ -40,7 +40,7 @@ dmontagu: url: https://github.com/dmontagu svlandeg: login: svlandeg - count: 16 + count: 17 avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 url: https://github.com/svlandeg nilslindemann: @@ -126,7 +126,7 @@ hitrust: ShahriyarR: login: ShahriyarR count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/3852029?u=631b2ae59360ab380c524b32bc3d245aff1165af&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/3852029?u=2dc6402d9053ee53f7afc407089cbab21c68f21d&v=4 url: https://github.com/ShahriyarR adriangb: login: adriangb @@ -266,7 +266,7 @@ Nimitha-jagadeesha: lucaromagnoli: login: lucaromagnoli count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/38782977?u=15df02e806a2293af40ac619fba11dbe3c0c4fd4&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/38782977?u=a09a2e916625fa035f9dfa25ebc58e07aac8ec36&v=4 url: https://github.com/lucaromagnoli salmantec: login: salmantec @@ -521,7 +521,7 @@ s111d: estebanx64: login: estebanx64 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=1900887aeed268699e5ea6f3fb7db614f7b77cd3&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=812422ae5d6a4bc5ff331c901fc54f9ab3cecf5c&v=4 url: https://github.com/estebanx64 ndimares: login: ndimares diff --git a/docs/en/data/translation_reviewers.yml b/docs/en/data/translation_reviewers.yml index 62db8e805..3d321818f 100644 --- a/docs/en/data/translation_reviewers.yml +++ b/docs/en/data/translation_reviewers.yml @@ -10,12 +10,12 @@ Xewus: url: https://github.com/Xewus sodaMelon: login: sodaMelon - count: 127 + count: 128 avatarUrl: https://avatars.githubusercontent.com/u/66295123?u=be939db90f1119efee9e6110cc05066ff1f40f00&v=4 url: https://github.com/sodaMelon ceb10n: login: ceb10n - count: 117 + count: 119 avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 url: https://github.com/ceb10n tokusumi: @@ -25,7 +25,7 @@ tokusumi: url: https://github.com/tokusumi hard-coders: login: hard-coders - count: 96 + count: 102 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=78d12d1acdf853c817700145e73de7fd9e5d068b&v=4 url: https://github.com/hard-coders hasansezertasan: @@ -50,7 +50,7 @@ AlertRED: url: https://github.com/AlertRED tiangolo: login: tiangolo - count: 73 + count: 78 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo Alexandrhub: @@ -58,26 +58,31 @@ Alexandrhub: count: 68 avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 url: https://github.com/Alexandrhub +cassiobotaro: + login: cassiobotaro + count: 64 + avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=a08022b191ddbd0a6159b2981d9d878b6d5bb71f&v=4 + url: https://github.com/cassiobotaro waynerv: login: waynerv count: 63 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 url: https://github.com/waynerv -cassiobotaro: - login: cassiobotaro - count: 62 - avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=a08022b191ddbd0a6159b2981d9d878b6d5bb71f&v=4 - url: https://github.com/cassiobotaro +nilslindemann: + login: nilslindemann + count: 61 + avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 + url: https://github.com/nilslindemann mattwang44: login: mattwang44 count: 61 avatarUrl: https://avatars.githubusercontent.com/u/24987826?u=58e37fb3927b9124b458945ac4c97aa0f1062d85&v=4 url: https://github.com/mattwang44 -nilslindemann: - login: nilslindemann - count: 59 - avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 - url: https://github.com/nilslindemann +YuriiMotov: + login: YuriiMotov + count: 56 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 + url: https://github.com/YuriiMotov Laineyzhang55: login: Laineyzhang55 count: 48 @@ -88,26 +93,21 @@ Kludex: count: 47 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex -YuriiMotov: - login: YuriiMotov - count: 46 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov komtaki: login: komtaki count: 45 avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki +svlandeg: + login: svlandeg + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg rostik1410: login: rostik1410 count: 42 avatarUrl: https://avatars.githubusercontent.com/u/11443899?u=e26a635c2ba220467b308a326a579b8ccf4a8701&v=4 url: https://github.com/rostik1410 -svlandeg: - login: svlandeg - count: 42 - avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 - url: https://github.com/svlandeg alperiox: login: alperiox count: 42 @@ -136,7 +136,7 @@ JavierSanchezCastro: alejsdev: login: alejsdev count: 37 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=0facffe3abf87f57a1f05fa773d1119cc5c2f6a5&v=4 url: https://github.com/alejsdev mezgoodle: login: mezgoodle @@ -383,6 +383,11 @@ 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 +maru0123-2004: + login: maru0123-2004 + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/43961566?u=16ed8603a4d6a4665cb6c53a7aece6f31379b769&v=4 + url: https://github.com/maru0123-2004 JaeHyuckSa: login: JaeHyuckSa count: 16 @@ -418,11 +423,6 @@ mattkoehne: count: 14 avatarUrl: https://avatars.githubusercontent.com/u/80362153?v=4 url: https://github.com/mattkoehne -maru0123-2004: - login: maru0123-2004 - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/43961566?u=16ed8603a4d6a4665cb6c53a7aece6f31379b769&v=4 - url: https://github.com/maru0123-2004 jovicon: login: jovicon count: 13 @@ -458,6 +458,11 @@ wesinalves: count: 13 avatarUrl: https://avatars.githubusercontent.com/u/13563128?u=9eb17ed50645dd684bfec47e75dba4e9772ec9c1&v=4 url: https://github.com/wesinalves +andersonrocha0: + login: andersonrocha0 + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/22346169?u=93a1359c8c5461d894802c0cc65bcd09217e7a02&v=4 + url: https://github.com/andersonrocha0 NastasiaSaby: login: NastasiaSaby count: 12 @@ -471,7 +476,7 @@ oandersonmagalhaes: mkdir700: login: mkdir700 count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/56359329?u=3d6ea8714f5000829b60dcf7b13a75b1e73aaf47&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/56359329?u=818e5f4b4dcc1a6ffb3e5aaa08fd827e5a726dfd&v=4 url: https://github.com/mkdir700 batlopes: login: batlopes @@ -493,11 +498,6 @@ KaniKim: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/19832624?u=296dbdd490e0eb96e3d45a2608c065603b17dc31&v=4 url: https://github.com/KaniKim -andersonrocha0: - login: andersonrocha0 - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/22346169?u=93a1359c8c5461d894802c0cc65bcd09217e7a02&v=4 - url: https://github.com/andersonrocha0 gitgernit: login: gitgernit count: 12 @@ -558,6 +558,11 @@ Zhongheng-Cheng: count: 11 avatarUrl: https://avatars.githubusercontent.com/u/95612344?u=a0f7730a3cc7486827965e01a119ad610bda4b0a&v=4 url: https://github.com/Zhongheng-Cheng +Pyth3rEx: + login: Pyth3rEx + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/26427764?u=087724f74d813c95925d51e354554bd4b6d6bb60&v=4 + url: https://github.com/Pyth3rEx mariacamilagl: login: mariacamilagl count: 10 @@ -611,7 +616,7 @@ socket-socket: nick-cjyx9: login: nick-cjyx9 count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/119087246?u=7227a2de948c68fb8396d5beff1ee5b0e057c42e&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/119087246?u=3d51dcbd79222ecb6538642f31dc7c8bb708d191&v=4 url: https://github.com/nick-cjyx9 marcelomarkus: login: marcelomarkus @@ -683,11 +688,6 @@ Yarous: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/61277193?u=5b462347458a373b2d599c6f416d2b75eddbffad&v=4 url: https://github.com/Yarous -Pyth3rEx: - login: Pyth3rEx - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/26427764?u=087724f74d813c95925d51e354554bd4b6d6bb60&v=4 - url: https://github.com/Pyth3rEx dimaqq: login: dimaqq count: 8 @@ -736,7 +736,7 @@ minaton-ru: sungchan1: login: sungchan1 count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/28076127?u=a816d86ef3e60450a7225f128caf9a394c9320f9&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/28076127?u=fadbf24840186aca639d344bb3e0ecf7ff3441cf&v=4 url: https://github.com/sungchan1 Serrones: login: Serrones @@ -761,7 +761,7 @@ anthonycepeda: fabioueno: login: fabioueno count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/14273852?u=edd700982b16317ac6ebfd24c47bc0029b21d360&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/14273852?u=a3d546449cdc96621c32bcc26cf74be6e4390209&v=4 url: https://github.com/fabioueno cfraboulet: login: cfraboulet @@ -793,6 +793,11 @@ Zerohertz: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/42334717?u=5ebf4d33e73b1ad373154f6cdee44f7cab4d05ba&v=4 url: https://github.com/Zerohertz +EdmilsonRodrigues: + login: EdmilsonRodrigues + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4 + url: https://github.com/EdmilsonRodrigues deniscapeto: login: deniscapeto count: 6 @@ -1028,11 +1033,6 @@ devluisrodrigues: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/21125286?v=4 url: https://github.com/11kkw -EdmilsonRodrigues: - login: EdmilsonRodrigues - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4 - url: https://github.com/EdmilsonRodrigues lpdswing: login: lpdswing count: 4 @@ -1178,6 +1178,11 @@ SBillion: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/1070649?u=3ab493dfc88b39da0eb1600e3b8e7df1c90a5dee&v=4 url: https://github.com/SBillion +seuthootDev: + login: seuthootDev + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/175179350?u=7c2cbc48ab43b52e0c86592111d92e013d72ea4d&v=4 + url: https://github.com/seuthootDev tyronedamasceno: login: tyronedamasceno count: 3 @@ -1266,7 +1271,7 @@ rafsaf: frnsimoes: login: frnsimoes count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/66239468?u=fd8d408946633acc4bea057c207e6c0833871527&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/66239468?u=cba345870d8d6b25dd6d56ee18f7120581e3c573&v=4 url: https://github.com/frnsimoes lieryan: login: lieryan @@ -1593,6 +1598,11 @@ ayr-ton: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/1090517?u=5cf70a0e0f0dbf084e074e494aa94d7c91a46ba6&v=4 url: https://github.com/ayr-ton +Kadermiyanyedi: + login: Kadermiyanyedi + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/48386782?u=e34f31bf50a8ed8d37fbfa4f301b0c190b1b4b86&v=4 + url: https://github.com/Kadermiyanyedi raphaelauv: login: raphaelauv count: 2 @@ -1831,7 +1841,7 @@ EgorOnishchuk: iamantonreznik: login: iamantonreznik count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/112612414?u=bf6de9a1ab17326fe14de0709719fff3826526d0&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/112612414?u=b9ba8d9b4d3940198bc3a4353dfce70c044a39b1&v=4 url: https://github.com/iamantonreznik Azazul123: login: Azazul123 @@ -1851,7 +1861,7 @@ NavesSapnis: isgin01: login: isgin01 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=ddffde10876b50f35dc90d1337f507a630530a6a&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=16d6466476cf7dbc55a4cd575b6ea920ebdd81e1&v=4 url: https://github.com/isgin01 syedasamina56: login: syedasamina56 diff --git a/docs/en/data/translators.yml b/docs/en/data/translators.yml index 940b128da..dd5900a41 100644 --- a/docs/en/data/translators.yml +++ b/docs/en/data/translators.yml @@ -8,9 +8,14 @@ jaystone776: count: 46 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 url: https://github.com/jaystone776 +tiangolo: + login: tiangolo + count: 31 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo ceb10n: login: ceb10n - count: 29 + count: 30 avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 url: https://github.com/ceb10n valentinDruzhinin: @@ -28,11 +33,6 @@ SwftAlpc: count: 23 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 url: https://github.com/SwftAlpc -tiangolo: - login: tiangolo - count: 22 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo hasansezertasan: login: hasansezertasan count: 22 @@ -43,16 +43,16 @@ waynerv: count: 20 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 url: https://github.com/waynerv +hard-coders: + login: hard-coders + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=78d12d1acdf853c817700145e73de7fd9e5d068b&v=4 + url: https://github.com/hard-coders AlertRED: login: AlertRED count: 16 avatarUrl: https://avatars.githubusercontent.com/u/15695000?u=f5a4944c6df443030409c88da7d7fa0b7ead985c&v=4 url: https://github.com/AlertRED -hard-coders: - login: hard-coders - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=78d12d1acdf853c817700145e73de7fd9e5d068b&v=4 - url: https://github.com/hard-coders Joao-Pedro-P-Holanda: login: Joao-Pedro-P-Holanda count: 14 @@ -108,6 +108,11 @@ pablocm83: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/28315068?u=3310fbb05bb8bfc50d2c48b6cb64ac9ee4a14549&v=4 url: https://github.com/pablocm83 +YuriiMotov: + login: YuriiMotov + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 + url: https://github.com/YuriiMotov ptt3199: login: ptt3199 count: 7 @@ -133,11 +138,6 @@ Alexandrhub: count: 6 avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 url: https://github.com/Alexandrhub -YuriiMotov: - login: YuriiMotov - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov Serrones: login: Serrones count: 5 @@ -291,7 +291,7 @@ hsuanchi: alejsdev: login: alejsdev count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=0facffe3abf87f57a1f05fa773d1119cc5c2f6a5&v=4 url: https://github.com/alejsdev riroan: login: riroan @@ -361,7 +361,7 @@ Rishat-F: ruzia: login: ruzia count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/24503?v=4 + avatarUrl: https://avatars.githubusercontent.com/u/24503?u=abce66d26c9611818720f11e6ae6773a6e0928f8&v=4 url: https://github.com/ruzia izaguerreiro: login: izaguerreiro @@ -413,6 +413,11 @@ ayr-ton: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/1090517?u=5cf70a0e0f0dbf084e074e494aa94d7c91a46ba6&v=4 url: https://github.com/ayr-ton +Kadermiyanyedi: + login: Kadermiyanyedi + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/48386782?u=e34f31bf50a8ed8d37fbfa4f301b0c190b1b4b86&v=4 + url: https://github.com/Kadermiyanyedi KdHyeon0661: login: KdHyeon0661 count: 2 @@ -461,7 +466,7 @@ ArtemKhymenko: hasnatsajid: login: hasnatsajid count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/86589885?u=6668823c3b029bfecf10a8918ed3af1aafb8b15e&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/86589885?v=4 url: https://github.com/hasnatsajid alperiox: login: alperiox From 779f7ddc37e1e03e199cf5b243e4d5d3c5f7c4e8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:24:48 +0000 Subject: [PATCH 118/243] =?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 699b96696..0fe3eebf9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -41,6 +41,7 @@ hide: ### Internal +* 👥 Update FastAPI People - Contributors and Translators. PR [#14796](https://github.com/fastapi/fastapi/pull/14796) by [@tiangolo](https://github.com/tiangolo). * 🔧 Ensure that an edit to `uv.lock` gets the `internal` label. PR [#14759](https://github.com/fastapi/fastapi/pull/14759) by [@svlandeg](https://github.com/svlandeg). * 🔧 Update sponsors: remove Requestly. PR [#14735](https://github.com/fastapi/fastapi/pull/14735) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, LambdaTest changes to TestMu AI. PR [#14734](https://github.com/fastapi/fastapi/pull/14734) by [@tiangolo](https://github.com/tiangolo). From 20ff394d7540dd560df1e9252584e3965d59b0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 2 Feb 2026 02:26:58 -0800 Subject: [PATCH 119/243] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20GitHu?= =?UTF-8?q?b=20topic=20repositories=20(#14803)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- docs/en/data/topic_repos.yml | 336 +++++++++++++++++------------------ 1 file changed, 168 insertions(+), 168 deletions(-) diff --git a/docs/en/data/topic_repos.yml b/docs/en/data/topic_repos.yml index d089c7e5a..a37cb6dcf 100644 --- a/docs/en/data/topic_repos.yml +++ b/docs/en/data/topic_repos.yml @@ -1,176 +1,181 @@ - name: full-stack-fastapi-template html_url: https://github.com/fastapi/full-stack-fastapi-template - stars: 40334 + stars: 41312 owner_login: fastapi owner_html_url: https://github.com/fastapi - name: Hello-Python html_url: https://github.com/mouredev/Hello-Python - stars: 33628 + stars: 34206 owner_login: mouredev owner_html_url: https://github.com/mouredev - name: serve html_url: https://github.com/jina-ai/serve - stars: 21817 + stars: 21832 owner_login: jina-ai owner_html_url: https://github.com/jina-ai - name: HivisionIDPhotos html_url: https://github.com/Zeyi-Lin/HivisionIDPhotos - stars: 20409 + stars: 20661 owner_login: Zeyi-Lin owner_html_url: https://github.com/Zeyi-Lin - name: sqlmodel html_url: https://github.com/fastapi/sqlmodel - stars: 17415 + stars: 17567 owner_login: fastapi owner_html_url: https://github.com/fastapi - name: fastapi-best-practices html_url: https://github.com/zhanymkanov/fastapi-best-practices - stars: 15776 + stars: 16291 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov - name: Douyin_TikTok_Download_API html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API - stars: 15588 + stars: 16132 owner_login: Evil0ctal owner_html_url: https://github.com/Evil0ctal -- name: machine-learning-zoomcamp - html_url: https://github.com/DataTalksClub/machine-learning-zoomcamp - stars: 12447 - owner_login: DataTalksClub - owner_html_url: https://github.com/DataTalksClub - name: SurfSense html_url: https://github.com/MODSetter/SurfSense - stars: 12128 + stars: 12723 owner_login: MODSetter owner_html_url: https://github.com/MODSetter +- name: machine-learning-zoomcamp + html_url: https://github.com/DataTalksClub/machine-learning-zoomcamp + stars: 12575 + owner_login: DataTalksClub + owner_html_url: https://github.com/DataTalksClub - name: fastapi_mcp html_url: https://github.com/tadata-org/fastapi_mcp - stars: 11326 + stars: 11478 owner_login: tadata-org owner_html_url: https://github.com/tadata-org - name: awesome-fastapi html_url: https://github.com/mjhea0/awesome-fastapi - stars: 10901 + stars: 11018 owner_login: mjhea0 owner_html_url: https://github.com/mjhea0 - name: XHS-Downloader html_url: https://github.com/JoeanAmier/XHS-Downloader - stars: 9584 + stars: 9938 owner_login: JoeanAmier owner_html_url: https://github.com/JoeanAmier - name: polar html_url: https://github.com/polarsource/polar - stars: 8951 + stars: 9348 owner_login: polarsource owner_html_url: https://github.com/polarsource - name: FastUI html_url: https://github.com/pydantic/FastUI - stars: 8934 + stars: 8949 owner_login: pydantic owner_html_url: https://github.com/pydantic - name: FileCodeBox html_url: https://github.com/vastsa/FileCodeBox - stars: 7934 + stars: 8060 owner_login: vastsa owner_html_url: https://github.com/vastsa - name: nonebot2 html_url: https://github.com/nonebot/nonebot2 - stars: 7248 + stars: 7311 owner_login: nonebot owner_html_url: https://github.com/nonebot - name: hatchet html_url: https://github.com/hatchet-dev/hatchet - stars: 6392 + stars: 6479 owner_login: hatchet-dev owner_html_url: https://github.com/hatchet-dev - name: fastapi-users html_url: https://github.com/fastapi-users/fastapi-users - stars: 5899 + stars: 5970 owner_login: fastapi-users owner_html_url: https://github.com/fastapi-users - name: serge html_url: https://github.com/serge-chat/serge - stars: 5754 + stars: 5751 owner_login: serge-chat owner_html_url: https://github.com/serge-chat - name: strawberry html_url: https://github.com/strawberry-graphql/strawberry - stars: 4577 + stars: 4598 owner_login: strawberry-graphql owner_html_url: https://github.com/strawberry-graphql +- name: devpush + html_url: https://github.com/hunvreus/devpush + stars: 4407 + owner_login: hunvreus + owner_html_url: https://github.com/hunvreus +- name: Kokoro-FastAPI + html_url: https://github.com/remsky/Kokoro-FastAPI + stars: 4359 + owner_login: remsky + owner_html_url: https://github.com/remsky - name: poem html_url: https://github.com/poem-web/poem - stars: 4303 + stars: 4337 owner_login: poem-web owner_html_url: https://github.com/poem-web - name: chatgpt-web-share html_url: https://github.com/chatpire/chatgpt-web-share - stars: 4287 + stars: 4279 owner_login: chatpire owner_html_url: https://github.com/chatpire - name: dynaconf html_url: https://github.com/dynaconf/dynaconf - stars: 4221 + stars: 4244 owner_login: dynaconf owner_html_url: https://github.com/dynaconf -- name: Kokoro-FastAPI - html_url: https://github.com/remsky/Kokoro-FastAPI - stars: 4181 - owner_login: remsky - owner_html_url: https://github.com/remsky +- name: Yuxi-Know + html_url: https://github.com/xerrors/Yuxi-Know + stars: 4154 + owner_login: xerrors + owner_html_url: https://github.com/xerrors - name: atrilabs-engine html_url: https://github.com/Atri-Labs/atrilabs-engine - stars: 4090 + stars: 4086 owner_login: Atri-Labs owner_html_url: https://github.com/Atri-Labs -- name: devpush - html_url: https://github.com/hunvreus/devpush - stars: 4037 - owner_login: hunvreus - owner_html_url: https://github.com/hunvreus - name: logfire html_url: https://github.com/pydantic/logfire - stars: 3896 + stars: 3975 owner_login: pydantic owner_html_url: https://github.com/pydantic - name: LitServe html_url: https://github.com/Lightning-AI/LitServe - stars: 3756 + stars: 3797 owner_login: Lightning-AI owner_html_url: https://github.com/Lightning-AI - name: huma html_url: https://github.com/danielgtaylor/huma - stars: 3702 + stars: 3785 owner_login: danielgtaylor owner_html_url: https://github.com/danielgtaylor -- name: Yuxi-Know - html_url: https://github.com/xerrors/Yuxi-Know - stars: 3680 - owner_login: xerrors - owner_html_url: https://github.com/xerrors - name: datamodel-code-generator html_url: https://github.com/koxudaxi/datamodel-code-generator - stars: 3675 + stars: 3731 owner_login: koxudaxi owner_html_url: https://github.com/koxudaxi - name: fastapi-admin html_url: https://github.com/fastapi-admin/fastapi-admin - stars: 3659 + stars: 3697 owner_login: fastapi-admin owner_html_url: https://github.com/fastapi-admin - name: farfalle html_url: https://github.com/rashadphz/farfalle - stars: 3497 + stars: 3506 owner_login: rashadphz owner_html_url: https://github.com/rashadphz - name: tracecat html_url: https://github.com/TracecatHQ/tracecat - stars: 3421 + stars: 3458 owner_login: TracecatHQ owner_html_url: https://github.com/TracecatHQ +- name: mcp-context-forge + html_url: https://github.com/IBM/mcp-context-forge + stars: 3216 + owner_login: IBM + owner_html_url: https://github.com/IBM - name: opyrator html_url: https://github.com/ml-tooling/opyrator - stars: 3136 + stars: 3134 owner_login: ml-tooling owner_html_url: https://github.com/ml-tooling - name: docarray @@ -180,316 +185,311 @@ owner_html_url: https://github.com/docarray - name: fastapi-realworld-example-app html_url: https://github.com/nsidnev/fastapi-realworld-example-app - stars: 3051 + stars: 3072 owner_login: nsidnev owner_html_url: https://github.com/nsidnev -- name: mcp-context-forge - html_url: https://github.com/IBM/mcp-context-forge - stars: 3034 - owner_login: IBM - owner_html_url: https://github.com/IBM - name: uvicorn-gunicorn-fastapi-docker html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker - stars: 2904 + stars: 2908 owner_login: tiangolo owner_html_url: https://github.com/tiangolo - name: FastAPI-template html_url: https://github.com/s3rius/FastAPI-template - stars: 2680 + stars: 2728 owner_login: s3rius owner_html_url: https://github.com/s3rius - name: best-of-web-python html_url: https://github.com/ml-tooling/best-of-web-python - stars: 2662 + stars: 2686 owner_login: ml-tooling owner_html_url: https://github.com/ml-tooling - name: YC-Killer html_url: https://github.com/sahibzada-allahyar/YC-Killer - stars: 2614 + stars: 2648 owner_login: sahibzada-allahyar owner_html_url: https://github.com/sahibzada-allahyar - name: sqladmin html_url: https://github.com/aminalaee/sqladmin - stars: 2587 + stars: 2637 owner_login: aminalaee owner_html_url: https://github.com/aminalaee - name: fastapi-react html_url: https://github.com/Buuntu/fastapi-react - stars: 2566 + stars: 2573 owner_login: Buuntu owner_html_url: https://github.com/Buuntu - name: RasaGPT html_url: https://github.com/paulpierre/RasaGPT - stars: 2456 + stars: 2460 owner_login: paulpierre owner_html_url: https://github.com/paulpierre - name: supabase-py html_url: https://github.com/supabase/supabase-py - stars: 2394 + stars: 2428 owner_login: supabase owner_html_url: https://github.com/supabase +- name: 30-Days-of-Python + html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python + stars: 2347 + owner_login: codingforentrepreneurs + owner_html_url: https://github.com/codingforentrepreneurs - name: nextpy html_url: https://github.com/dot-agent/nextpy - stars: 2338 + stars: 2337 owner_login: dot-agent owner_html_url: https://github.com/dot-agent - name: fastapi-utils html_url: https://github.com/fastapiutils/fastapi-utils - stars: 2289 + stars: 2299 owner_login: fastapiutils owner_html_url: https://github.com/fastapiutils - name: langserve html_url: https://github.com/langchain-ai/langserve - stars: 2234 + stars: 2255 owner_login: langchain-ai owner_html_url: https://github.com/langchain-ai -- name: 30-Days-of-Python - html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python - stars: 2232 - owner_login: codingforentrepreneurs - owner_html_url: https://github.com/codingforentrepreneurs +- name: NoteDiscovery + html_url: https://github.com/gamosoft/NoteDiscovery + stars: 2182 + owner_login: gamosoft + owner_html_url: https://github.com/gamosoft - name: solara html_url: https://github.com/widgetti/solara - stars: 2141 + stars: 2154 owner_login: widgetti owner_html_url: https://github.com/widgetti - name: mangum html_url: https://github.com/Kludex/mangum - stars: 2046 + stars: 2071 owner_login: Kludex owner_html_url: https://github.com/Kludex - name: fastapi_best_architecture html_url: https://github.com/fastapi-practices/fastapi_best_architecture - stars: 1963 + stars: 2036 owner_login: fastapi-practices owner_html_url: https://github.com/fastapi-practices -- name: NoteDiscovery - html_url: https://github.com/gamosoft/NoteDiscovery - stars: 1943 - owner_login: gamosoft - owner_html_url: https://github.com/gamosoft -- name: agentkit - html_url: https://github.com/BCG-X-Official/agentkit - stars: 1936 - owner_login: BCG-X-Official - owner_html_url: https://github.com/BCG-X-Official - name: vue-fastapi-admin html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin - stars: 1909 + stars: 1983 owner_login: mizhexiaoxiao owner_html_url: https://github.com/mizhexiaoxiao -- name: manage-fastapi - html_url: https://github.com/ycd/manage-fastapi - stars: 1887 - owner_login: ycd - owner_html_url: https://github.com/ycd +- name: agentkit + html_url: https://github.com/BCG-X-Official/agentkit + stars: 1941 + owner_login: BCG-X-Official + owner_html_url: https://github.com/BCG-X-Official +- name: fastapi-langgraph-agent-production-ready-template + html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template + stars: 1920 + owner_login: wassim249 + owner_html_url: https://github.com/wassim249 - name: openapi-python-client html_url: https://github.com/openapi-generators/openapi-python-client - stars: 1879 + stars: 1900 owner_login: openapi-generators owner_html_url: https://github.com/openapi-generators +- name: manage-fastapi + html_url: https://github.com/ycd/manage-fastapi + stars: 1894 + owner_login: ycd + owner_html_url: https://github.com/ycd - name: slowapi html_url: https://github.com/laurentS/slowapi - stars: 1845 + stars: 1891 owner_login: laurentS owner_html_url: https://github.com/laurentS - name: piccolo html_url: https://github.com/piccolo-orm/piccolo - stars: 1843 + stars: 1854 owner_login: piccolo-orm owner_html_url: https://github.com/piccolo-orm +- name: fastapi-cache + html_url: https://github.com/long2ice/fastapi-cache + stars: 1816 + owner_login: long2ice + owner_html_url: https://github.com/long2ice - name: python-week-2022 html_url: https://github.com/rochacbruno/python-week-2022 stars: 1813 owner_login: rochacbruno owner_html_url: https://github.com/rochacbruno -- name: fastapi-cache - html_url: https://github.com/long2ice/fastapi-cache - stars: 1805 - owner_login: long2ice - owner_html_url: https://github.com/long2ice - name: ormar html_url: https://github.com/collerek/ormar - stars: 1785 + stars: 1797 owner_login: collerek owner_html_url: https://github.com/collerek -- name: fastapi-langgraph-agent-production-ready-template - html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template - stars: 1780 - owner_login: wassim249 - owner_html_url: https://github.com/wassim249 - name: FastAPI-boilerplate html_url: https://github.com/benavlabs/FastAPI-boilerplate - stars: 1734 + stars: 1792 owner_login: benavlabs owner_html_url: https://github.com/benavlabs - name: termpair html_url: https://github.com/cs01/termpair - stars: 1724 + stars: 1727 owner_login: cs01 owner_html_url: https://github.com/cs01 - name: fastapi-crudrouter html_url: https://github.com/awtkns/fastapi-crudrouter - stars: 1671 + stars: 1677 owner_login: awtkns owner_html_url: https://github.com/awtkns - name: langchain-serve html_url: https://github.com/jina-ai/langchain-serve - stars: 1633 + stars: 1634 owner_login: jina-ai owner_html_url: https://github.com/jina-ai - name: fastapi-pagination html_url: https://github.com/uriyyo/fastapi-pagination - stars: 1588 + stars: 1607 owner_login: uriyyo owner_html_url: https://github.com/uriyyo - name: awesome-fastapi-projects html_url: https://github.com/Kludex/awesome-fastapi-projects - stars: 1583 + stars: 1592 owner_login: Kludex owner_html_url: https://github.com/Kludex -- name: coronavirus-tracker-api - html_url: https://github.com/ExpDev07/coronavirus-tracker-api - stars: 1571 - owner_login: ExpDev07 - owner_html_url: https://github.com/ExpDev07 - name: bracket html_url: https://github.com/evroon/bracket - stars: 1549 + stars: 1580 owner_login: evroon owner_html_url: https://github.com/evroon +- name: coronavirus-tracker-api + html_url: https://github.com/ExpDev07/coronavirus-tracker-api + stars: 1570 + owner_login: ExpDev07 + owner_html_url: https://github.com/ExpDev07 - name: fastapi-amis-admin html_url: https://github.com/amisadmin/fastapi-amis-admin - stars: 1491 + stars: 1512 owner_login: amisadmin owner_html_url: https://github.com/amisadmin -- name: fastapi-boilerplate - html_url: https://github.com/teamhide/fastapi-boilerplate - stars: 1452 - owner_login: teamhide - owner_html_url: https://github.com/teamhide - name: fastcrud html_url: https://github.com/benavlabs/fastcrud - stars: 1452 + stars: 1471 owner_login: benavlabs owner_html_url: https://github.com/benavlabs +- name: fastapi-boilerplate + html_url: https://github.com/teamhide/fastapi-boilerplate + stars: 1461 + owner_login: teamhide + owner_html_url: https://github.com/teamhide - name: awesome-python-resources html_url: https://github.com/DjangoEx/awesome-python-resources - stars: 1430 + stars: 1435 owner_login: DjangoEx owner_html_url: https://github.com/DjangoEx - name: prometheus-fastapi-instrumentator html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator - stars: 1399 + stars: 1417 owner_login: trallnag owner_html_url: https://github.com/trallnag - name: fastapi-code-generator html_url: https://github.com/koxudaxi/fastapi-code-generator - stars: 1371 + stars: 1382 owner_login: koxudaxi owner_html_url: https://github.com/koxudaxi +- name: fastapi-scaff + html_url: https://github.com/atpuxiner/fastapi-scaff + stars: 1367 + owner_login: atpuxiner + owner_html_url: https://github.com/atpuxiner - name: fastapi-tutorial html_url: https://github.com/liaogx/fastapi-tutorial - stars: 1346 + stars: 1360 owner_login: liaogx owner_html_url: https://github.com/liaogx - name: budgetml html_url: https://github.com/ebhy/budgetml - stars: 1345 + stars: 1343 owner_login: ebhy owner_html_url: https://github.com/ebhy -- name: fastapi-scaff - html_url: https://github.com/atpuxiner/fastapi-scaff - stars: 1331 - owner_login: atpuxiner - owner_html_url: https://github.com/atpuxiner - name: bolt-python html_url: https://github.com/slackapi/bolt-python - stars: 1266 + stars: 1276 owner_login: slackapi owner_html_url: https://github.com/slackapi - name: bedrock-chat html_url: https://github.com/aws-samples/bedrock-chat - stars: 1266 + stars: 1268 owner_login: aws-samples owner_html_url: https://github.com/aws-samples - name: fastapi-alembic-sqlmodel-async - html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async - stars: 1260 - owner_login: jonra1993 - owner_html_url: https://github.com/jonra1993 + html_url: https://github.com/vargasjona/fastapi-alembic-sqlmodel-async + stars: 1265 + owner_login: vargasjona + owner_html_url: https://github.com/vargasjona - name: fastapi_production_template html_url: https://github.com/zhanymkanov/fastapi_production_template - stars: 1222 + stars: 1227 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov -- name: langchain-extract - html_url: https://github.com/langchain-ai/langchain-extract - stars: 1179 - owner_login: langchain-ai - owner_html_url: https://github.com/langchain-ai - name: restish html_url: https://github.com/rest-sh/restish - stars: 1152 + stars: 1200 owner_login: rest-sh owner_html_url: https://github.com/rest-sh +- name: langchain-extract + html_url: https://github.com/langchain-ai/langchain-extract + stars: 1183 + owner_login: langchain-ai + owner_html_url: https://github.com/langchain-ai - name: odmantic html_url: https://github.com/art049/odmantic - stars: 1143 + stars: 1162 owner_login: art049 owner_html_url: https://github.com/art049 -- name: authx - html_url: https://github.com/yezz123/authx - stars: 1128 - owner_login: yezz123 - owner_html_url: https://github.com/yezz123 -- name: SAG - html_url: https://github.com/Zleap-AI/SAG - stars: 1104 - owner_login: Zleap-AI - owner_html_url: https://github.com/Zleap-AI - name: aktools html_url: https://github.com/akfamily/aktools - stars: 1072 + stars: 1155 owner_login: akfamily owner_html_url: https://github.com/akfamily - name: RuoYi-Vue3-FastAPI html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI - stars: 1063 + stars: 1155 owner_login: insistence owner_html_url: https://github.com/insistence +- name: authx + html_url: https://github.com/yezz123/authx + stars: 1142 + owner_login: yezz123 + owner_html_url: https://github.com/yezz123 +- name: SAG + html_url: https://github.com/Zleap-AI/SAG + stars: 1110 + owner_login: Zleap-AI + owner_html_url: https://github.com/Zleap-AI - name: flock html_url: https://github.com/Onelevenvy/flock - stars: 1059 + stars: 1069 owner_login: Onelevenvy owner_html_url: https://github.com/Onelevenvy - name: fastapi-observability html_url: https://github.com/blueswen/fastapi-observability - stars: 1046 + stars: 1063 owner_login: blueswen owner_html_url: https://github.com/blueswen - name: enterprise-deep-research html_url: https://github.com/SalesforceAIResearch/enterprise-deep-research - stars: 1019 + stars: 1061 owner_login: SalesforceAIResearch owner_html_url: https://github.com/SalesforceAIResearch - name: titiler html_url: https://github.com/developmentseed/titiler - stars: 1016 + stars: 1039 owner_login: developmentseed owner_html_url: https://github.com/developmentseed - name: every-pdf html_url: https://github.com/DDULDDUCK/every-pdf - stars: 1004 + stars: 1017 owner_login: DDULDDUCK owner_html_url: https://github.com/DDULDDUCK - name: autollm html_url: https://github.com/viddexa/autollm - stars: 1003 + stars: 1005 owner_login: viddexa owner_html_url: https://github.com/viddexa - name: lanarky html_url: https://github.com/ajndkr/lanarky - stars: 996 + stars: 995 owner_login: ajndkr owner_html_url: https://github.com/ajndkr From 6173733200c3498408942a86792871b245dec032 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:27:24 +0000 Subject: [PATCH 120/243] =?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 0fe3eebf9..8f8edd5f6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -41,6 +41,7 @@ hide: ### Internal +* 👥 Update FastAPI GitHub topic repositories. PR [#14803](https://github.com/fastapi/fastapi/pull/14803) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Contributors and Translators. PR [#14796](https://github.com/fastapi/fastapi/pull/14796) by [@tiangolo](https://github.com/tiangolo). * 🔧 Ensure that an edit to `uv.lock` gets the `internal` label. PR [#14759](https://github.com/fastapi/fastapi/pull/14759) by [@svlandeg](https://github.com/svlandeg). * 🔧 Update sponsors: remove Requestly. PR [#14735](https://github.com/fastapi/fastapi/pull/14735) by [@tiangolo](https://github.com/tiangolo). From b833e53ade85dcb330145ac6e95a5b7c74a78fcd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:54:49 +0100 Subject: [PATCH 121/243] =?UTF-8?q?=E2=AC=86=20Bump=20typer=20from=200.16.?= =?UTF-8?q?0=20to=200.21.1=20(#14799)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [typer](https://github.com/fastapi/typer) from 0.16.0 to 0.21.1. - [Release notes](https://github.com/fastapi/typer/releases) - [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/fastapi/typer/compare/0.16.0...0.21.1) --- updated-dependencies: - dependency-name: typer dependency-version: 0.21.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem <svlandeg@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fe9b3a7ea..b104eed80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,7 +147,7 @@ docs = [ "pillow==11.3.0", "python-slugify==8.0.4", "pyyaml>=5.3.1,<7.0.0", - "typer==0.16.0", + "typer==0.21.1", ] docs-tests = [ "httpx>=0.23.0,<1.0.0", diff --git a/uv.lock b/uv.lock index 9ae2220e0..5cdf5f767 100644 --- a/uv.lock +++ b/uv.lock @@ -1230,7 +1230,7 @@ dev = [ { name = "ruff", specifier = "==0.14.3" }, { name = "sqlmodel", specifier = "==0.0.27" }, { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, - { name = "typer", specifier = "==0.16.0" }, + { name = "typer", specifier = "==0.21.1" }, { name = "types-orjson", specifier = "==3.6.2" }, { name = "types-ujson", specifier = "==5.10.0.20240515" }, ] @@ -1251,7 +1251,7 @@ docs = [ { name = "python-slugify", specifier = "==8.0.4" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, { name = "ruff", specifier = "==0.14.3" }, - { name = "typer", specifier = "==0.16.0" }, + { name = "typer", specifier = "==0.21.1" }, ] docs-tests = [ { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, @@ -4737,7 +4737,7 @@ wheels = [ [[package]] name = "typer" -version = "0.16.0" +version = "0.21.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -4746,9 +4746,9 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, ] [[package]] From da4083c30fcd1f4e2a4cc4bb5bb286545d124788 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:55:12 +0000 Subject: [PATCH 122/243] =?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 8f8edd5f6..afe456a3f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -41,6 +41,7 @@ hide: ### Internal +* ⬆ Bump typer from 0.16.0 to 0.21.1. PR [#14799](https://github.com/fastapi/fastapi/pull/14799) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👥 Update FastAPI GitHub topic repositories. PR [#14803](https://github.com/fastapi/fastapi/pull/14803) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Contributors and Translators. PR [#14796](https://github.com/fastapi/fastapi/pull/14796) by [@tiangolo](https://github.com/tiangolo). * 🔧 Ensure that an edit to `uv.lock` gets the `internal` label. PR [#14759](https://github.com/fastapi/fastapi/pull/14759) by [@svlandeg](https://github.com/svlandeg). From a0e34c747340b5b731a363f6b683fa0d0062ca49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:56:33 +0100 Subject: [PATCH 123/243] =?UTF-8?q?=E2=AC=86=20Bump=20gitpython=20from=203?= =?UTF-8?q?.1.45=20to=203.1.46=20(#14800)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.45 to 3.1.46. - [Release notes](https://github.com/gitpython-developers/GitPython/releases) - [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES) - [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.45...3.1.46) --- updated-dependencies: - dependency-name: gitpython dependency-version: 3.1.46 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem <svlandeg@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b104eed80..39a6bd155 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,7 +180,7 @@ tests = [ "types-ujson==5.10.0.20240515", ] translations = [ - "gitpython==3.1.45", + "gitpython==3.1.46", "pydantic-ai==0.4.10", "pygithub==2.8.1", ] diff --git a/uv.lock b/uv.lock index 5cdf5f767..f3a5f8e6d 100644 --- a/uv.lock +++ b/uv.lock @@ -1203,7 +1203,7 @@ dev = [ { name = "coverage", extras = ["toml"], specifier = ">=6.5.0,<8.0" }, { name = "dirty-equals", specifier = "==0.9.0" }, { name = "flask", specifier = ">=1.1.2,<4.0.0" }, - { name = "gitpython", specifier = "==3.1.45" }, + { name = "gitpython", specifier = "==3.1.46" }, { name = "griffe-typingdoc", specifier = "==0.3.0" }, { name = "griffe-warnings-deprecated", specifier = "==1.1.0" }, { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, @@ -1285,7 +1285,7 @@ tests = [ { name = "types-ujson", specifier = "==5.10.0.20240515" }, ] translations = [ - { name = "gitpython", specifier = "==3.1.45" }, + { name = "gitpython", specifier = "==3.1.46" }, { name = "pydantic-ai", specifier = "==0.4.10" }, { name = "pygithub", specifier = "==2.8.1" }, ] @@ -1632,15 +1632,15 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.45" +version = "3.1.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, + { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, ] [[package]] From f2487ce88c4ae8f7e64668409770427b245c3a60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:57:22 +0100 Subject: [PATCH 124/243] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-macros-plugin?= =?UTF-8?q?=20from=201.4.1=20to=201.5.0=20(#14801)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-macros-plugin](https://github.com/fralau/mkdocs_macros_plugin) from 1.4.1 to 1.5.0. - [Release notes](https://github.com/fralau/mkdocs_macros_plugin/releases) - [Changelog](https://github.com/fralau/mkdocs-macros-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/fralau/mkdocs_macros_plugin/compare/v1.4.1...v1.5.0) --- updated-dependencies: - dependency-name: mkdocs-macros-plugin dependency-version: 1.5.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem <svlandeg@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 39a6bd155..524d52718 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,7 +140,7 @@ docs = [ "jieba==0.42.1", "markdown-include-variants==0.0.8", "mdx-include>=1.4.1,<2.0.0", - "mkdocs-macros-plugin==1.4.1", + "mkdocs-macros-plugin==1.5.0", "mkdocs-material==9.7.0", "mkdocs-redirects>=1.2.1,<1.3.0", "mkdocstrings[python]==0.30.1", diff --git a/uv.lock b/uv.lock index f3a5f8e6d..312181b4e 100644 --- a/uv.lock +++ b/uv.lock @@ -1211,7 +1211,7 @@ dev = [ { name = "jieba", specifier = "==0.42.1" }, { name = "markdown-include-variants", specifier = "==0.0.8" }, { name = "mdx-include", specifier = ">=1.4.1,<2.0.0" }, - { name = "mkdocs-macros-plugin", specifier = "==1.4.1" }, + { name = "mkdocs-macros-plugin", specifier = "==1.5.0" }, { name = "mkdocs-material", specifier = "==9.7.0" }, { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, { name = "mkdocstrings", extras = ["python"], specifier = "==0.30.1" }, @@ -1243,7 +1243,7 @@ docs = [ { name = "jieba", specifier = "==0.42.1" }, { name = "markdown-include-variants", specifier = "==0.0.8" }, { name = "mdx-include", specifier = ">=1.4.1,<2.0.0" }, - { name = "mkdocs-macros-plugin", specifier = "==1.4.1" }, + { name = "mkdocs-macros-plugin", specifier = "==1.5.0" }, { name = "mkdocs-material", specifier = "==9.7.0" }, { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, { name = "mkdocstrings", extras = ["python"], specifier = "==0.30.1" }, @@ -2652,7 +2652,7 @@ wheels = [ [[package]] name = "mkdocs-macros-plugin" -version = "1.4.1" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "hjson" }, @@ -2667,9 +2667,9 @@ dependencies = [ { name = "termcolor", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "termcolor", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/42/bb2ceed148c77f82b57c6d3b7f584f0f34ababf7a9a8ff85809380d1f400/mkdocs_macros_plugin-1.4.1.tar.gz", hash = "sha256:55a9c93871e3744cdeb0736316783d60830a6d5d97b1132364e6b491607f2332", size = 35094, upload-time = "2025-10-25T12:37:20.689Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/15/e6a44839841ebc9c5872fa0e6fad1c3757424e4fe026093b68e9f386d136/mkdocs_macros_plugin-1.5.0.tar.gz", hash = "sha256:12aa45ce7ecb7a445c66b9f649f3dd05e9b92e8af6bc65e4acd91d26f878c01f", size = 37730, upload-time = "2025-11-13T08:08:55.545Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/ec/e6e96a7ae8df414f03f43681821234b0d3b86666f7b91f70ab26775a8809/mkdocs_macros_plugin-1.4.1-py3-none-any.whl", hash = "sha256:5a9e483f6056fe7ad0923802affe699233ca468672e20a9640dba237165b3240", size = 40155, upload-time = "2025-10-25T12:37:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/51/62/9fffba5bb9ed3d31a932ad35038ba9483d59850256ee0fea7f1187173983/mkdocs_macros_plugin-1.5.0-py3-none-any.whl", hash = "sha256:c10fabd812bf50f9170609d0ed518e54f1f0e12c334ac29141723a83c881dd6f", size = 44626, upload-time = "2025-11-13T08:08:53.878Z" }, ] [[package]] From 5c3f0307ae475fd0ceeb47aaa2404119f2b6da8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:57:43 +0100 Subject: [PATCH 125/243] =?UTF-8?q?=E2=AC=86=20Bump=20sqlmodel=20from=200.?= =?UTF-8?q?0.27=20to=200.0.31=20(#14802)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [sqlmodel](https://github.com/fastapi/sqlmodel) from 0.0.27 to 0.0.31. - [Release notes](https://github.com/fastapi/sqlmodel/releases) - [Changelog](https://github.com/fastapi/sqlmodel/blob/main/docs/release-notes.md) - [Commits](https://github.com/fastapi/sqlmodel/compare/0.0.27...0.0.31) --- updated-dependencies: - dependency-name: sqlmodel dependency-version: 0.0.31 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem <svlandeg@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 524d52718..ef4330a0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -174,7 +174,7 @@ tests = [ "pytest>=7.1.3,<9.0.0", "pytest-codspeed==4.2.0", "pyyaml>=5.3.1,<7.0.0", - "sqlmodel==0.0.27", + "sqlmodel==0.0.31", "strawberry-graphql>=0.200.0,<1.0.0", "types-orjson==3.6.2", "types-ujson==5.10.0.20240515", diff --git a/uv.lock b/uv.lock index 312181b4e..8b202e089 100644 --- a/uv.lock +++ b/uv.lock @@ -1228,7 +1228,7 @@ dev = [ { name = "python-slugify", specifier = "==8.0.4" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, { name = "ruff", specifier = "==0.14.3" }, - { name = "sqlmodel", specifier = "==0.0.27" }, + { name = "sqlmodel", specifier = "==0.0.31" }, { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, { name = "typer", specifier = "==0.21.1" }, { name = "types-orjson", specifier = "==3.6.2" }, @@ -1279,7 +1279,7 @@ tests = [ { name = "pytest-codspeed", specifier = "==4.2.0" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, { name = "ruff", specifier = "==0.14.3" }, - { name = "sqlmodel", specifier = "==0.0.27" }, + { name = "sqlmodel", specifier = "==0.0.31" }, { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, { name = "types-orjson", specifier = "==3.6.2" }, { name = "types-ujson", specifier = "==5.10.0.20240515" }, @@ -4415,15 +4415,15 @@ wheels = [ [[package]] name = "sqlmodel" -version = "0.0.27" +version = "0.0.31" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/5a/693d90866233e837d182da76082a6d4c2303f54d3aaaa5c78e1238c5d863/sqlmodel-0.0.27.tar.gz", hash = "sha256:ad1227f2014a03905aef32e21428640848ac09ff793047744a73dfdd077ff620", size = 118053, upload-time = "2025-10-08T16:39:11.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/b8/e7cd6def4a773f25d6e29ffce63ccbfd6cf9488b804ab6fb9b80d334b39d/sqlmodel-0.0.31.tar.gz", hash = "sha256:2d41a8a9ee05e40736e2f9db8ea28cbfe9b5d4e5a18dd139e80605025e0c516c", size = 94952, upload-time = "2025-12-28T12:35:01.436Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/92/c35e036151fe53822893979f8a13e6f235ae8191f4164a79ae60a95d66aa/sqlmodel-0.0.27-py3-none-any.whl", hash = "sha256:667fe10aa8ff5438134668228dc7d7a08306f4c5c4c7e6ad3ad68defa0e7aa49", size = 29131, upload-time = "2025-10-08T16:39:10.917Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/5aa5be921800f6418a949a73c9bb7054890881143e6bc604a93d228a95a3/sqlmodel-0.0.31-py3-none-any.whl", hash = "sha256:6d946d56cac4c2db296ba1541357cee2e795d68174e2043cd138b916794b1513", size = 27093, upload-time = "2025-12-28T12:35:00.108Z" }, ] [[package]] From ecb1444738a3315167a8c11eac9b8503aec5a3d3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:57:46 +0000 Subject: [PATCH 126/243] =?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 afe456a3f..997bd98d5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -41,6 +41,7 @@ hide: ### Internal +* ⬆ Bump gitpython from 3.1.45 to 3.1.46. PR [#14800](https://github.com/fastapi/fastapi/pull/14800) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.16.0 to 0.21.1. PR [#14799](https://github.com/fastapi/fastapi/pull/14799) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👥 Update FastAPI GitHub topic repositories. PR [#14803](https://github.com/fastapi/fastapi/pull/14803) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Contributors and Translators. PR [#14796](https://github.com/fastapi/fastapi/pull/14796) by [@tiangolo](https://github.com/tiangolo). From 87b5333e8ae48cd68ef90ac29897bce25bb6540c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:00:26 +0000 Subject: [PATCH 127/243] =?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 997bd98d5..7fd3b9178 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -41,6 +41,7 @@ hide: ### Internal +* ⬆ Bump mkdocs-macros-plugin from 1.4.1 to 1.5.0. PR [#14801](https://github.com/fastapi/fastapi/pull/14801) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump gitpython from 3.1.45 to 3.1.46. PR [#14800](https://github.com/fastapi/fastapi/pull/14800) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.16.0 to 0.21.1. PR [#14799](https://github.com/fastapi/fastapi/pull/14799) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👥 Update FastAPI GitHub topic repositories. PR [#14803](https://github.com/fastapi/fastapi/pull/14803) by [@tiangolo](https://github.com/tiangolo). From 608ff552ba249c0a6cc8fdf755b43135cd45a9fa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:00:28 +0000 Subject: [PATCH 128/243] =?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 7fd3b9178..fb08caca4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -41,6 +41,7 @@ hide: ### Internal +* ⬆ Bump sqlmodel from 0.0.27 to 0.0.31. PR [#14802](https://github.com/fastapi/fastapi/pull/14802) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-macros-plugin from 1.4.1 to 1.5.0. PR [#14801](https://github.com/fastapi/fastapi/pull/14801) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump gitpython from 3.1.45 to 3.1.46. PR [#14800](https://github.com/fastapi/fastapi/pull/14800) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.16.0 to 0.21.1. PR [#14799](https://github.com/fastapi/fastapi/pull/14799) by [@dependabot[bot]](https://github.com/apps/dependabot). From bc9ad6b1342bd191693942cbcbd0fee954cf1a3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:09:12 +0000 Subject: [PATCH 129/243] =?UTF-8?q?=E2=AC=86=20Bump=20pyasn1=20from=200.6.?= =?UTF-8?q?1=20to=200.6.2=20(#14804)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pyasn1](https://github.com/pyasn1/pyasn1) from 0.6.1 to 0.6.2. - [Release notes](https://github.com/pyasn1/pyasn1/releases) - [Changelog](https://github.com/pyasn1/pyasn1/blob/main/CHANGES.rst) - [Commits](https://github.com/pyasn1/pyasn1/compare/v0.6.1...v0.6.2) --- updated-dependencies: - dependency-name: pyasn1 dependency-version: 0.6.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem <svlandeg@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 8b202e089..3be38bb80 100644 --- a/uv.lock +++ b/uv.lock @@ -3238,11 +3238,11 @@ argon2 = [ [[package]] name = "pyasn1" -version = "0.6.1" +version = "0.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, ] [[package]] From 82959de14c755e898c4216b2a2d241801c2606eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:09:37 +0000 Subject: [PATCH 130/243] =?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 fb08caca4..0252730c8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -41,6 +41,7 @@ hide: ### Internal +* ⬆ Bump pyasn1 from 0.6.1 to 0.6.2. PR [#14804](https://github.com/fastapi/fastapi/pull/14804) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump sqlmodel from 0.0.27 to 0.0.31. PR [#14802](https://github.com/fastapi/fastapi/pull/14802) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-macros-plugin from 1.4.1 to 1.5.0. PR [#14801](https://github.com/fastapi/fastapi/pull/14801) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump gitpython from 3.1.45 to 3.1.46. PR [#14800](https://github.com/fastapi/fastapi/pull/14800) by [@dependabot[bot]](https://github.com/apps/dependabot). From ec07e62e1c3c054cd3fa0608708103f2acfc3a03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:34:26 +0100 Subject: [PATCH 131/243] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.14.3?= =?UTF-8?q?=20to=200.14.14=20(#14798)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.3 to 0.14.14. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.14.3...0.14.14) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.14 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem <svlandeg@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 48 ++++++++++++++++++++++++------------------------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ef4330a0e..dea61c6a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,7 +151,7 @@ docs = [ ] docs-tests = [ "httpx>=0.23.0,<1.0.0", - "ruff==0.14.3", + "ruff==0.14.14", ] github-actions = [ "httpx>=0.27.0,<1.0.0", diff --git a/uv.lock b/uv.lock index 3be38bb80..2df0b256b 100644 --- a/uv.lock +++ b/uv.lock @@ -1227,7 +1227,7 @@ dev = [ { name = "pytest-codspeed", specifier = "==4.2.0" }, { name = "python-slugify", specifier = "==8.0.4" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, - { name = "ruff", specifier = "==0.14.3" }, + { name = "ruff", specifier = "==0.14.14" }, { name = "sqlmodel", specifier = "==0.0.31" }, { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, { name = "typer", specifier = "==0.21.1" }, @@ -1250,12 +1250,12 @@ docs = [ { name = "pillow", specifier = "==11.3.0" }, { name = "python-slugify", specifier = "==8.0.4" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, - { name = "ruff", specifier = "==0.14.3" }, + { name = "ruff", specifier = "==0.14.14" }, { name = "typer", specifier = "==0.21.1" }, ] docs-tests = [ { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, - { name = "ruff", specifier = "==0.14.3" }, + { name = "ruff", specifier = "==0.14.14" }, ] github-actions = [ { name = "httpx", specifier = ">=0.27.0,<1.0.0" }, @@ -1278,7 +1278,7 @@ tests = [ { name = "pytest", specifier = ">=7.1.3,<9.0.0" }, { name = "pytest-codspeed", specifier = "==4.2.0" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, - { name = "ruff", specifier = "==0.14.3" }, + { name = "ruff", specifier = "==0.14.14" }, { name = "sqlmodel", specifier = "==0.0.31" }, { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, { name = "types-orjson", specifier = "==3.6.2" }, @@ -4248,28 +4248,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.3" +version = "0.14.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/62/50b7727004dfe361104dfbf898c45a9a2fdfad8c72c04ae62900224d6ecf/ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153", size = 5558687, upload-time = "2025-10-31T00:26:26.878Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/8e/0c10ff1ea5d4360ab8bfca4cb2c9d979101a391f3e79d2616c9bf348cd26/ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371", size = 12535613, upload-time = "2025-10-31T00:25:44.302Z" }, - { url = "https://files.pythonhosted.org/packages/d3/c8/6724f4634c1daf52409fbf13fefda64aa9c8f81e44727a378b7b73dc590b/ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654", size = 12855812, upload-time = "2025-10-31T00:25:47.793Z" }, - { url = "https://files.pythonhosted.org/packages/de/03/db1bce591d55fd5f8a08bb02517fa0b5097b2ccabd4ea1ee29aa72b67d96/ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14", size = 11944026, upload-time = "2025-10-31T00:25:49.657Z" }, - { url = "https://files.pythonhosted.org/packages/0b/75/4f8dbd48e03272715d12c87dc4fcaaf21b913f0affa5f12a4e9c6f8a0582/ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed", size = 12356818, upload-time = "2025-10-31T00:25:51.949Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9b/506ec5b140c11d44a9a4f284ea7c14ebf6f8b01e6e8917734a3325bff787/ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc", size = 12336745, upload-time = "2025-10-31T00:25:54.248Z" }, - { url = "https://files.pythonhosted.org/packages/c7/e1/c560d254048c147f35e7f8131d30bc1f63a008ac61595cf3078a3e93533d/ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd", size = 13101684, upload-time = "2025-10-31T00:25:56.253Z" }, - { url = "https://files.pythonhosted.org/packages/a5/32/e310133f8af5cd11f8cc30f52522a3ebccc5ea5bff4b492f94faceaca7a8/ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb", size = 14535000, upload-time = "2025-10-31T00:25:58.397Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a1/7b0470a22158c6d8501eabc5e9b6043c99bede40fa1994cadf6b5c2a61c7/ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20", size = 14156450, upload-time = "2025-10-31T00:26:00.889Z" }, - { url = "https://files.pythonhosted.org/packages/0a/96/24bfd9d1a7f532b560dcee1a87096332e461354d3882124219bcaff65c09/ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0", size = 13568414, upload-time = "2025-10-31T00:26:03.291Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e7/138b883f0dfe4ad5b76b58bf4ae675f4d2176ac2b24bdd81b4d966b28c61/ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e", size = 13315293, upload-time = "2025-10-31T00:26:05.708Z" }, - { url = "https://files.pythonhosted.org/packages/33/f4/c09bb898be97b2eb18476b7c950df8815ef14cf956074177e9fbd40b7719/ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5", size = 13539444, upload-time = "2025-10-31T00:26:08.09Z" }, - { url = "https://files.pythonhosted.org/packages/9c/aa/b30a1db25fc6128b1dd6ff0741fa4abf969ded161599d07ca7edd0739cc0/ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e", size = 12252581, upload-time = "2025-10-31T00:26:10.297Z" }, - { url = "https://files.pythonhosted.org/packages/da/13/21096308f384d796ffe3f2960b17054110a9c3828d223ca540c2b7cc670b/ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e", size = 12307503, upload-time = "2025-10-31T00:26:12.646Z" }, - { url = "https://files.pythonhosted.org/packages/cb/cc/a350bac23f03b7dbcde3c81b154706e80c6f16b06ff1ce28ed07dc7b07b0/ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa", size = 12675457, upload-time = "2025-10-31T00:26:15.044Z" }, - { url = "https://files.pythonhosted.org/packages/cb/76/46346029fa2f2078826bc88ef7167e8c198e58fe3126636e52f77488cbba/ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f", size = 13403980, upload-time = "2025-10-31T00:26:17.81Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a4/35f1ef68c4e7b236d4a5204e3669efdeefaef21f0ff6a456792b3d8be438/ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7", size = 12500045, upload-time = "2025-10-31T00:26:20.503Z" }, - { url = "https://files.pythonhosted.org/packages/03/15/51960ae340823c9859fb60c63301d977308735403e2134e17d1d2858c7fb/ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f", size = 13594005, upload-time = "2025-10-31T00:26:22.533Z" }, - { url = "https://files.pythonhosted.org/packages/b7/73/4de6579bac8e979fca0a77e54dec1f1e011a0d268165eb8a9bc0982a6564/ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1", size = 12590017, upload-time = "2025-10-31T00:26:24.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] [[package]] From 2247750d74b26bc212300cdaff9d82a15bf71eca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:34:52 +0000 Subject: [PATCH 132/243] =?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 0252730c8..e009de8a0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -41,6 +41,7 @@ hide: ### Internal +* ⬆ Bump ruff from 0.14.3 to 0.14.14. PR [#14798](https://github.com/fastapi/fastapi/pull/14798) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pyasn1 from 0.6.1 to 0.6.2. PR [#14804](https://github.com/fastapi/fastapi/pull/14804) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump sqlmodel from 0.0.27 to 0.0.31. PR [#14802](https://github.com/fastapi/fastapi/pull/14802) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-macros-plugin from 1.4.1 to 1.5.0. PR [#14801](https://github.com/fastapi/fastapi/pull/14801) by [@dependabot[bot]](https://github.com/apps/dependabot). From f3f498100f1d0edc344e200b215d42a6e46473fd Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:08:08 +0300 Subject: [PATCH 133/243] =?UTF-8?q?=F0=9F=91=B7=20Run=20mypy=20by=20pre-co?= =?UTF-8?q?mmit=20(#14806)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- .pre-commit-config.yaml | 7 +++++++ fastapi/_compat/v2.py | 8 ++++---- fastapi/dependencies/utils.py | 4 ++-- fastapi/utils.py | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d88b70b7b..64b84bfbd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,6 +30,13 @@ repos: language: unsupported types: [python] + - id: local-mypy + name: mypy check + entry: uv run mypy fastapi + require_serial: true + language: unsupported + pass_filenames: false + - id: add-permalinks-pages language: unsupported name: add-permalinks-pages diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 25b681453..dae78a32e 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -477,7 +477,7 @@ def get_model_fields(model: type[BaseModel]) -> list[ModelField]: @lru_cache def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]: - return get_model_fields(model) # type: ignore[return-value] + return get_model_fields(model) # Duplicate of several schema functions from Pydantic v1 to make them compatible with @@ -500,13 +500,13 @@ def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: - all_flat_models = set() + all_flat_models: TypeModelSet = set() v2_model_fields = [field for field in fields if isinstance(field, ModelField)] v2_flat_models = get_flat_models_from_fields(v2_model_fields, known_models=set()) - all_flat_models = all_flat_models.union(v2_flat_models) # type: ignore[arg-type] + all_flat_models = all_flat_models.union(v2_flat_models) - model_name_map = get_model_name_map(all_flat_models) # type: ignore[arg-type] + model_name_map = get_model_name_map(all_flat_models) return model_name_map diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 45e1ff3ed..2afc734ba 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -399,7 +399,7 @@ def analyze_param( if isinstance(fastapi_annotation, FieldInfo): # Copy `field_info` because we mutate `field_info.default` below. field_info = copy_field_info( - field_info=fastapi_annotation, # type: ignore[arg-type] + field_info=fastapi_annotation, annotation=use_annotation, ) assert ( @@ -433,7 +433,7 @@ def analyze_param( "Cannot specify FastAPI annotations in `Annotated` and default value" f" together for {param_name!r}" ) - field_info = value # type: ignore[assignment] + field_info = value if isinstance(field_info, FieldInfo): field_info.annotation = type_annotation diff --git a/fastapi/utils.py b/fastapi/utils.py index 78fdcbb5b..1c3a0881f 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -90,7 +90,7 @@ def create_model_field( field_info = field_info or FieldInfo(annotation=type_, default=default, alias=alias) kwargs = {"mode": mode, "name": name, "field_info": field_info} try: - return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type] + return v2.ModelField(**kwargs) # type: ignore[arg-type] except PydanticSchemaGenerationError: raise fastapi.exceptions.FastAPIError( _invalid_args_message.format(type_=type_) From eacbce24c9d299c6a28110d9fc8ac50f53cddb08 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:08:36 +0000 Subject: [PATCH 134/243] =?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 e009de8a0..ebf3a348f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -41,6 +41,7 @@ hide: ### Internal +* 👷 Run mypy by pre-commit. PR [#14806](https://github.com/fastapi/fastapi/pull/14806) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆ Bump ruff from 0.14.3 to 0.14.14. PR [#14798](https://github.com/fastapi/fastapi/pull/14798) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pyasn1 from 0.6.1 to 0.6.2. PR [#14804](https://github.com/fastapi/fastapi/pull/14804) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump sqlmodel from 0.0.27 to 0.0.31. PR [#14802](https://github.com/fastapi/fastapi/pull/14802) by [@dependabot[bot]](https://github.com/apps/dependabot). From 7621a3aa4b29713aa2ce0ab9605a008a5a8b9335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Wed, 4 Feb 2026 03:46:32 -0800 Subject: [PATCH 135/243] =?UTF-8?q?=F0=9F=91=B7=20Run=20tests=20only=20on?= =?UTF-8?q?=20relevant=20code=20changes=20(not=20on=20docs)=20(#14813)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 891f76717..5cbbde61f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,35 @@ env: UV_NO_SYNC: true jobs: + changes: + runs-on: ubuntu-latest + # Required permissions + permissions: + pull-requests: read + # Set job outputs to values from filter step + outputs: + src: ${{ steps.filter.outputs.src }} + steps: + - uses: actions/checkout@v6 + # For pull requests it's not necessary to checkout the code but for the main branch it is + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + src: + - .github/workflows/test.yml + - docs_src/** + - fastapi/** + - scripts/** + - tests/** + - .python-version + - pyproject.toml + - uv.lock + test: + needs: + - changes + if: needs.changes.outputs.src == 'true' strategy: matrix: os: [ windows-latest, macos-latest ] @@ -91,7 +119,8 @@ jobs: include-hidden-files: true coverage-combine: - needs: [test] + needs: + - test runs-on: ubuntu-latest steps: - name: Dump GitHub context @@ -143,3 +172,4 @@ jobs: uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} + allowed-skips: coverage-combine,test From dd780f8caa13e3fa755cfabbec0e603f485cae97 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:46:55 +0000 Subject: [PATCH 136/243] =?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 ebf3a348f..85c1128fc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -41,6 +41,7 @@ hide: ### Internal +* 👷 Run tests only on relevant code changes (not on docs). PR [#14813](https://github.com/fastapi/fastapi/pull/14813) by [@tiangolo](https://github.com/tiangolo). * 👷 Run mypy by pre-commit. PR [#14806](https://github.com/fastapi/fastapi/pull/14806) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆ Bump ruff from 0.14.3 to 0.14.14. PR [#14798](https://github.com/fastapi/fastapi/pull/14798) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pyasn1 from 0.6.1 to 0.6.2. PR [#14804](https://github.com/fastapi/fastapi/pull/14804) by [@dependabot[bot]](https://github.com/apps/dependabot). From 261736ab4c3d7a3ec642071ba6f9408ebf9618d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Wed, 4 Feb 2026 03:49:00 -0800 Subject: [PATCH 137/243] =?UTF-8?q?=F0=9F=92=A1=20Update=20comment=20for?= =?UTF-8?q?=20Pydantic=20internals=20(#14814)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat/shared.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py index 419b58f7f..68b9bbdf1 100644 --- a/fastapi/_compat/shared.py +++ b/fastapi/_compat/shared.py @@ -17,7 +17,7 @@ from pydantic.version import VERSION as PYDANTIC_VERSION from starlette.datastructures import UploadFile from typing_extensions import get_args, get_origin -# Copy from Pydantic v2, compatible with v1 +# Copy from Pydantic: pydantic/_internal/_typing_extra.py if sys.version_info < (3, 10): WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias) # type: ignore[attr-defined] else: @@ -45,7 +45,7 @@ sequence_types = tuple(sequence_annotation_to_type.keys()) Url: type[Any] -# Copy of Pydantic v2, compatible with v1 +# Copy of Pydantic: pydantic/_internal/_utils.py def lenient_issubclass( cls: Any, class_or_tuple: Union[type[Any], tuple[type[Any], ...], None] ) -> bool: From 573c593dd04365f0d1dddde2e167851f6d19cef7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:49:22 +0000 Subject: [PATCH 138/243] =?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 85c1128fc..20af681fd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* 💡 Update comment for Pydantic internals. PR [#14814](https://github.com/fastapi/fastapi/pull/14814) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Fix minor typos in release notes. PR [#14780](https://github.com/fastapi/fastapi/pull/14780) by [@whyvineet](https://github.com/whyvineet). From dc3278654f13614d255edca66954326966f6620d Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:54:23 +0300 Subject: [PATCH 139/243] =?UTF-8?q?=F0=9F=93=9D=20Use=20`WSGIMiddleware`?= =?UTF-8?q?=20from=20`a2wsgi`=20instead=20of=20deprecated=20`fastapi.middl?= =?UTF-8?q?eware.wsgi.WSGIMiddleware`=20(#14756)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- docs/en/docs/advanced/wsgi.md | 20 ++++++++++++++++++-- docs/en/docs/reference/middleware.md | 8 -------- docs_src/wsgi/tutorial001_py39.py | 2 +- fastapi/middleware/wsgi.py | 4 +++- pyproject.toml | 2 +- uv.lock | 16 ++++++++++++++++ 6 files changed, 39 insertions(+), 13 deletions(-) diff --git a/docs/en/docs/advanced/wsgi.md b/docs/en/docs/advanced/wsgi.md index 07147df0a..aa68617cf 100644 --- a/docs/en/docs/advanced/wsgi.md +++ b/docs/en/docs/advanced/wsgi.md @@ -6,13 +6,29 @@ For that, you can use the `WSGIMiddleware` and use it to wrap your WSGI applicat ## Using `WSGIMiddleware` { #using-wsgimiddleware } -You need to import `WSGIMiddleware`. +/// info + +This requires installing `a2wsgi` for example with `pip install a2wsgi`. + +/// + +You need to import `WSGIMiddleware` from `a2wsgi`. Then wrap the WSGI (e.g. Flask) app with the middleware. And then mount that under a path. -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note + +Previously, it was recommended to use `WSGIMiddleware` from `fastapi.middleware.wsgi`, but it is now deprecated. + +It’s advised to use the `a2wsgi` package instead. The usage remains the same. + +Just ensure that you have the `a2wsgi` package installed and import `WSGIMiddleware` correctly from `a2wsgi`. + +/// ## Check it { #check-it } diff --git a/docs/en/docs/reference/middleware.md b/docs/en/docs/reference/middleware.md index 3c666ccda..48ff85158 100644 --- a/docs/en/docs/reference/middleware.md +++ b/docs/en/docs/reference/middleware.md @@ -35,11 +35,3 @@ It can be imported from `fastapi`: ```python from fastapi.middleware.trustedhost import TrustedHostMiddleware ``` - -::: fastapi.middleware.wsgi.WSGIMiddleware - -It can be imported from `fastapi`: - -```python -from fastapi.middleware.wsgi import WSGIMiddleware -``` diff --git a/docs_src/wsgi/tutorial001_py39.py b/docs_src/wsgi/tutorial001_py39.py index 7f27a85a1..8eeceb829 100644 --- a/docs_src/wsgi/tutorial001_py39.py +++ b/docs_src/wsgi/tutorial001_py39.py @@ -1,5 +1,5 @@ +from a2wsgi import WSGIMiddleware from fastapi import FastAPI -from fastapi.middleware.wsgi import WSGIMiddleware from flask import Flask, request from markupsafe import escape diff --git a/fastapi/middleware/wsgi.py b/fastapi/middleware/wsgi.py index c4c6a797d..69e4dcab9 100644 --- a/fastapi/middleware/wsgi.py +++ b/fastapi/middleware/wsgi.py @@ -1 +1,3 @@ -from starlette.middleware.wsgi import WSGIMiddleware as WSGIMiddleware # noqa +from starlette.middleware.wsgi import ( + WSGIMiddleware as WSGIMiddleware, +) # pragma: no cover # noqa diff --git a/pyproject.toml b/pyproject.toml index dea61c6a1..0f6bf1e4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,6 +178,7 @@ tests = [ "strawberry-graphql>=0.200.0,<1.0.0", "types-orjson==3.6.2", "types-ujson==5.10.0.20240515", + "a2wsgi>=1.9.0,<=2.0.0", ] translations = [ "gitpython==3.1.46", @@ -231,7 +232,6 @@ xfail_strict = true junit_family = "xunit2" filterwarnings = [ "error", - 'ignore:starlette.middleware.wsgi is deprecated and will be removed in a future release\..*:DeprecationWarning:starlette', # see https://trio.readthedocs.io/en/stable/history.html#trio-0-22-0-2022-09-28 "ignore:You seem to already have a custom.*:RuntimeWarning:trio", # TODO: remove after upgrading SQLAlchemy to a version that includes the following changes diff --git a/uv.lock b/uv.lock index 2df0b256b..931a27021 100644 --- a/uv.lock +++ b/uv.lock @@ -7,6 +7,18 @@ resolution-markers = [ "python_full_version < '3.10'", ] +[[package]] +name = "a2wsgi" +version = "1.10.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/cb/822c56fbea97e9eee201a2e434a80437f6750ebcb1ed307ee3a0a7505b14/a2wsgi-1.10.10.tar.gz", hash = "sha256:a5bcffb52081ba39df0d5e9a884fc6f819d92e3a42389343ba77cbf809fe1f45", size = 18799, upload-time = "2025-06-18T09:00:10.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/d5/349aba3dc421e73cbd4958c0ce0a4f1aa3a738bc0d7de75d2f40ed43a535/a2wsgi-1.10.10-py3-none-any.whl", hash = "sha256:d2b21379479718539dc15fce53b876251a0efe7615352dfe49f6ad1bc507848d", size = 17389, upload-time = "2025-06-18T09:00:09.676Z" }, +] + [[package]] name = "ag-ui-protocol" version = "0.1.10" @@ -1058,6 +1070,7 @@ standard-no-fastapi-cloud-cli = [ [package.dev-dependencies] dev = [ + { name = "a2wsgi" }, { name = "anyio", extra = ["trio"] }, { name = "black" }, { name = "cairosvg" }, @@ -1131,6 +1144,7 @@ github-actions = [ { name = "smokeshow" }, ] tests = [ + { name = "a2wsgi" }, { name = "anyio", extra = ["trio"] }, { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, @@ -1197,6 +1211,7 @@ provides-extras = ["standard", "standard-no-fastapi-cloud-cli", "all"] [package.metadata.requires-dev] dev = [ + { name = "a2wsgi", specifier = ">=1.9.0,<=2.0.0" }, { name = "anyio", extras = ["trio"], specifier = ">=3.2.1,<5.0.0" }, { name = "black", specifier = "==25.1.0" }, { name = "cairosvg", specifier = "==2.8.2" }, @@ -1266,6 +1281,7 @@ github-actions = [ { name = "smokeshow", specifier = ">=0.5.0" }, ] tests = [ + { name = "a2wsgi", specifier = ">=1.9.0,<=2.0.0" }, { name = "anyio", extras = ["trio"], specifier = ">=3.2.1,<5.0.0" }, { name = "coverage", extras = ["toml"], specifier = ">=6.5.0,<8.0" }, { name = "dirty-equals", specifier = "==0.9.0" }, From e3a66494c91c0dc4fc423d812c5bf04795358e03 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:54:46 +0000 Subject: [PATCH 140/243] =?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 20af681fd..8fc6e8621 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Docs +* 📝 Use `WSGIMiddleware` from `a2wsgi` instead of deprecated `fastapi.middleware.wsgi.WSGIMiddleware`. PR [#14756](https://github.com/fastapi/fastapi/pull/14756) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Fix minor typos in release notes. PR [#14780](https://github.com/fastapi/fastapi/pull/14780) by [@whyvineet](https://github.com/whyvineet). * 🐛 Fix copy button in custom.js. PR [#14722](https://github.com/fastapi/fastapi/pull/14722) by [@fcharrier](https://github.com/fcharrier). * 📝 Add contribution instructions about LLM generated code and comments and automated tools for PRs. PR [#14706](https://github.com/fastapi/fastapi/pull/14706) by [@tiangolo](https://github.com/tiangolo). From 27b91d4ad606876a06f25ea82c989374d54311fd Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:05:19 +0300 Subject: [PATCH 141/243] =?UTF-8?q?=F0=9F=94=A8=20Update=20translation=20s?= =?UTF-8?q?cript=20to=20retry=20if=20LLM-response=20doesn't=20pass=20valid?= =?UTF-8?q?ation=20with=20Translation=20Fixer=20tool=20(#14749)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- scripts/translate.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/scripts/translate.py b/scripts/translate.py index eba4ad6a2..9eda7b390 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -10,6 +10,7 @@ from typing import Annotated import git import typer import yaml +from doc_parsing_utils import check_translation from github import Github from pydantic_ai import Agent from rich import print @@ -119,9 +120,30 @@ def translate_page( ] ) prompt = "\n\n".join(prompt_segments) - print(f"Running agent for {out_path}") - result = agent.run_sync(prompt) - out_content = f"{result.output.strip()}\n" + + MAX_ATTEMPTS = 3 + for attempt_no in range(1, MAX_ATTEMPTS + 1): + print(f"Running agent for {out_path} (attempt {attempt_no}/{MAX_ATTEMPTS})") + result = agent.run_sync(prompt) + out_content = f"{result.output.strip()}\n" + try: + check_translation( + doc_lines=out_content.splitlines(), + en_doc_lines=original_content.splitlines(), + lang_code=language, + auto_fix=False, + path=str(out_path), + ) + break # Exit loop if no errors + except ValueError as e: + print( + f"Translation check failed on attempt {attempt_no}/{MAX_ATTEMPTS}: {e}" + ) + continue # Retry if not reached max attempts + else: # Max retry attempts reached + print(f"Translation failed for {out_path} after {MAX_ATTEMPTS} attempts") + raise typer.Exit(code=1) + print(f"Saving translation to {out_path}") out_path.write_text(out_content, encoding="utf-8", newline="\n") From f6ba0141f4736fb78ec80e509634be8d505ae768 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:05:41 +0000 Subject: [PATCH 142/243] =?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 8fc6e8621..30704f3e5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -46,6 +46,7 @@ hide: ### Internal +* 🔨 Update translation script to retry if LLM-response doesn't pass validation with Translation Fixer tool. PR [#14749](https://github.com/fastapi/fastapi/pull/14749) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Run tests only on relevant code changes (not on docs). PR [#14813](https://github.com/fastapi/fastapi/pull/14813) by [@tiangolo](https://github.com/tiangolo). * 👷 Run mypy by pre-commit. PR [#14806](https://github.com/fastapi/fastapi/pull/14806) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆ Bump ruff from 0.14.3 to 0.14.14. PR [#14798](https://github.com/fastapi/fastapi/pull/14798) by [@dependabot[bot]](https://github.com/apps/dependabot). From 741c7345eaecbeedd3ea83f6e7d1048b33ce332b Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:07:26 +0300 Subject: [PATCH 143/243] =?UTF-8?q?=F0=9F=93=9D=20Use=20return=20type=20an?= =?UTF-8?q?notation=20instead=20of=20`response=5Fmodel`=20when=20possible?= =?UTF-8?q?=20(#14753)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/path-operation-configuration.md | 4 ++-- docs_src/app_testing/app_b_an_py310/main.py | 4 ++-- docs_src/app_testing/app_b_an_py39/main.py | 4 ++-- docs_src/app_testing/app_b_py310/main.py | 4 ++-- docs_src/app_testing/app_b_py39/main.py | 4 ++-- docs_src/body_updates/tutorial002_py310.py | 4 ++-- docs_src/body_updates/tutorial002_py39.py | 4 ++-- .../tutorial004_py310.py | 4 ++-- .../path_operation_advanced_configuration/tutorial004_py39.py | 4 ++-- docs_src/path_operation_configuration/tutorial001_py310.py | 4 ++-- docs_src/path_operation_configuration/tutorial001_py39.py | 4 ++-- docs_src/path_operation_configuration/tutorial002_py310.py | 4 ++-- docs_src/path_operation_configuration/tutorial002_py39.py | 4 ++-- docs_src/path_operation_configuration/tutorial003_py310.py | 3 +-- docs_src/path_operation_configuration/tutorial003_py39.py | 3 +-- docs_src/path_operation_configuration/tutorial004_py310.py | 4 ++-- docs_src/path_operation_configuration/tutorial004_py39.py | 4 ++-- docs_src/path_operation_configuration/tutorial005_py310.py | 3 +-- docs_src/path_operation_configuration/tutorial005_py39.py | 3 +-- docs_src/security/tutorial004_an_py310.py | 4 ++-- docs_src/security/tutorial004_an_py39.py | 4 ++-- docs_src/security/tutorial004_py310.py | 4 ++-- docs_src/security/tutorial004_py39.py | 4 ++-- docs_src/security/tutorial005_an_py310.py | 4 ++-- docs_src/security/tutorial005_an_py39.py | 4 ++-- docs_src/security/tutorial005_py310.py | 4 ++-- docs_src/security/tutorial005_py39.py | 4 ++-- 27 files changed, 50 insertions(+), 54 deletions(-) diff --git a/docs/en/docs/tutorial/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md index 59be5ab37..1bb7ee554 100644 --- a/docs/en/docs/tutorial/path-operation-configuration.md +++ b/docs/en/docs/tutorial/path-operation-configuration.md @@ -52,7 +52,7 @@ In these cases, it could make sense to store the tags in an `Enum`. You can add a `summary` and `description`: -{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} ## Description from docstring { #description-from-docstring } @@ -70,7 +70,7 @@ It will be used in the interactive docs: You can specify the response description with the parameter `response_description`: -{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info diff --git a/docs_src/app_testing/app_b_an_py310/main.py b/docs_src/app_testing/app_b_an_py310/main.py index c5952be0b..120289f56 100644 --- a/docs_src/app_testing/app_b_an_py310/main.py +++ b/docs_src/app_testing/app_b_an_py310/main.py @@ -28,8 +28,8 @@ async def read_main(item_id: str, x_token: Annotated[str, Header()]): return fake_db[item_id] -@app.post("/items/", response_model=Item) -async def create_item(item: Item, x_token: Annotated[str, Header()]): +@app.post("/items/") +async def create_item(item: Item, x_token: Annotated[str, Header()]) -> Item: if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: diff --git a/docs_src/app_testing/app_b_an_py39/main.py b/docs_src/app_testing/app_b_an_py39/main.py index 142e23a26..801d5f21e 100644 --- a/docs_src/app_testing/app_b_an_py39/main.py +++ b/docs_src/app_testing/app_b_an_py39/main.py @@ -28,8 +28,8 @@ async def read_main(item_id: str, x_token: Annotated[str, Header()]): return fake_db[item_id] -@app.post("/items/", response_model=Item) -async def create_item(item: Item, x_token: Annotated[str, Header()]): +@app.post("/items/") +async def create_item(item: Item, x_token: Annotated[str, Header()]) -> Item: if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: diff --git a/docs_src/app_testing/app_b_py310/main.py b/docs_src/app_testing/app_b_py310/main.py index eccedcc7c..6c5c34130 100644 --- a/docs_src/app_testing/app_b_py310/main.py +++ b/docs_src/app_testing/app_b_py310/main.py @@ -26,8 +26,8 @@ async def read_main(item_id: str, x_token: str = Header()): return fake_db[item_id] -@app.post("/items/", response_model=Item) -async def create_item(item: Item, x_token: str = Header()): +@app.post("/items/") +async def create_item(item: Item, x_token: str = Header()) -> Item: if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: diff --git a/docs_src/app_testing/app_b_py39/main.py b/docs_src/app_testing/app_b_py39/main.py index 45a103378..89053c432 100644 --- a/docs_src/app_testing/app_b_py39/main.py +++ b/docs_src/app_testing/app_b_py39/main.py @@ -28,8 +28,8 @@ async def read_main(item_id: str, x_token: str = Header()): return fake_db[item_id] -@app.post("/items/", response_model=Item) -async def create_item(item: Item, x_token: str = Header()): +@app.post("/items/") +async def create_item(item: Item, x_token: str = Header()) -> Item: if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: diff --git a/docs_src/body_updates/tutorial002_py310.py b/docs_src/body_updates/tutorial002_py310.py index e5db71110..d30e41027 100644 --- a/docs_src/body_updates/tutorial002_py310.py +++ b/docs_src/body_updates/tutorial002_py310.py @@ -25,8 +25,8 @@ async def read_item(item_id: str): return items[item_id] -@app.patch("/items/{item_id}", response_model=Item) -async def update_item(item_id: str, item: Item): +@app.patch("/items/{item_id}") +async def update_item(item_id: str, item: Item) -> Item: stored_item_data = items[item_id] stored_item_model = Item(**stored_item_data) update_data = item.model_dump(exclude_unset=True) diff --git a/docs_src/body_updates/tutorial002_py39.py b/docs_src/body_updates/tutorial002_py39.py index eddd7af71..3714b5a55 100644 --- a/docs_src/body_updates/tutorial002_py39.py +++ b/docs_src/body_updates/tutorial002_py39.py @@ -27,8 +27,8 @@ async def read_item(item_id: str): return items[item_id] -@app.patch("/items/{item_id}", response_model=Item) -async def update_item(item_id: str, item: Item): +@app.patch("/items/{item_id}") +async def update_item(item_id: str, item: Item) -> Item: stored_item_data = items[item_id] stored_item_model = Item(**stored_item_data) update_data = item.model_dump(exclude_unset=True) diff --git a/docs_src/path_operation_advanced_configuration/tutorial004_py310.py b/docs_src/path_operation_advanced_configuration/tutorial004_py310.py index a815a564b..f222b11dc 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial004_py310.py +++ b/docs_src/path_operation_advanced_configuration/tutorial004_py310.py @@ -12,8 +12,8 @@ class Item(BaseModel): tags: set[str] = set() -@app.post("/items/", response_model=Item, summary="Create an item") -async def create_item(item: Item): +@app.post("/items/", summary="Create an item") +async def create_item(item: Item) -> Item: """ Create an item with all the information: diff --git a/docs_src/path_operation_advanced_configuration/tutorial004_py39.py b/docs_src/path_operation_advanced_configuration/tutorial004_py39.py index d5fe6705c..8fabe7cb8 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial004_py39.py +++ b/docs_src/path_operation_advanced_configuration/tutorial004_py39.py @@ -14,8 +14,8 @@ class Item(BaseModel): tags: set[str] = set() -@app.post("/items/", response_model=Item, summary="Create an item") -async def create_item(item: Item): +@app.post("/items/", summary="Create an item") +async def create_item(item: Item) -> Item: """ Create an item with all the information: diff --git a/docs_src/path_operation_configuration/tutorial001_py310.py b/docs_src/path_operation_configuration/tutorial001_py310.py index da078fdf5..2e7488ea4 100644 --- a/docs_src/path_operation_configuration/tutorial001_py310.py +++ b/docs_src/path_operation_configuration/tutorial001_py310.py @@ -12,6 +12,6 @@ class Item(BaseModel): tags: set[str] = set() -@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED) -async def create_item(item: Item): +@app.post("/items/", status_code=status.HTTP_201_CREATED) +async def create_item(item: Item) -> Item: return item diff --git a/docs_src/path_operation_configuration/tutorial001_py39.py b/docs_src/path_operation_configuration/tutorial001_py39.py index a9dcbf389..09b318282 100644 --- a/docs_src/path_operation_configuration/tutorial001_py39.py +++ b/docs_src/path_operation_configuration/tutorial001_py39.py @@ -14,6 +14,6 @@ class Item(BaseModel): tags: set[str] = set() -@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED) -async def create_item(item: Item): +@app.post("/items/", status_code=status.HTTP_201_CREATED) +async def create_item(item: Item) -> Item: return item diff --git a/docs_src/path_operation_configuration/tutorial002_py310.py b/docs_src/path_operation_configuration/tutorial002_py310.py index 9a8af5432..59908ed7c 100644 --- a/docs_src/path_operation_configuration/tutorial002_py310.py +++ b/docs_src/path_operation_configuration/tutorial002_py310.py @@ -12,8 +12,8 @@ class Item(BaseModel): tags: set[str] = set() -@app.post("/items/", response_model=Item, tags=["items"]) -async def create_item(item: Item): +@app.post("/items/", tags=["items"]) +async def create_item(item: Item) -> Item: return item diff --git a/docs_src/path_operation_configuration/tutorial002_py39.py b/docs_src/path_operation_configuration/tutorial002_py39.py index e7ced7de7..fca3b0de9 100644 --- a/docs_src/path_operation_configuration/tutorial002_py39.py +++ b/docs_src/path_operation_configuration/tutorial002_py39.py @@ -14,8 +14,8 @@ class Item(BaseModel): tags: set[str] = set() -@app.post("/items/", response_model=Item, tags=["items"]) -async def create_item(item: Item): +@app.post("/items/", tags=["items"]) +async def create_item(item: Item) -> Item: return item diff --git a/docs_src/path_operation_configuration/tutorial003_py310.py b/docs_src/path_operation_configuration/tutorial003_py310.py index 3d94afe2c..56bd7e36a 100644 --- a/docs_src/path_operation_configuration/tutorial003_py310.py +++ b/docs_src/path_operation_configuration/tutorial003_py310.py @@ -14,9 +14,8 @@ class Item(BaseModel): @app.post( "/items/", - response_model=Item, summary="Create an item", description="Create an item with all the information, name, description, price, tax and a set of unique tags", ) -async def create_item(item: Item): +async def create_item(item: Item) -> Item: return item diff --git a/docs_src/path_operation_configuration/tutorial003_py39.py b/docs_src/path_operation_configuration/tutorial003_py39.py index 607c5707e..a77fb34d8 100644 --- a/docs_src/path_operation_configuration/tutorial003_py39.py +++ b/docs_src/path_operation_configuration/tutorial003_py39.py @@ -16,9 +16,8 @@ class Item(BaseModel): @app.post( "/items/", - response_model=Item, summary="Create an item", description="Create an item with all the information, name, description, price, tax and a set of unique tags", ) -async def create_item(item: Item): +async def create_item(item: Item) -> Item: return item diff --git a/docs_src/path_operation_configuration/tutorial004_py310.py b/docs_src/path_operation_configuration/tutorial004_py310.py index 4cb8bdd43..44404aa08 100644 --- a/docs_src/path_operation_configuration/tutorial004_py310.py +++ b/docs_src/path_operation_configuration/tutorial004_py310.py @@ -12,8 +12,8 @@ class Item(BaseModel): tags: set[str] = set() -@app.post("/items/", response_model=Item, summary="Create an item") -async def create_item(item: Item): +@app.post("/items/", summary="Create an item") +async def create_item(item: Item) -> Item: """ Create an item with all the information: diff --git a/docs_src/path_operation_configuration/tutorial004_py39.py b/docs_src/path_operation_configuration/tutorial004_py39.py index fc25680c5..31dfbff1d 100644 --- a/docs_src/path_operation_configuration/tutorial004_py39.py +++ b/docs_src/path_operation_configuration/tutorial004_py39.py @@ -14,8 +14,8 @@ class Item(BaseModel): tags: set[str] = set() -@app.post("/items/", response_model=Item, summary="Create an item") -async def create_item(item: Item): +@app.post("/items/", summary="Create an item") +async def create_item(item: Item) -> Item: """ Create an item with all the information: diff --git a/docs_src/path_operation_configuration/tutorial005_py310.py b/docs_src/path_operation_configuration/tutorial005_py310.py index b176631d8..a4129d600 100644 --- a/docs_src/path_operation_configuration/tutorial005_py310.py +++ b/docs_src/path_operation_configuration/tutorial005_py310.py @@ -14,11 +14,10 @@ class Item(BaseModel): @app.post( "/items/", - response_model=Item, summary="Create an item", response_description="The created item", ) -async def create_item(item: Item): +async def create_item(item: Item) -> Item: """ Create an item with all the information: diff --git a/docs_src/path_operation_configuration/tutorial005_py39.py b/docs_src/path_operation_configuration/tutorial005_py39.py index ddf29b733..0a53a8f2d 100644 --- a/docs_src/path_operation_configuration/tutorial005_py39.py +++ b/docs_src/path_operation_configuration/tutorial005_py39.py @@ -16,11 +16,10 @@ class Item(BaseModel): @app.post( "/items/", - response_model=Item, summary="Create an item", response_description="The created item", ) -async def create_item(item: Item): +async def create_item(item: Item) -> Item: """ Create an item with all the information: diff --git a/docs_src/security/tutorial004_an_py310.py b/docs_src/security/tutorial004_an_py310.py index 18ea96bc5..368c743bf 100644 --- a/docs_src/security/tutorial004_an_py310.py +++ b/docs_src/security/tutorial004_an_py310.py @@ -133,10 +133,10 @@ async def login_for_access_token( return Token(access_token=access_token, token_type="bearer") -@app.get("/users/me/", response_model=User) +@app.get("/users/me/") async def read_users_me( current_user: Annotated[User, Depends(get_current_active_user)], -): +) -> User: return current_user diff --git a/docs_src/security/tutorial004_an_py39.py b/docs_src/security/tutorial004_an_py39.py index d3fd29e5a..73b3d456d 100644 --- a/docs_src/security/tutorial004_an_py39.py +++ b/docs_src/security/tutorial004_an_py39.py @@ -133,10 +133,10 @@ async def login_for_access_token( return Token(access_token=access_token, token_type="bearer") -@app.get("/users/me/", response_model=User) +@app.get("/users/me/") async def read_users_me( current_user: Annotated[User, Depends(get_current_active_user)], -): +) -> User: return current_user diff --git a/docs_src/security/tutorial004_py310.py b/docs_src/security/tutorial004_py310.py index cd1dcff46..8d0785b40 100644 --- a/docs_src/security/tutorial004_py310.py +++ b/docs_src/security/tutorial004_py310.py @@ -130,8 +130,8 @@ async def login_for_access_token( return Token(access_token=access_token, token_type="bearer") -@app.get("/users/me/", response_model=User) -async def read_users_me(current_user: User = Depends(get_current_active_user)): +@app.get("/users/me/") +async def read_users_me(current_user: User = Depends(get_current_active_user)) -> User: return current_user diff --git a/docs_src/security/tutorial004_py39.py b/docs_src/security/tutorial004_py39.py index 130dc699a..e67403d5d 100644 --- a/docs_src/security/tutorial004_py39.py +++ b/docs_src/security/tutorial004_py39.py @@ -131,8 +131,8 @@ async def login_for_access_token( return Token(access_token=access_token, token_type="bearer") -@app.get("/users/me/", response_model=User) -async def read_users_me(current_user: User = Depends(get_current_active_user)): +@app.get("/users/me/") +async def read_users_me(current_user: User = Depends(get_current_active_user)) -> User: return current_user diff --git a/docs_src/security/tutorial005_an_py310.py b/docs_src/security/tutorial005_an_py310.py index df55951c0..fef0ab71c 100644 --- a/docs_src/security/tutorial005_an_py310.py +++ b/docs_src/security/tutorial005_an_py310.py @@ -160,10 +160,10 @@ async def login_for_access_token( return Token(access_token=access_token, token_type="bearer") -@app.get("/users/me/", response_model=User) +@app.get("/users/me/") async def read_users_me( current_user: Annotated[User, Depends(get_current_active_user)], -): +) -> User: return current_user diff --git a/docs_src/security/tutorial005_an_py39.py b/docs_src/security/tutorial005_an_py39.py index 983c1c22c..1aeba688a 100644 --- a/docs_src/security/tutorial005_an_py39.py +++ b/docs_src/security/tutorial005_an_py39.py @@ -160,10 +160,10 @@ async def login_for_access_token( return Token(access_token=access_token, token_type="bearer") -@app.get("/users/me/", response_model=User) +@app.get("/users/me/") async def read_users_me( current_user: Annotated[User, Depends(get_current_active_user)], -): +) -> User: return current_user diff --git a/docs_src/security/tutorial005_py310.py b/docs_src/security/tutorial005_py310.py index d08e2c59f..412fbf798 100644 --- a/docs_src/security/tutorial005_py310.py +++ b/docs_src/security/tutorial005_py310.py @@ -159,8 +159,8 @@ async def login_for_access_token( return Token(access_token=access_token, token_type="bearer") -@app.get("/users/me/", response_model=User) -async def read_users_me(current_user: User = Depends(get_current_active_user)): +@app.get("/users/me/") +async def read_users_me(current_user: User = Depends(get_current_active_user)) -> User: return current_user diff --git a/docs_src/security/tutorial005_py39.py b/docs_src/security/tutorial005_py39.py index 5bde47ef4..32280aa48 100644 --- a/docs_src/security/tutorial005_py39.py +++ b/docs_src/security/tutorial005_py39.py @@ -160,8 +160,8 @@ async def login_for_access_token( return Token(access_token=access_token, token_type="bearer") -@app.get("/users/me/", response_model=User) -async def read_users_me(current_user: User = Depends(get_current_active_user)): +@app.get("/users/me/") +async def read_users_me(current_user: User = Depends(get_current_active_user)) -> User: return current_user From 5083f27e034ee3b1b3dad459bb420596a952e769 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:07:48 +0000 Subject: [PATCH 144/243] =?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 30704f3e5..819c58897 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Docs +* 📝 Use return type annotation instead of `response_model` when possible. PR [#14753](https://github.com/fastapi/fastapi/pull/14753) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Use `WSGIMiddleware` from `a2wsgi` instead of deprecated `fastapi.middleware.wsgi.WSGIMiddleware`. PR [#14756](https://github.com/fastapi/fastapi/pull/14756) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Fix minor typos in release notes. PR [#14780](https://github.com/fastapi/fastapi/pull/14780) by [@whyvineet](https://github.com/whyvineet). * 🐛 Fix copy button in custom.js. PR [#14722](https://github.com/fastapi/fastapi/pull/14722) by [@fcharrier](https://github.com/fcharrier). From 21b7b0893b43c1ea359a62b0ec74323d533635fb Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:30:47 +0300 Subject: [PATCH 145/243] =?UTF-8?q?=F0=9F=93=9D=20Fix=20dependency=20insta?= =?UTF-8?q?llation=20command=20in=20`docs/en/docs/contributing.md`=20(#147?= =?UTF-8?q?57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/contributing.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index a4d896109..1505dfd1e 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -13,7 +13,7 @@ Create a virtual environment and install the required packages with <a href="htt <div class="termy"> ```console -$ uv sync +$ uv sync --extra all ---> 100% ``` @@ -32,9 +32,9 @@ That way, you don't have to "install" your local version to be able to test ever /// note | Technical Details -This only happens when you install using `uv sync` instead of running `pip install fastapi` directly. +This only happens when you install using `uv sync --extra all` instead of running `pip install fastapi` directly. -That is because `uv sync` will install the local version of FastAPI in "editable" mode by default. +That is because `uv sync --extra all` will install the local version of FastAPI in "editable" mode by default. /// From 938c5f3500f4b052ecb5bf73c7ef9058607bfd31 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:31:13 +0000 Subject: [PATCH 146/243] =?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 819c58897..926a26a30 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Docs +* 📝 Fix dependency installation command in `docs/en/docs/contributing.md`. PR [#14757](https://github.com/fastapi/fastapi/pull/14757) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Use return type annotation instead of `response_model` when possible. PR [#14753](https://github.com/fastapi/fastapi/pull/14753) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Use `WSGIMiddleware` from `a2wsgi` instead of deprecated `fastapi.middleware.wsgi.WSGIMiddleware`. PR [#14756](https://github.com/fastapi/fastapi/pull/14756) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Fix minor typos in release notes. PR [#14780](https://github.com/fastapi/fastapi/pull/14780) by [@whyvineet](https://github.com/whyvineet). From 4414286f2c2bfad302dd87637641d635e9a45c00 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:33:07 +0300 Subject: [PATCH 147/243] =?UTF-8?q?=F0=9F=93=9D=20Update=20embedded=20code?= =?UTF-8?q?=20examples=20to=20Python=203.10=20syntax=20(#14758)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 ++++++------------ docs/en/docs/deployment/docker.md | 4 +--- docs/en/docs/index.md | 18 ++++++------------ docs/en/docs/tutorial/body-multiple-params.md | 11 ++++++----- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 1057b8694..963de51ed 100644 --- a/README.md +++ b/README.md @@ -164,8 +164,6 @@ $ pip install "fastapi[standard]" Create a file `main.py` with: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -177,7 +175,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -186,9 +184,7 @@ def read_item(item_id: int, q: Union[str, None] = None): If your code uses `async` / `await`, use `async def`: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -200,7 +196,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -291,9 +287,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request. Declare the body using standard Python types, thanks to Pydantic. -```Python hl_lines="4 9-12 25-27" -from typing import Union - +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -303,7 +297,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -312,7 +306,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} diff --git a/docs/en/docs/deployment/docker.md b/docs/en/docs/deployment/docker.md index 6b71f7360..7219f3afc 100644 --- a/docs/en/docs/deployment/docker.md +++ b/docs/en/docs/deployment/docker.md @@ -145,8 +145,6 @@ There are other formats and tools to define and install package dependencies. * Create a `main.py` file with: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -158,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 5eb47c7b3..ff0f4e9d9 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Create a file `main.py` with: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): If your code uses `async` / `await`, use `async def`: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request. Declare the body using standard Python types, thanks to Pydantic. -```Python hl_lines="4 9-12 25-27" -from typing import Union - +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -300,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -309,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index ed23c8149..bb0c58368 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -102,15 +102,16 @@ Of course, you can also declare additional query parameters whenever you need, a As, by default, singular values are interpreted as query parameters, you don't have to explicitly add a `Query`, you can just do: +```Python +q: str | None = None +``` + +Or in Python 3.9: + ```Python q: Union[str, None] = None ``` -Or in Python 3.10 and above: - -```Python -q: str | None = None -``` For example: From c9e512d808de5eb4196aaaf11ceebf0e9500e55b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:33:37 +0000 Subject: [PATCH 148/243] =?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 926a26a30..d5bc81668 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Docs +* 📝 Update embedded code examples to Python 3.10 syntax. PR [#14758](https://github.com/fastapi/fastapi/pull/14758) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Fix dependency installation command in `docs/en/docs/contributing.md`. PR [#14757](https://github.com/fastapi/fastapi/pull/14757) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Use return type annotation instead of `response_model` when possible. PR [#14753](https://github.com/fastapi/fastapi/pull/14753) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Use `WSGIMiddleware` from `a2wsgi` instead of deprecated `fastapi.middleware.wsgi.WSGIMiddleware`. PR [#14756](https://github.com/fastapi/fastapi/pull/14756) by [@YuriiMotov](https://github.com/YuriiMotov). From 3a41403ccda1a727135fec91dea118ad8f2cf50a Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:41:54 +0300 Subject: [PATCH 149/243] =?UTF-8?q?=F0=9F=93=9D=20Add=20links=20to=20relat?= =?UTF-8?q?ed=20sections=20of=20docs=20to=20docstrings=20(#14776)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- docs/en/docs/reference/request.md | 2 + docs/en/docs/reference/response.md | 2 + docs/en/docs/reference/responses.md | 2 + docs/en/docs/reference/security/index.md | 2 + docs/en/docs/reference/websockets.md | 24 +++--- fastapi/applications.py | 7 +- fastapi/exceptions.py | 10 +++ fastapi/openapi/docs.py | 30 ++++++++ fastapi/param_functions.py | 94 +++++++++++++++++++++++- fastapi/security/oauth2.py | 30 ++++++++ 10 files changed, 190 insertions(+), 13 deletions(-) diff --git a/docs/en/docs/reference/request.md b/docs/en/docs/reference/request.md index f1de21642..7994bf8a8 100644 --- a/docs/en/docs/reference/request.md +++ b/docs/en/docs/reference/request.md @@ -2,6 +2,8 @@ You can declare a parameter in a *path operation function* or dependency to be of type `Request` and then you can access the raw request object directly, without any validation, etc. +Read more about it in the [FastAPI docs about using Request directly](https://fastapi.tiangolo.com/advanced/using-request-directly/) + You can import it directly from `fastapi`: ```python diff --git a/docs/en/docs/reference/response.md b/docs/en/docs/reference/response.md index 00cf2c499..c9085766c 100644 --- a/docs/en/docs/reference/response.md +++ b/docs/en/docs/reference/response.md @@ -4,6 +4,8 @@ You can declare a parameter in a *path operation function* or dependency to be o You can also use it directly to create an instance of it and return it from your *path operations*. +Read more about it in the [FastAPI docs about returning a custom Response](https://fastapi.tiangolo.com/advanced/response-directly/#returning-a-custom-response) + You can import it directly from `fastapi`: ```python diff --git a/docs/en/docs/reference/responses.md b/docs/en/docs/reference/responses.md index 46f014fcc..bd5786129 100644 --- a/docs/en/docs/reference/responses.md +++ b/docs/en/docs/reference/responses.md @@ -56,6 +56,8 @@ There are a couple of custom FastAPI response classes, you can use them to optim ## Starlette Responses +You can read more about all of them in the [FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/) and in the [Starlette docs about Responses](https://starlette.dev/responses/). + ::: fastapi.responses.FileResponse options: members: diff --git a/docs/en/docs/reference/security/index.md b/docs/en/docs/reference/security/index.md index 9a5c5e15f..8163aa2df 100644 --- a/docs/en/docs/reference/security/index.md +++ b/docs/en/docs/reference/security/index.md @@ -28,6 +28,8 @@ from fastapi.security import ( ) ``` +Read more about them in the [FastAPI docs about Security](https://fastapi.tiangolo.com/tutorial/security/). + ## API Key Security Schemes ::: fastapi.security.APIKeyCookie diff --git a/docs/en/docs/reference/websockets.md b/docs/en/docs/reference/websockets.md index 4b7244e08..bd9f438be 100644 --- a/docs/en/docs/reference/websockets.md +++ b/docs/en/docs/reference/websockets.md @@ -2,6 +2,8 @@ When defining WebSockets, you normally declare a parameter of type `WebSocket` and with it you can read data from the client and send data to it. +Read more about it in the [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/) + It is provided directly by Starlette, but you can import it from `fastapi`: ```python @@ -44,16 +46,6 @@ When you want to define dependencies that should be compatible with both HTTP an - send_json - close -When a client disconnects, a `WebSocketDisconnect` exception is raised, you can catch it. - -You can import it directly form `fastapi`: - -```python -from fastapi import WebSocketDisconnect -``` - -::: fastapi.WebSocketDisconnect - ## WebSockets - additional classes Additional classes for handling WebSockets. @@ -66,4 +58,16 @@ from fastapi.websockets import WebSocketDisconnect, WebSocketState ::: fastapi.websockets.WebSocketDisconnect +When a client disconnects, a `WebSocketDisconnect` exception is raised, you can catch it. + +You can import it directly form `fastapi`: + +```python +from fastapi import WebSocketDisconnect +``` + +Read more about it in the [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/#handling-disconnections-and-multiple-clients) + ::: fastapi.websockets.WebSocketState + +`WebSocketState` is an enumeration of the possible states of a WebSocket connection. diff --git a/fastapi/applications.py b/fastapi/applications.py index 54175cb3b..340cabfc2 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -672,7 +672,7 @@ class FastAPI(Starlette): in the autogenerated OpenAPI using the `root_path`. Read more about it in the - [FastAPI docs for Behind a Proxy](https://fastapi.tiangolo.com/advanced/behind-a-proxy/#disable-automatic-server-from-root_path). + [FastAPI docs for Behind a Proxy](https://fastapi.tiangolo.com/advanced/behind-a-proxy/#disable-automatic-server-from-root-path). **Example** @@ -739,7 +739,7 @@ class FastAPI(Starlette): It will be added to the generated OpenAPI (e.g. visible at `/docs`). Read more about it in the - [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#deprecate-a-path-operation). """ ), ] = None, @@ -812,6 +812,9 @@ class FastAPI(Starlette): In this case, there would be two different schemas, one for input and another one for output. + + Read more about it in the + [FastAPI docs about how to separate schemas for input and output](https://fastapi.tiangolo.com/how-to/separate-openapi-schemas) """ ), ] = True, diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 1a3abd80c..62b4674de 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -49,6 +49,9 @@ class HTTPException(StarletteHTTPException): Doc( """ HTTP status code to send to the client. + + Read more about it in the + [FastAPI docs for Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/#use-httpexception) """ ), ], @@ -58,6 +61,9 @@ class HTTPException(StarletteHTTPException): """ Any data to be sent to the client in the `detail` key of the JSON response. + + Read more about it in the + [FastAPI docs for Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/#use-httpexception) """ ), ] = None, @@ -66,6 +72,10 @@ class HTTPException(StarletteHTTPException): Doc( """ Any headers to send to the client in the response. + + Read more about it in the + [FastAPI docs for Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/#add-custom-headers) + """ ), ] = None, diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index 82380f85d..632cc2c1d 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -33,6 +33,9 @@ def get_swagger_ui_html( This is normally done automatically by FastAPI using the default URL `/openapi.json`. + + Read more about it in the + [FastAPI docs for Conditional OpenAPI](https://fastapi.tiangolo.com/how-to/conditional-openapi/#conditional-openapi-from-settings-and-env-vars) """ ), ], @@ -41,6 +44,9 @@ def get_swagger_ui_html( Doc( """ The HTML `<title>` content, normally shown in the browser tab. + + Read more about it in the + [FastAPI docs for Custom Docs UI Static Assets](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/) """ ), ], @@ -51,6 +57,9 @@ def get_swagger_ui_html( The URL to use to load the Swagger UI JavaScript. It is normally set to a CDN URL. + + Read more about it in the + [FastAPI docs for Custom Docs UI Static Assets](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/) """ ), ] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js", @@ -61,6 +70,9 @@ def get_swagger_ui_html( The URL to use to load the Swagger UI CSS. It is normally set to a CDN URL. + + Read more about it in the + [FastAPI docs for Custom Docs UI Static Assets](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/) """ ), ] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css", @@ -77,6 +89,9 @@ def get_swagger_ui_html( Doc( """ The OAuth2 redirect URL, it is normally automatically handled by FastAPI. + + Read more about it in the + [FastAPI docs for Custom Docs UI Static Assets](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/) """ ), ] = None, @@ -85,6 +100,9 @@ def get_swagger_ui_html( Doc( """ A dictionary with Swagger UI OAuth2 initialization configurations. + + Read more about the available configuration options in the + [Swagger UI docs](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/). """ ), ] = None, @@ -95,6 +113,9 @@ def get_swagger_ui_html( Configuration parameters for Swagger UI. It defaults to [swagger_ui_default_parameters][fastapi.openapi.docs.swagger_ui_default_parameters]. + + Read more about it in the + [FastAPI docs about how to Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/). """ ), ] = None, @@ -168,6 +189,9 @@ def get_redoc_html( This is normally done automatically by FastAPI using the default URL `/openapi.json`. + + Read more about it in the + [FastAPI docs for Conditional OpenAPI](https://fastapi.tiangolo.com/how-to/conditional-openapi/#conditional-openapi-from-settings-and-env-vars) """ ), ], @@ -176,6 +200,9 @@ def get_redoc_html( Doc( """ The HTML `<title>` content, normally shown in the browser tab. + + Read more about it in the + [FastAPI docs for Custom Docs UI Static Assets](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/) """ ), ], @@ -186,6 +213,9 @@ def get_redoc_html( The URL to use to load the ReDoc JavaScript. It is normally set to a CDN URL. + + Read more about it in the + [FastAPI docs for Custom Docs UI Static Assets](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/) """ ), ] = "https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js", diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 0834fd741..9bd92be4c 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -79,6 +79,9 @@ def Path( # noqa: N802 Doc( """ Human-readable title. + + Read more about it in the + [FastAPI docs for Path Parameters and Numeric Validations](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#declare-metadata) """ ), ] = None, @@ -96,6 +99,9 @@ def Path( # noqa: N802 """ Greater than. If set, value must be greater than this. Only applicable to numbers. + + Read more about it in the + [FastAPI docs about Path parameters numeric validations](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#number-validations-greater-than-and-less-than-or-equal) """ ), ] = None, @@ -105,6 +111,9 @@ def Path( # noqa: N802 """ Greater than or equal. If set, value must be greater than or equal to this. Only applicable to numbers. + + Read more about it in the + [FastAPI docs about Path parameters numeric validations](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#number-validations-greater-than-and-less-than-or-equal) """ ), ] = None, @@ -113,6 +122,9 @@ def Path( # noqa: N802 Doc( """ Less than. If set, value must be less than this. Only applicable to numbers. + + Read more about it in the + [FastAPI docs about Path parameters numeric validations](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#number-validations-greater-than-and-less-than-or-equal) """ ), ] = None, @@ -122,6 +134,9 @@ def Path( # noqa: N802 """ Less than or equal. If set, value must be less than or equal to this. Only applicable to numbers. + + Read more about it in the + [FastAPI docs about Path parameters numeric validations](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#number-validations-greater-than-and-less-than-or-equal) """ ), ] = None, @@ -213,6 +228,9 @@ def Path( # noqa: N802 Doc( """ Example values for this field. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/) """ ), ] = None, @@ -343,6 +361,9 @@ def Query( # noqa: N802 Doc( """ Default value if the parameter field is not set. + + Read more about it in the + [FastAPI docs about Query parameters](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#alternative-old-query-as-the-default-value) """ ), ] = Undefined, @@ -367,6 +388,9 @@ def Query( # noqa: N802 This will be used to extract the data and for the generated OpenAPI. It is particularly useful when you can't use the name you want because it is a Python reserved keyword or similar. + + Read more about it in the + [FastAPI docs about Query parameters](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#alias-parameters) """ ), ] = None, @@ -402,6 +426,9 @@ def Query( # noqa: N802 Doc( """ Human-readable title. + + Read more about it in the + [FastAPI docs about Query parameters](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#declare-more-metadata) """ ), ] = None, @@ -410,6 +437,9 @@ def Query( # noqa: N802 Doc( """ Human-readable description. + + Read more about it in the + [FastAPI docs about Query parameters](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#declare-more-metadata) """ ), ] = None, @@ -419,6 +449,9 @@ def Query( # noqa: N802 """ Greater than. If set, value must be greater than this. Only applicable to numbers. + + Read more about it in the + [FastAPI docs about Path parameters numeric validations](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#number-validations-greater-than-and-less-than-or-equal) """ ), ] = None, @@ -428,6 +461,9 @@ def Query( # noqa: N802 """ Greater than or equal. If set, value must be greater than or equal to this. Only applicable to numbers. + + Read more about it in the + [FastAPI docs about Path parameters numeric validations](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#number-validations-greater-than-and-less-than-or-equal) """ ), ] = None, @@ -436,6 +472,9 @@ def Query( # noqa: N802 Doc( """ Less than. If set, value must be less than this. Only applicable to numbers. + + Read more about it in the + [FastAPI docs about Path parameters numeric validations](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#number-validations-greater-than-and-less-than-or-equal) """ ), ] = None, @@ -445,6 +484,9 @@ def Query( # noqa: N802 """ Less than or equal. If set, value must be less than or equal to this. Only applicable to numbers. + + Read more about it in the + [FastAPI docs about Path parameters numeric validations](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#number-validations-greater-than-and-less-than-or-equal) """ ), ] = None, @@ -453,6 +495,9 @@ def Query( # noqa: N802 Doc( """ Minimum length for strings. + + Read more about it in the + [FastAPI docs about Query parameters](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/) """ ), ] = None, @@ -461,6 +506,9 @@ def Query( # noqa: N802 Doc( """ Maximum length for strings. + + Read more about it in the + [FastAPI docs about Query parameters](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/) """ ), ] = None, @@ -469,6 +517,9 @@ def Query( # noqa: N802 Doc( """ RegEx pattern for strings. + + Read more about it in the + [FastAPI docs about Query parameters](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#add-regular-expressions """ ), ] = None, @@ -536,6 +587,9 @@ def Query( # noqa: N802 Doc( """ Example values for this field. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/) """ ), ] = None, @@ -570,6 +624,9 @@ def Query( # noqa: N802 Mark this parameter field as deprecated. It will affect the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs about Query parameters](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#deprecating-parameters) """ ), ] = None, @@ -581,6 +638,9 @@ def Query( # noqa: N802 You probably don't need it, but it's available. This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs about Query parameters](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-parameters-from-openapi """ ), ] = True, @@ -849,6 +909,9 @@ def Header( # noqa: N802 Doc( """ Example values for this field. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/) """ ), ] = None, @@ -1152,6 +1215,9 @@ def Cookie( # noqa: N802 Doc( """ Example values for this field. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/) """ ), ] = None, @@ -1477,6 +1543,9 @@ def Body( # noqa: N802 Doc( """ Example values for this field. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/) """ ), ] = None, @@ -1790,6 +1859,9 @@ def Form( # noqa: N802 Doc( """ Example values for this field. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/) """ ), ] = None, @@ -2102,6 +2174,9 @@ def File( # noqa: N802 Doc( """ Example values for this field. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/) """ ), ] = None, @@ -2215,6 +2290,9 @@ def Depends( # noqa: N802 Don't call it directly, FastAPI will call it for you, just pass the object directly. + + Read more about it in the + [FastAPI docs for Dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/) """ ), ] = None, @@ -2230,6 +2308,9 @@ def Depends( # noqa: N802 Set `use_cache` to `False` to disable this behavior and ensure the dependency is called again (if declared more than once) in the same request. + + Read more about it in the + [FastAPI docs about sub-dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/sub-dependencies/#using-the-same-dependency-multiple-times) """ ), ] = True, @@ -2250,6 +2331,9 @@ def Depends( # noqa: N802 that handles the request (similar to when using `"function"`), but end **after** the response is sent back to the client. So, the dependency function will be executed **around** the **request** and response cycle. + + Read more about it in the + [FastAPI docs for FastAPI Dependencies with yield](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#early-exit-and-scope) """ ), ] = None, @@ -2295,6 +2379,9 @@ def Security( # noqa: N802 Don't call it directly, FastAPI will call it for you, just pass the object directly. + + Read more about it in the + [FastAPI docs for Dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/) """ ), ] = None, @@ -2312,7 +2399,9 @@ def Security( # noqa: N802 These scopes are integrated with OpenAPI (and the API docs at `/docs`). So they are visible in the OpenAPI specification. - ) + + Read more about it in the + [FastAPI docs about OAuth2 scopes](https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/) """ ), ] = None, @@ -2327,6 +2416,9 @@ def Security( # noqa: N802 Set `use_cache` to `False` to disable this behavior and ensure the dependency is called again (if declared more than once) in the same request. + + Read more about it in the + [FastAPI docs about sub-dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/sub-dependencies/#using-the-same-dependency-multiple-times) """ ), ] = True, diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index fc49ba190..58ffc5c76 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -68,6 +68,9 @@ class OAuth2PasswordRequestForm: "password". Nevertheless, this dependency class is permissive and allows not passing it. If you want to enforce it, use instead the `OAuth2PasswordRequestFormStrict` dependency. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). """ ), ] = None, @@ -78,6 +81,9 @@ class OAuth2PasswordRequestForm: """ `username` string. The OAuth2 spec requires the exact field name `username`. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). """ ), ], @@ -88,6 +94,9 @@ class OAuth2PasswordRequestForm: """ `password` string. The OAuth2 spec requires the exact field name `password`. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). """ ), ], @@ -112,6 +121,9 @@ class OAuth2PasswordRequestForm: * `users:read` * `profile` * `openid` + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). """ ), ] = "", @@ -222,6 +234,9 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): "password". This dependency is strict about it. If you want to be permissive, use instead the `OAuth2PasswordRequestForm` dependency class. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). """ ), ], @@ -232,6 +247,9 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): """ `username` string. The OAuth2 spec requires the exact field name `username`. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). """ ), ], @@ -242,6 +260,9 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): """ `password` string. The OAuth2 spec requires the exact field name `password`. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). """ ), ], @@ -266,6 +287,9 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): * `users:read` * `profile` * `openid` + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). """ ), ] = "", @@ -423,6 +447,9 @@ class OAuth2PasswordBearer(OAuth2): """ The URL to obtain the OAuth2 token. This would be the *path operation* that has `OAuth2PasswordRequestForm` as a dependency. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). """ ), ], @@ -442,6 +469,9 @@ class OAuth2PasswordBearer(OAuth2): """ The OAuth2 scopes that would be required by the *path operations* that use this dependency. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). """ ), ] = None, From 6ab68c62b86c5d222659a37cb2abbd4a52136a94 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:42:18 +0000 Subject: [PATCH 150/243] =?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 d5bc81668..0c7d44049 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Docs +* 📝 Add links to related sections of docs to docstrings. PR [#14776](https://github.com/fastapi/fastapi/pull/14776) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Update embedded code examples to Python 3.10 syntax. PR [#14758](https://github.com/fastapi/fastapi/pull/14758) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Fix dependency installation command in `docs/en/docs/contributing.md`. PR [#14757](https://github.com/fastapi/fastapi/pull/14757) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Use return type annotation instead of `response_model` when possible. PR [#14753](https://github.com/fastapi/fastapi/pull/14753) by [@YuriiMotov](https://github.com/YuriiMotov). From 08dad5c69fbeac83616226b4d56b6c79b8683767 Mon Sep 17 00:00:00 2001 From: DJ Melisso <DJMcoder@users.noreply.github.com> Date: Wed, 4 Feb 2026 05:23:08 -0800 Subject: [PATCH 151/243] =?UTF-8?q?=F0=9F=90=9B=20Fix=20OpenAPI=20duplicat?= =?UTF-8?q?ion=20of=20`anyOf`=20refs=20for=20app-level=20responses=20with?= =?UTF-8?q?=20specified=20`content`=20and=20`model`=20as=20`Union`=20(#144?= =?UTF-8?q?63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- fastapi/openapi/utils.py | 3 +- ...itional_responses_union_duplicate_anyof.py | 123 ++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tests/test_additional_responses_union_duplicate_anyof.py diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 75ff26102..d56027b50 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -1,3 +1,4 @@ +import copy import http.client import inspect import warnings @@ -377,7 +378,7 @@ def get_openapi_path( additional_status_code, additional_response, ) in route.responses.items(): - process_response = additional_response.copy() + process_response = copy.deepcopy(additional_response) process_response.pop("model", None) status_code_key = str(additional_status_code).upper() if status_code_key == "DEFAULT": diff --git a/tests/test_additional_responses_union_duplicate_anyof.py b/tests/test_additional_responses_union_duplicate_anyof.py new file mode 100644 index 000000000..f5d987ca3 --- /dev/null +++ b/tests/test_additional_responses_union_duplicate_anyof.py @@ -0,0 +1,123 @@ +""" +Regression test: Ensure app-level responses with Union models and content/examples +don't accumulate duplicate $ref entries in anyOf arrays. +See https://github.com/fastapi/fastapi/pull/14463 +""" + +from typing import Union + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class ModelA(BaseModel): + a: str + + +class ModelB(BaseModel): + b: str + + +app = FastAPI( + responses={ + 500: { + "model": Union[ModelA, ModelB], + "content": {"application/json": {"examples": {"Case A": {"value": "a"}}}}, + } + } +) + + +@app.get("/route1") +async def route1(): + pass # pragma: no cover + + +@app.get("/route2") +async def route2(): + pass # pragma: no cover + + +client = TestClient(app) + + +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": { + "/route1": { + "get": { + "summary": "Route1", + "operationId": "route1_route1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {"$ref": "#/components/schemas/ModelA"}, + {"$ref": "#/components/schemas/ModelB"}, + ], + "title": "Response 500 Route1 Route1 Get", + }, + "examples": {"Case A": {"value": "a"}}, + } + }, + }, + }, + } + }, + "/route2": { + "get": { + "summary": "Route2", + "operationId": "route2_route2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {"$ref": "#/components/schemas/ModelA"}, + {"$ref": "#/components/schemas/ModelB"}, + ], + "title": "Response 500 Route2 Route2 Get", + }, + "examples": {"Case A": {"value": "a"}}, + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "ModelA": { + "properties": {"a": {"type": "string", "title": "A"}}, + "type": "object", + "required": ["a"], + "title": "ModelA", + }, + "ModelB": { + "properties": {"b": {"type": "string", "title": "B"}}, + "type": "object", + "required": ["b"], + "title": "ModelB", + }, + } + }, + } From ca4692a8c6552de17646a4eabed8ea3cafce597b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:23:34 +0000 Subject: [PATCH 152/243] =?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 0c7d44049..59cf84c86 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix OpenAPI duplication of `anyOf` refs for app-level responses with specified `content` and `model` as `Union`. PR [#14463](https://github.com/fastapi/fastapi/pull/14463) by [@DJMcoder](https://github.com/DJMcoder). + ### Refactors * 💡 Update comment for Pydantic internals. PR [#14814](https://github.com/fastapi/fastapi/pull/14814) by [@tiangolo](https://github.com/tiangolo). From 41352de24c41f3df540ea410696aa2116ab59394 Mon Sep 17 00:00:00 2001 From: Anton <34218036+retwish@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:24:59 +0100 Subject: [PATCH 153/243] =?UTF-8?q?=F0=9F=9A=B8=20Improve=20error=20messag?= =?UTF-8?q?e=20for=20invalid=20query=20parameter=20type=20annotations=20(#?= =?UTF-8?q?14479)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Anton.D <anton.dehtiarenko@chdp-tech.net> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- fastapi/dependencies/utils.py | 2 +- tests/test_invalid_sequence_param.py | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 2afc734ba..b647818c4 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -519,7 +519,7 @@ def analyze_param( # For Pydantic v1 and getattr(field, "shape", 1) == 1 ) - ) + ), f"Query parameter {param_name!r} must be one of the supported types" return ParamDetails(type_annotation=type_annotation, depends=depends, field=field) diff --git a/tests/test_invalid_sequence_param.py b/tests/test_invalid_sequence_param.py index 2b8fd059e..3695344f7 100644 --- a/tests/test_invalid_sequence_param.py +++ b/tests/test_invalid_sequence_param.py @@ -6,7 +6,10 @@ from pydantic import BaseModel def test_invalid_sequence(): - with pytest.raises(AssertionError): + with pytest.raises( + AssertionError, + match="Query parameter 'q' must be one of the supported types", + ): app = FastAPI() class Item(BaseModel): @@ -18,7 +21,10 @@ def test_invalid_sequence(): def test_invalid_tuple(): - with pytest.raises(AssertionError): + with pytest.raises( + AssertionError, + match="Query parameter 'q' must be one of the supported types", + ): app = FastAPI() class Item(BaseModel): @@ -30,7 +36,10 @@ def test_invalid_tuple(): def test_invalid_dict(): - with pytest.raises(AssertionError): + with pytest.raises( + AssertionError, + match="Query parameter 'q' must be one of the supported types", + ): app = FastAPI() class Item(BaseModel): @@ -42,7 +51,10 @@ def test_invalid_dict(): def test_invalid_simple_dict(): - with pytest.raises(AssertionError): + with pytest.raises( + AssertionError, + match="Query parameter 'q' must be one of the supported types", + ): app = FastAPI() class Item(BaseModel): From 9e0f4ca77ae27798764766fb5dfd48fb15812f86 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:25:27 +0000 Subject: [PATCH 154/243] =?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 59cf84c86..0f0b9dd0a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* 🚸 Improve error message for invalid query parameter type annotations. PR [#14479](https://github.com/fastapi/fastapi/pull/14479) by [@retwish](https://github.com/retwish). + ### Fixes * 🐛 Fix OpenAPI duplication of `anyOf` refs for app-level responses with specified `content` and `model` as `Union`. PR [#14463](https://github.com/fastapi/fastapi/pull/14463) by [@DJMcoder](https://github.com/DJMcoder). From 61f95c9606ff548d449e9d1e0612d93e2ec3c97f Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:26:02 +0300 Subject: [PATCH 155/243] =?UTF-8?q?=F0=9F=93=9D=20Add=20banner=20to=20tran?= =?UTF-8?q?slated=20pages=20(#14809)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add banner to translated pages * Add link to English version. Use modern syntax for details block * 🎨 Auto format * Move `translation-banner.md` inside `docs` directory * 🎨 Auto format --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- docs/en/docs/translation-banner.md | 11 +++++++++++ docs/ru/docs/translation-banner.md | 11 +++++++++++ scripts/mkdocs_hooks.py | 30 +++++++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 docs/en/docs/translation-banner.md create mode 100644 docs/ru/docs/translation-banner.md diff --git a/docs/en/docs/translation-banner.md b/docs/en/docs/translation-banner.md new file mode 100644 index 000000000..142287074 --- /dev/null +++ b/docs/en/docs/translation-banner.md @@ -0,0 +1,11 @@ +/// details | 🌐 Translation by AI and humans + +This translation was made by AI guided by humans. 🤝 + +It could have mistakes of misunderstanding the original meaning, or looking unnatural, etc. 🤖 + +You can improve this translation by [helping us guide the AI LLM better](https://fastapi.tiangolo.com/contributing/#translations). + +[English version](ENGLISH_VERSION_URL) + +/// diff --git a/docs/ru/docs/translation-banner.md b/docs/ru/docs/translation-banner.md new file mode 100644 index 000000000..78ebd676c --- /dev/null +++ b/docs/ru/docs/translation-banner.md @@ -0,0 +1,11 @@ +/// details | 🌐 Перевод выполнен с помощью ИИ и людей + +Этот перевод был сделан ИИ под руководством людей. 🤝 + +В нем могут быть ошибки из-за неправильного понимания оригинального смысла или неестественности и т. д. 🤖 + +Вы можете улучшить этот перевод, [помогая нам лучше направлять ИИ LLM](https://fastapi.tiangolo.com/ru/contributing/#translations). + +[Английская версия](ENGLISH_VERSION_URL) + +/// diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index 4b781270a..567c0111d 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -26,6 +26,17 @@ def get_missing_translation_content(docs_dir: str) -> str: return missing_translation_path.read_text(encoding="utf-8") +@lru_cache +def get_translation_banner_content(docs_dir: str) -> str: + docs_dir_path = Path(docs_dir) + translation_banner_path = docs_dir_path / "translation-banner.md" + if not translation_banner_path.is_file(): + translation_banner_path = ( + docs_dir_path.parent.parent / "en" / "docs" / "translation-banner.md" + ) + return translation_banner_path.read_text(encoding="utf-8") + + @lru_cache def get_mkdocs_material_langs() -> list[str]: material_path = Path(material.__file__).parent @@ -151,4 +162,21 @@ def on_page_markdown( if markdown.startswith("#"): header, _, body = markdown.partition("\n\n") return f"{header}\n\n{missing_translation_content}\n\n{body}" - return markdown + + docs_dir_path = Path(config.docs_dir) + en_docs_dir_path = docs_dir_path.parent.parent / "en/docs" + + if docs_dir_path == en_docs_dir_path: + return markdown + + # For translated pages add translation banner + translation_banner_content = get_translation_banner_content(config.docs_dir) + en_url = "https://fastapi.tiangolo.com/" + page.url.lstrip("/") + translation_banner_content = translation_banner_content.replace( + "ENGLISH_VERSION_URL", en_url + ) + header = "" + body = markdown + if markdown.startswith("#"): + header, _, body = markdown.partition("\n\n") + return f"{header}\n\n{translation_banner_content}\n\n{body}" From a4297066c22071e89f55fcb9f720dec57bd95814 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:26:29 +0000 Subject: [PATCH 156/243] =?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 0f0b9dd0a..7658ab970 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -21,6 +21,7 @@ hide: ### Docs +* 📝 Add banner to translated pages. PR [#14809](https://github.com/fastapi/fastapi/pull/14809) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Add links to related sections of docs to docstrings. PR [#14776](https://github.com/fastapi/fastapi/pull/14776) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Update embedded code examples to Python 3.10 syntax. PR [#14758](https://github.com/fastapi/fastapi/pull/14758) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Fix dependency installation command in `docs/en/docs/contributing.md`. PR [#14757](https://github.com/fastapi/fastapi/pull/14757) by [@YuriiMotov](https://github.com/YuriiMotov). From 3ee652dd0c9e5c1bc38b6cadb6a2a036866c0dd3 Mon Sep 17 00:00:00 2001 From: johnson-earls <125391700+johnson-earls@users.noreply.github.com> Date: Wed, 4 Feb 2026 05:29:02 -0800 Subject: [PATCH 157/243] =?UTF-8?q?=F0=9F=93=9D=20Fix=20example=20of=20lic?= =?UTF-8?q?ense=20identifier=20in=20documentation=20(#14492)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: svlandeg <svlandeg@github.com> --- docs_src/metadata/tutorial001_1_py39.py | 2 +- tests/test_tutorial/test_metadata/test_tutorial001_1.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs_src/metadata/tutorial001_1_py39.py b/docs_src/metadata/tutorial001_1_py39.py index a8f5b9458..419232d86 100644 --- a/docs_src/metadata/tutorial001_1_py39.py +++ b/docs_src/metadata/tutorial001_1_py39.py @@ -28,7 +28,7 @@ app = FastAPI( }, license_info={ "name": "Apache 2.0", - "identifier": "MIT", + "identifier": "Apache-2.0", }, ) diff --git a/tests/test_tutorial/test_metadata/test_tutorial001_1.py b/tests/test_tutorial/test_metadata/test_tutorial001_1.py index 40878ccfd..10cb35c54 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial001_1.py +++ b/tests/test_tutorial/test_metadata/test_tutorial001_1.py @@ -28,7 +28,7 @@ def test_openapi_schema(): }, "license": { "name": "Apache 2.0", - "identifier": "MIT", + "identifier": "Apache-2.0", }, "version": "0.0.1", }, From b134f406d1ef4c0b2241f7a0402f3fcb9e99dc8c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:29:24 +0000 Subject: [PATCH 158/243] =?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 7658ab970..95d0d794b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -21,6 +21,7 @@ hide: ### Docs +* 📝 Fix example of license identifier in documentation. PR [#14492](https://github.com/fastapi/fastapi/pull/14492) by [@johnson-earls](https://github.com/johnson-earls). * 📝 Add banner to translated pages. PR [#14809](https://github.com/fastapi/fastapi/pull/14809) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Add links to related sections of docs to docstrings. PR [#14776](https://github.com/fastapi/fastapi/pull/14776) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Update embedded code examples to Python 3.10 syntax. PR [#14758](https://github.com/fastapi/fastapi/pull/14758) by [@YuriiMotov](https://github.com/YuriiMotov). From 9df1f8293d979506cd75280ed4812658323c2c98 Mon Sep 17 00:00:00 2001 From: Tima <126619011+timakaa@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:32:24 +0300 Subject: [PATCH 159/243] =?UTF-8?q?=F0=9F=93=9D=20Fix=20typing=20issue=20i?= =?UTF-8?q?n=20`docs=5Fsrc/app=5Ftesting/app=5Fb`=20code=20example=20(#145?= =?UTF-8?q?73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- docs_src/app_testing/app_b_an_py310/main.py | 2 +- docs_src/app_testing/app_b_an_py39/main.py | 2 +- docs_src/app_testing/app_b_py310/main.py | 2 +- docs_src/app_testing/app_b_py39/main.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs_src/app_testing/app_b_an_py310/main.py b/docs_src/app_testing/app_b_an_py310/main.py index 120289f56..1b77dd137 100644 --- a/docs_src/app_testing/app_b_an_py310/main.py +++ b/docs_src/app_testing/app_b_an_py310/main.py @@ -34,5 +34,5 @@ async def create_item(item: Item, x_token: Annotated[str, Header()]) -> Item: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: raise HTTPException(status_code=409, detail="Item already exists") - fake_db[item.id] = item + fake_db[item.id] = item.model_dump() return item diff --git a/docs_src/app_testing/app_b_an_py39/main.py b/docs_src/app_testing/app_b_an_py39/main.py index 801d5f21e..42026a81a 100644 --- a/docs_src/app_testing/app_b_an_py39/main.py +++ b/docs_src/app_testing/app_b_an_py39/main.py @@ -34,5 +34,5 @@ async def create_item(item: Item, x_token: Annotated[str, Header()]) -> Item: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: raise HTTPException(status_code=409, detail="Item already exists") - fake_db[item.id] = item + fake_db[item.id] = item.model_dump() return item diff --git a/docs_src/app_testing/app_b_py310/main.py b/docs_src/app_testing/app_b_py310/main.py index 6c5c34130..83f6fa142 100644 --- a/docs_src/app_testing/app_b_py310/main.py +++ b/docs_src/app_testing/app_b_py310/main.py @@ -32,5 +32,5 @@ async def create_item(item: Item, x_token: str = Header()) -> Item: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: raise HTTPException(status_code=409, detail="Item already exists") - fake_db[item.id] = item + fake_db[item.id] = item.model_dump() return item diff --git a/docs_src/app_testing/app_b_py39/main.py b/docs_src/app_testing/app_b_py39/main.py index 89053c432..ed38f4721 100644 --- a/docs_src/app_testing/app_b_py39/main.py +++ b/docs_src/app_testing/app_b_py39/main.py @@ -34,5 +34,5 @@ async def create_item(item: Item, x_token: str = Header()) -> Item: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: raise HTTPException(status_code=409, detail="Item already exists") - fake_db[item.id] = item + fake_db[item.id] = item.model_dump() return item From a1bb70e5a5a29cea2c3fce50ab3510b341fdaf8c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:32:48 +0000 Subject: [PATCH 160/243] =?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 95d0d794b..aa516c0d6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -21,6 +21,7 @@ hide: ### Docs +* 📝 Fix typing issue in `docs_src/app_testing/app_b` code example. PR [#14573](https://github.com/fastapi/fastapi/pull/14573) by [@timakaa](https://github.com/timakaa). * 📝 Fix example of license identifier in documentation. PR [#14492](https://github.com/fastapi/fastapi/pull/14492) by [@johnson-earls](https://github.com/johnson-earls). * 📝 Add banner to translated pages. PR [#14809](https://github.com/fastapi/fastapi/pull/14809) by [@YuriiMotov](https://github.com/YuriiMotov). * 📝 Add links to related sections of docs to docstrings. PR [#14776](https://github.com/fastapi/fastapi/pull/14776) by [@YuriiMotov](https://github.com/YuriiMotov). From 0748214c43d0793b56a309199f45b9bf78c0da26 Mon Sep 17 00:00:00 2001 From: mvanderlee <918128+mvanderlee@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:34:01 +0100 Subject: [PATCH 161/243] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20Re-export=20`In?= =?UTF-8?q?cEx`=20type=20from=20Pydantic=20instead=20of=20duplicating=20it?= =?UTF-8?q?=20(#14641)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/types.py b/fastapi/types.py index d3e980cb4..1c3a6de74 100644 --- a/fastapi/types.py +++ b/fastapi/types.py @@ -3,9 +3,9 @@ from enum import Enum from typing import Any, Callable, Optional, TypeVar, Union from pydantic import BaseModel +from pydantic.main import IncEx as IncEx DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]) UnionType = getattr(types, "UnionType", Union) ModelNameMap = dict[Union[type[BaseModel], type[Enum]], str] -IncEx = Union[set[int], set[str], dict[int, Any], dict[str, Any]] DependencyCacheKey = tuple[Optional[Callable[..., Any]], tuple[str, ...], str] From 9656e925a9b53dbb288c24f5dd4cea805c02429b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:34:29 +0000 Subject: [PATCH 162/243] =?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 aa516c0d6..12a9ac8a1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Refactors +* 🏷️ Re-export `IncEx` type from Pydantic instead of duplicating it. PR [#14641](https://github.com/fastapi/fastapi/pull/14641) by [@mvanderlee](https://github.com/mvanderlee). * 💡 Update comment for Pydantic internals. PR [#14814](https://github.com/fastapi/fastapi/pull/14814) by [@tiangolo](https://github.com/tiangolo). ### Docs From 741f77d571eeeb2cde4fe11dcfe038c4d925a046 Mon Sep 17 00:00:00 2001 From: Joab <joab0@proton.me> Date: Wed, 4 Feb 2026 10:35:58 -0300 Subject: [PATCH 163/243] =?UTF-8?q?=E2=9C=A8=20Add=20`viewport`=20meta=20t?= =?UTF-8?q?ag=20to=20improve=20Swagger=20UI=20on=20mobile=20devices=20(#14?= =?UTF-8?q?777)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/openapi/docs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index 632cc2c1d..bb387c609 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -139,6 +139,7 @@ def get_swagger_ui_html( <!DOCTYPE html> <html> <head> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link type="text/css" rel="stylesheet" href="{swagger_css_url}"> <link rel="shortcut icon" href="{swagger_favicon_url}"> <title>{title} From 3675e284abfae4f43dd321301ddfa45bcc494056 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 13:36:24 +0000 Subject: [PATCH 164/243] =?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 12a9ac8a1..45e6cf1c2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Features +* ✨ Add `viewport` meta tag to improve Swagger UI on mobile devices. PR [#14777](https://github.com/fastapi/fastapi/pull/14777) by [@Joab0](https://github.com/Joab0). * 🚸 Improve error message for invalid query parameter type annotations. PR [#14479](https://github.com/fastapi/fastapi/pull/14479) by [@retwish](https://github.com/retwish). ### Fixes From 1d96b3e3f14fa5a226ae817d1095b868796beb60 Mon Sep 17 00:00:00 2001 From: Cecilia Madrid <61908819+WaveTheory1@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:46:46 +0100 Subject: [PATCH 165/243] =?UTF-8?q?=F0=9F=90=9B=20Strip=20whitespaces=20fr?= =?UTF-8?q?om=20`Authorization`=20header=20credentials=20(#14786)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/security/utils.py | 2 +- tests/test_security_http_base.py | 6 ++++++ tests/test_security_oauth2_authorization_code_bearer.py | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/fastapi/security/utils.py b/fastapi/security/utils.py index 002e68b44..fd349aec7 100644 --- a/fastapi/security/utils.py +++ b/fastapi/security/utils.py @@ -7,4 +7,4 @@ def get_authorization_scheme_param( if not authorization_header_value: return "", "" scheme, _, param = authorization_header_value.partition(" ") - return scheme, param + return scheme, param.strip() diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index 8cf259a75..ac38ee718 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -21,6 +21,12 @@ def test_security_http_base(): assert response.json() == {"scheme": "Other", "credentials": "foobar"} +def test_security_http_base_with_whitespaces(): + response = client.get("/users/me", headers={"Authorization": "Other foobar "}) + assert response.status_code == 200, response.text + assert response.json() == {"scheme": "Other", "credentials": "foobar"} + + def test_security_http_base_no_credentials(): response = client.get("/users/me") assert response.status_code == 401, response.text diff --git a/tests/test_security_oauth2_authorization_code_bearer.py b/tests/test_security_oauth2_authorization_code_bearer.py index f2097b149..66f53ab00 100644 --- a/tests/test_security_oauth2_authorization_code_bearer.py +++ b/tests/test_security_oauth2_authorization_code_bearer.py @@ -37,6 +37,12 @@ def test_token(): assert response.json() == {"token": "testtoken"} +def test_token_with_whitespaces(): + response = client.get("/items", headers={"Authorization": "Bearer testtoken "}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text From c944add5a9680f7b3dea3a5bab4787949671d2c4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 13:47:09 +0000 Subject: [PATCH 166/243] =?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 45e6cf1c2..091a4c0aa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Fixes +* 🐛 Strip whitespaces from `Authorization` header credentials. PR [#14786](https://github.com/fastapi/fastapi/pull/14786) by [@WaveTheory1](https://github.com/WaveTheory1). * 🐛 Fix OpenAPI duplication of `anyOf` refs for app-level responses with specified `content` and `model` as `Union`. PR [#14463](https://github.com/fastapi/fastapi/pull/14463) by [@DJMcoder](https://github.com/DJMcoder). ### Refactors From 09f5941f0e18db2b28b40d35a5da7a94c23eb9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Gu=C3=A9rin?= Date: Wed, 4 Feb 2026 14:49:44 +0100 Subject: [PATCH 167/243] =?UTF-8?q?=F0=9F=90=9B=20Fix=20TYPE=5FCHECKING=20?= =?UTF-8?q?annotations=20for=20Python=203.14=20(PEP=20649)=20(#14789)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 7 ++++- ...stringified_annotation_dependency_py314.py | 30 +++++++++++++++++++ .../test_dependencies/test_tutorial008.py | 10 +++++-- tests/utils.py | 4 +-- 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 tests/test_stringified_annotation_dependency_py314.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index b647818c4..fc5dfed85 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -204,7 +204,12 @@ def _get_signature(call: Callable[..., Any]) -> inspect.Signature: except NameError: # Handle type annotations with if TYPE_CHECKING, not used by FastAPI # e.g. dependency return types - signature = inspect.signature(call) + if sys.version_info >= (3, 14): + from annotationlib import Format + + signature = inspect.signature(call, annotation_format=Format.FORWARDREF) + else: + signature = inspect.signature(call) else: signature = inspect.signature(call) return signature diff --git a/tests/test_stringified_annotation_dependency_py314.py b/tests/test_stringified_annotation_dependency_py314.py new file mode 100644 index 000000000..da9b429fc --- /dev/null +++ b/tests/test_stringified_annotation_dependency_py314.py @@ -0,0 +1,30 @@ +from typing import TYPE_CHECKING, Annotated + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +from .utils import needs_py314 + +if TYPE_CHECKING: # pragma: no cover + + class DummyUser: ... + + +@needs_py314 +def test_stringified_annotation(): + # python3.14: Use forward reference without "from __future__ import annotations" + async def get_current_user() -> DummyUser | None: + return None + + app = FastAPI() + + client = TestClient(app) + + @app.get("/") + async def get( + current_user: Annotated[DummyUser | None, Depends(get_current_user)], + ) -> str: + return "hello world" + + response = client.get("/") + assert response.status_code == 200 diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008.py b/tests/test_tutorial/test_dependencies/test_tutorial008.py index 9d7377ebe..5a2d226bf 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial008.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial008.py @@ -1,4 +1,5 @@ import importlib +import sys from types import ModuleType from typing import Annotated, Any from unittest.mock import Mock, patch @@ -12,8 +13,13 @@ from fastapi.testclient import TestClient name="module", params=[ "tutorial008_py39", - # Fails with `NameError: name 'DepA' is not defined` - pytest.param("tutorial008_an_py39", marks=pytest.mark.xfail), + pytest.param( + "tutorial008_an_py39", + marks=pytest.mark.xfail( + sys.version_info < (3, 14), + reason="Fails with `NameError: name 'DepA' is not defined`", + ), + ), ], ) def get_module(request: pytest.FixtureRequest): diff --git a/tests/utils.py b/tests/utils.py index efa0bfd52..4cbfee79f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,8 +6,8 @@ needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires pyth needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) -needs_py_lt_314 = pytest.mark.skipif( - sys.version_info >= (3, 14), reason="requires python3.13-" +needs_py314 = pytest.mark.skipif( + sys.version_info < (3, 14), reason="requires python3.14+" ) From 5d50b7491579d6b9e9a30cdbe24b08f70dcb8d61 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 13:51:56 +0000 Subject: [PATCH 168/243] =?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 091a4c0aa..eff732da1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Fixes +* 🐛 Fix TYPE_CHECKING annotations for Python 3.14 (PEP 649). PR [#14789](https://github.com/fastapi/fastapi/pull/14789) by [@mgu](https://github.com/mgu). * 🐛 Strip whitespaces from `Authorization` header credentials. PR [#14786](https://github.com/fastapi/fastapi/pull/14786) by [@WaveTheory1](https://github.com/WaveTheory1). * 🐛 Fix OpenAPI duplication of `anyOf` refs for app-level responses with specified `content` and `model` as `Union`. PR [#14463](https://github.com/fastapi/fastapi/pull/14463) by [@DJMcoder](https://github.com/DJMcoder). From 440bfd70a948c187dadb2e89ee7e94933e867a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 4 Feb 2026 06:20:36 -0800 Subject: [PATCH 169/243] =?UTF-8?q?=F0=9F=8E=A8=20Tweak=20types=20for=20my?= =?UTF-8?q?py=20(#14816)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/encoders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index e8610c983..b4661be4f 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -219,9 +219,9 @@ def jsonable_encoder( if isinstance(obj, encoder_type): return encoder_instance(obj) if include is not None and not isinstance(include, (set, dict)): - include = set(include) + include = set(include) # type: ignore[assignment] if exclude is not None and not isinstance(exclude, (set, dict)): - exclude = set(exclude) + exclude = set(exclude) # type: ignore[assignment] if isinstance(obj, BaseModel): obj_dict = obj.model_dump( mode="json", From 1e5e8b44cb042f774a8637f27bce93b4ceb68f7d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 14:21:00 +0000 Subject: [PATCH 170/243] =?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 eff732da1..8d90983d4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Refactors +* 🎨 Tweak types for mypy. PR [#14816](https://github.com/fastapi/fastapi/pull/14816) by [@tiangolo](https://github.com/tiangolo). * 🏷️ Re-export `IncEx` type from Pydantic instead of duplicating it. PR [#14641](https://github.com/fastapi/fastapi/pull/14641) by [@mvanderlee](https://github.com/mvanderlee). * 💡 Update comment for Pydantic internals. PR [#14814](https://github.com/fastapi/fastapi/pull/14814) by [@tiangolo](https://github.com/tiangolo). From 75c47187f31b6f60e59dda2d6db26984043ab95c Mon Sep 17 00:00:00 2001 From: Jonathan Fulton Date: Wed, 4 Feb 2026 09:34:02 -0500 Subject: [PATCH 171/243] =?UTF-8?q?=F0=9F=90=9B=20Update=20`ValidationErro?= =?UTF-8?q?r`=20schema=20to=20include=20`input`=20and=20`ctx`=20(#14791)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- fastapi/openapi/utils.py | 2 ++ tests/test_additional_properties.py | 2 ++ tests/test_additional_properties_bool.py | 2 ++ ...dditional_responses_custom_model_in_callback.py | 2 ++ ...additional_responses_default_validationerror.py | 2 ++ tests/test_annotated.py | 2 ++ tests/test_application.py | 2 ++ tests/test_dependency_duplicates.py | 2 ++ tests/test_enforce_once_required_parameter.py | 2 ++ tests/test_extra_routes.py | 2 ++ tests/test_filter_pydantic_sub_model_pv2.py | 2 ++ tests/test_forms_single_param.py | 2 ++ tests/test_generate_unique_id_function.py | 14 ++++++++++++++ .../test_get_model_definitions_formfeed_escape.py | 2 ++ tests/test_get_request_body.py | 2 ++ tests/test_include_router_defaults_overrides.py | 2 ++ tests/test_infer_param_optionality.py | 2 ++ tests/test_modules_same_name_body/test_main.py | 2 ++ tests/test_multi_body_errors.py | 2 ++ tests/test_multi_query_errors.py | 2 ++ tests/test_no_schema_split.py | 2 ++ tests/test_openapi_examples.py | 2 ++ tests/test_openapi_query_parameter_extension.py | 2 ++ .../test_openapi_separate_input_output_schemas.py | 4 ++++ tests/test_param_in_path_and_dependency.py | 2 ++ tests/test_param_include_in_schema.py | 2 ++ tests/test_put_no_body.py | 2 ++ tests/test_regex_deprecated_body.py | 2 ++ tests/test_regex_deprecated_params.py | 2 ++ tests/test_repeated_dependency_schema.py | 2 ++ tests/test_repeated_parameter_alias.py | 2 ++ tests/test_reponse_set_reponse_code_empty.py | 2 ++ tests/test_request_body_parameters_media_type.py | 2 ++ tests/test_schema_extra_examples.py | 2 ++ tests/test_security_oauth2.py | 2 ++ tests/test_security_oauth2_optional.py | 2 ++ tests/test_security_oauth2_optional_description.py | 2 ++ tests/test_starlette_exception.py | 2 ++ tests/test_sub_callbacks.py | 2 ++ tests/test_tuples.py | 2 ++ .../test_additional_responses/test_tutorial001.py | 2 ++ .../test_additional_responses/test_tutorial002.py | 2 ++ .../test_additional_responses/test_tutorial003.py | 2 ++ .../test_additional_responses/test_tutorial004.py | 2 ++ .../test_bigger_applications/test_main.py | 2 ++ tests/test_tutorial/test_body/test_tutorial001.py | 2 ++ tests/test_tutorial/test_body/test_tutorial002.py | 2 ++ tests/test_tutorial/test_body/test_tutorial003.py | 2 ++ tests/test_tutorial/test_body/test_tutorial004.py | 2 ++ .../test_body_fields/test_tutorial001.py | 2 ++ .../test_body_multiple_params/test_tutorial001.py | 2 ++ .../test_body_multiple_params/test_tutorial002.py | 2 ++ .../test_body_multiple_params/test_tutorial003.py | 2 ++ .../test_body_multiple_params/test_tutorial004.py | 2 ++ .../test_body_multiple_params/test_tutorial005.py | 2 ++ .../test_tutorial001_tutorial002_tutorial003.py | 2 ++ .../test_body_nested_models/test_tutorial004.py | 2 ++ .../test_body_nested_models/test_tutorial005.py | 2 ++ .../test_body_nested_models/test_tutorial006.py | 2 ++ .../test_body_nested_models/test_tutorial007.py | 2 ++ .../test_body_nested_models/test_tutorial008.py | 2 ++ .../test_body_nested_models/test_tutorial009.py | 2 ++ .../test_body_updates/test_tutorial001.py | 2 ++ .../test_body_updates/test_tutorial002.py | 2 ++ .../test_cookie_param_models/test_tutorial001.py | 2 ++ .../test_cookie_param_models/test_tutorial002.py | 2 ++ .../test_cookie_params/test_tutorial001.py | 2 ++ .../test_dataclasses/test_tutorial001.py | 2 ++ .../test_dataclasses/test_tutorial003.py | 2 ++ .../test_tutorial001_tutorial001_02.py | 2 ++ .../test_tutorial002_tutorial003_tutorial004.py | 2 ++ .../test_dependencies/test_tutorial005.py | 2 ++ .../test_dependencies/test_tutorial006.py | 2 ++ .../test_dependencies/test_tutorial011.py | 2 ++ .../test_dependencies/test_tutorial012.py | 2 ++ .../test_tutorial/test_encoder/test_tutorial001.py | 2 ++ .../test_tutorial/test_events/test_tutorial001.py | 2 ++ .../test_tutorial/test_events/test_tutorial003.py | 2 ++ .../test_extra_data_types/test_tutorial001.py | 2 ++ .../test_tutorial001_tutorial002.py | 2 ++ .../test_extra_models/test_tutorial003.py | 2 ++ .../test_generate_clients/test_tutorial001.py | 2 ++ .../test_generate_clients/test_tutorial002.py | 2 ++ .../test_generate_clients/test_tutorial003.py | 2 ++ .../test_generate_clients/test_tutorial004.py | 2 ++ .../test_handling_errors/test_tutorial001.py | 2 ++ .../test_handling_errors/test_tutorial002.py | 2 ++ .../test_handling_errors/test_tutorial003.py | 2 ++ .../test_handling_errors/test_tutorial004.py | 2 ++ .../test_handling_errors/test_tutorial005.py | 2 ++ .../test_handling_errors/test_tutorial006.py | 2 ++ .../test_header_param_models/test_tutorial001.py | 2 ++ .../test_header_param_models/test_tutorial002.py | 2 ++ .../test_header_param_models/test_tutorial003.py | 2 ++ .../test_header_params/test_tutorial001.py | 2 ++ .../test_header_params/test_tutorial002.py | 2 ++ .../test_header_params/test_tutorial003.py | 2 ++ .../test_openapi_callbacks/test_tutorial001.py | 2 ++ .../test_openapi_webhooks/test_tutorial001.py | 2 ++ .../test_tutorial004.py | 2 ++ .../test_tutorial001.py | 2 ++ .../test_tutorial002.py | 2 ++ .../test_tutorial003_tutorial004.py | 2 ++ .../test_tutorial005.py | 2 ++ .../test_path_params/test_tutorial001.py | 2 ++ .../test_path_params/test_tutorial002.py | 2 ++ .../test_path_params/test_tutorial003.py | 2 ++ .../test_path_params/test_tutorial004.py | 2 ++ .../test_path_params/test_tutorial005.py | 2 ++ .../test_tutorial001.py | 2 ++ .../test_tutorial002_tutorial003.py | 2 ++ .../test_tutorial004.py | 2 ++ .../test_tutorial005.py | 2 ++ .../test_tutorial006.py | 2 ++ .../test_query_param_models/test_tutorial001.py | 2 ++ .../test_query_param_models/test_tutorial002.py | 2 ++ .../test_query_params/test_tutorial001.py | 2 ++ .../test_query_params/test_tutorial002.py | 2 ++ .../test_query_params/test_tutorial003.py | 2 ++ .../test_query_params/test_tutorial004.py | 2 ++ .../test_query_params/test_tutorial005.py | 2 ++ .../test_query_params/test_tutorial006.py | 2 ++ .../test_tutorial001.py | 2 ++ .../test_tutorial002.py | 2 ++ .../test_tutorial003.py | 2 ++ .../test_tutorial004.py | 2 ++ .../test_tutorial005.py | 2 ++ .../test_tutorial006.py | 2 ++ .../test_tutorial006c.py | 2 ++ .../test_tutorial007.py | 2 ++ .../test_tutorial008.py | 2 ++ .../test_tutorial009.py | 2 ++ .../test_tutorial010.py | 2 ++ .../test_tutorial011.py | 2 ++ .../test_tutorial012.py | 2 ++ .../test_tutorial013.py | 2 ++ .../test_tutorial014.py | 2 ++ .../test_tutorial015.py | 2 ++ .../test_request_files/test_tutorial001.py | 2 ++ .../test_request_files/test_tutorial001_02.py | 2 ++ .../test_request_files/test_tutorial001_03.py | 2 ++ .../test_request_files/test_tutorial002.py | 2 ++ .../test_request_files/test_tutorial003.py | 2 ++ .../test_request_form_models/test_tutorial001.py | 2 ++ .../test_request_form_models/test_tutorial002.py | 2 ++ .../test_request_forms/test_tutorial001.py | 2 ++ .../test_tutorial001.py | 2 ++ .../test_response_directly/test_tutorial001.py | 2 ++ .../test_tutorial001_tutorial001_01.py | 2 ++ .../test_response_model/test_tutorial002.py | 2 ++ .../test_response_model/test_tutorial003.py | 2 ++ .../test_response_model/test_tutorial003_01.py | 2 ++ .../test_response_model/test_tutorial003_02.py | 2 ++ .../test_response_model/test_tutorial003_05.py | 2 ++ .../test_response_model/test_tutorial004.py | 2 ++ .../test_response_model/test_tutorial005.py | 2 ++ .../test_response_model/test_tutorial006.py | 2 ++ .../test_tutorial001_tutorial002.py | 2 ++ .../test_schema_extra_example/test_tutorial001.py | 2 ++ .../test_schema_extra_example/test_tutorial002.py | 2 ++ .../test_schema_extra_example/test_tutorial003.py | 2 ++ .../test_schema_extra_example/test_tutorial004.py | 2 ++ .../test_schema_extra_example/test_tutorial005.py | 2 ++ .../test_security/test_tutorial003.py | 2 ++ .../test_security/test_tutorial004.py | 2 ++ .../test_security/test_tutorial005.py | 2 ++ .../test_tutorial001.py | 2 ++ .../test_tutorial002.py | 2 ++ .../test_sql_databases/test_tutorial001.py | 2 ++ .../test_sql_databases/test_tutorial002.py | 2 ++ .../test_tutorial001.py | 2 ++ tests/test_union_body.py | 2 ++ tests/test_union_body_discriminator.py | 2 ++ tests/test_union_body_discriminator_annotated.py | 2 ++ tests/test_union_forms.py | 2 ++ tests/test_union_inherited_body.py | 2 ++ tests/test_webhooks_security.py | 2 ++ 177 files changed, 368 insertions(+) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index d56027b50..5736af3b7 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -50,6 +50,8 @@ validation_error_definition = { }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, "required": ["loc", "msg", "type"], } diff --git a/tests/test_additional_properties.py b/tests/test_additional_properties.py index 262236640..935acca42 100644 --- a/tests/test_additional_properties.py +++ b/tests/test_additional_properties.py @@ -89,6 +89,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py index 063297a3f..3b323ad46 100644 --- a/tests/test_additional_properties_bool.py +++ b/tests/test_additional_properties_bool.py @@ -101,6 +101,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_additional_responses_custom_model_in_callback.py b/tests/test_additional_responses_custom_model_in_callback.py index 376d7714e..3a37c924d 100644 --- a/tests/test_additional_responses_custom_model_in_callback.py +++ b/tests/test_additional_responses_custom_model_in_callback.py @@ -128,6 +128,8 @@ def test_openapi_schema(): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_additional_responses_default_validationerror.py b/tests/test_additional_responses_default_validationerror.py index 153f04f57..acc081fb1 100644 --- a/tests/test_additional_responses_default_validationerror.py +++ b/tests/test_additional_responses_default_validationerror.py @@ -66,6 +66,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 39f6f83b2..99bd03ae6 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -284,6 +284,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_application.py b/tests/test_application.py index 001586ff7..fe97e674c 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1260,6 +1260,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py index a8658e03b..3ca6a3e89 100644 --- a/tests/test_dependency_duplicates.py +++ b/tests/test_dependency_duplicates.py @@ -223,6 +223,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_enforce_once_required_parameter.py b/tests/test_enforce_once_required_parameter.py index 2e5ac6c06..c46a54357 100644 --- a/tests/test_enforce_once_required_parameter.py +++ b/tests/test_enforce_once_required_parameter.py @@ -42,6 +42,8 @@ expected_schema = { }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "title": "Location", diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index 45734ec28..251af4a59 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -347,6 +347,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py index fc5876410..1de2b50f7 100644 --- a/tests/test_filter_pydantic_sub_model_pv2.py +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -165,6 +165,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_forms_single_param.py b/tests/test_forms_single_param.py index 67f054b34..fc163cb1e 100644 --- a/tests/test_forms_single_param.py +++ b/tests/test_forms_single_param.py @@ -81,6 +81,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_generate_unique_id_function.py b/tests/test_generate_unique_id_function.py index 62ebfbc96..49510d08a 100644 --- a/tests/test_generate_unique_id_function.py +++ b/tests/test_generate_unique_id_function.py @@ -213,6 +213,8 @@ def test_top_level_generate_unique_id(): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", @@ -414,6 +416,8 @@ def test_router_overrides_generate_unique_id(): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", @@ -615,6 +619,8 @@ def test_router_include_overrides_generate_unique_id(): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", @@ -889,6 +895,8 @@ def test_subrouter_top_level_include_overrides_generate_unique_id(): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", @@ -1093,6 +1101,8 @@ def test_router_path_operation_overrides_generate_unique_id(): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", @@ -1301,6 +1311,8 @@ def test_app_path_operation_overrides_generate_unique_id(): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", @@ -1587,6 +1599,8 @@ def test_callback_override_generate_unique_id(): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_get_model_definitions_formfeed_escape.py b/tests/test_get_model_definitions_formfeed_escape.py index eb7939b69..46f8aa595 100644 --- a/tests/test_get_model_definitions_formfeed_escape.py +++ b/tests/test_get_model_definitions_formfeed_escape.py @@ -98,6 +98,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_get_request_body.py b/tests/test_get_request_body.py index cc567b88f..b21889e30 100644 --- a/tests/test_get_request_body.py +++ b/tests/test_get_request_body.py @@ -100,6 +100,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_include_router_defaults_overrides.py b/tests/test_include_router_defaults_overrides.py index 33baa25e6..6d2ffc44a 100644 --- a/tests/test_include_router_defaults_overrides.py +++ b/tests/test_include_router_defaults_overrides.py @@ -7290,6 +7290,8 @@ def test_openapi(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_infer_param_optionality.py b/tests/test_infer_param_optionality.py index 147018996..b11a1ca43 100644 --- a/tests/test_infer_param_optionality.py +++ b/tests/test_infer_param_optionality.py @@ -325,6 +325,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_modules_same_name_body/test_main.py b/tests/test_modules_same_name_body/test_main.py index 263d87df2..276de539d 100644 --- a/tests/test_modules_same_name_body/test_main.py +++ b/tests/test_modules_same_name_body/test_main.py @@ -131,6 +131,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 4418c77cb..fa3e0c635 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -167,6 +167,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index 5df51ba18..7540367a6 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -97,6 +97,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_no_schema_split.py b/tests/test_no_schema_split.py index 131a3755e..66bb89902 100644 --- a/tests/test_no_schema_split.py +++ b/tests/test_no_schema_split.py @@ -154,6 +154,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py index bd0d55452..93e5b366f 100644 --- a/tests/test_openapi_examples.py +++ b/tests/test_openapi_examples.py @@ -398,6 +398,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py index 084cb695d..b6c3c3d8d 100644 --- a/tests/test_openapi_query_parameter_extension.py +++ b/tests/test_openapi_query_parameter_extension.py @@ -119,6 +119,8 @@ def test_openapi(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py index 1891f0bde..f941e323b 100644 --- a/tests/test_openapi_separate_input_output_schemas.py +++ b/tests/test_openapi_separate_input_output_schemas.py @@ -418,6 +418,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] @@ -649,6 +651,8 @@ def test_openapi_schema_no_separate(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_param_in_path_and_dependency.py b/tests/test_param_in_path_and_dependency.py index 08eb0f40f..6b1f660cb 100644 --- a/tests/test_param_in_path_and_dependency.py +++ b/tests/test_param_in_path_and_dependency.py @@ -86,6 +86,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_param_include_in_schema.py b/tests/test_param_include_in_schema.py index f461947c9..5060920f1 100644 --- a/tests/test_param_include_in_schema.py +++ b/tests/test_param_include_in_schema.py @@ -151,6 +151,8 @@ openapi_schema = { }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_put_no_body.py b/tests/test_put_no_body.py index 8f4c82532..2b9299bc5 100644 --- a/tests/test_put_no_body.py +++ b/tests/test_put_no_body.py @@ -78,6 +78,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_regex_deprecated_body.py b/tests/test_regex_deprecated_body.py index 5b4daa450..6074206ff 100644 --- a/tests/test_regex_deprecated_body.py +++ b/tests/test_regex_deprecated_body.py @@ -132,6 +132,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_regex_deprecated_params.py b/tests/test_regex_deprecated_params.py index d6eaa45fb..6074b6282 100644 --- a/tests/test_regex_deprecated_params.py +++ b/tests/test_regex_deprecated_params.py @@ -121,6 +121,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_repeated_dependency_schema.py b/tests/test_repeated_dependency_schema.py index c21829bd9..0fc7e3d3e 100644 --- a/tests/test_repeated_dependency_schema.py +++ b/tests/test_repeated_dependency_schema.py @@ -35,6 +35,8 @@ schema = { }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "title": "Location", diff --git a/tests/test_repeated_parameter_alias.py b/tests/test_repeated_parameter_alias.py index fd72eaab2..49e4ad4a2 100644 --- a/tests/test_repeated_parameter_alias.py +++ b/tests/test_repeated_parameter_alias.py @@ -41,6 +41,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_reponse_set_reponse_code_empty.py b/tests/test_reponse_set_reponse_code_empty.py index bf3aa758c..b31aefa47 100644 --- a/tests/test_reponse_set_reponse_code_empty.py +++ b/tests/test_reponse_set_reponse_code_empty.py @@ -90,6 +90,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_request_body_parameters_media_type.py b/tests/test_request_body_parameters_media_type.py index d1bff9ddf..8731c3e5d 100644 --- a/tests/test_request_body_parameters_media_type.py +++ b/tests/test_request_body_parameters_media_type.py @@ -168,6 +168,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index ac8999c90..8caf6ce7a 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -845,6 +845,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 7ad936995..bff1226ad 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -237,6 +237,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index 57c16058a..5bcd5040f 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -240,6 +240,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index 60c6c242e..0353ba4c2 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -241,6 +241,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_starlette_exception.py b/tests/test_starlette_exception.py index 229fe8016..2be37b8bb 100644 --- a/tests/test_starlette_exception.py +++ b/tests/test_starlette_exception.py @@ -184,6 +184,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index cc7e5f5c6..442e709fb 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -277,6 +277,8 @@ def test_openapi_schema(): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tuples.py b/tests/test_tuples.py index d3c89045b..de9487df2 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -262,6 +262,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial001.py b/tests/test_tutorial/test_additional_responses/test_tutorial001.py index 1a18db75c..78ccb8442 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial001.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial001.py @@ -98,6 +98,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index 820860595..cdab56d7a 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -115,6 +115,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial003.py b/tests/test_tutorial/test_additional_responses/test_tutorial003.py index 90dc4e371..fda786b39 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial003.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial003.py @@ -102,6 +102,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index c6abf5e46..f36d3d79c 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -118,6 +118,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index f5e243b95..f80563d14 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -593,6 +593,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index 5a7cae160..9a837483f 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -328,6 +328,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body/test_tutorial002.py b/tests/test_tutorial/test_body/test_tutorial002.py index b6d51d523..e8b23e8f6 100644 --- a/tests/test_tutorial/test_body/test_tutorial002.py +++ b/tests/test_tutorial/test_body/test_tutorial002.py @@ -143,6 +143,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body/test_tutorial003.py b/tests/test_tutorial/test_body/test_tutorial003.py index 227a125e7..7b8b7ea89 100644 --- a/tests/test_tutorial/test_body/test_tutorial003.py +++ b/tests/test_tutorial/test_body/test_tutorial003.py @@ -153,6 +153,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body/test_tutorial004.py b/tests/test_tutorial/test_body/test_tutorial004.py index 10212843e..d78c760f5 100644 --- a/tests/test_tutorial/test_body/test_tutorial004.py +++ b/tests/test_tutorial/test_body/test_tutorial004.py @@ -164,6 +164,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001.py b/tests/test_tutorial/test_body_fields/test_tutorial001.py index 0ecadbb66..cb6da2908 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -166,6 +166,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index 63c9c16d6..a4f24627b 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -162,6 +162,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py index e98d5860f..155bda0c9 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py @@ -325,6 +325,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ 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 76b7ff709..2f403797f 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -202,6 +202,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py index 979c054cd..506e55eeb 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py @@ -272,6 +272,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py index d47aa1b4f..20859d12c 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py @@ -236,6 +236,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py b/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py index d452929c3..ae494350b 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py @@ -233,6 +233,8 @@ def test_openapi_schema(client: TestClient, mod_name: str): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial004.py b/tests/test_tutorial/test_body_nested_models/test_tutorial004.py index ff9596943..c1410330c 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial004.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial004.py @@ -257,6 +257,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial005.py b/tests/test_tutorial/test_body_nested_models/test_tutorial005.py index 9a07a904e..c09e0c1b1 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial005.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial005.py @@ -283,6 +283,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial006.py b/tests/test_tutorial/test_body_nested_models/test_tutorial006.py index 088177cb9..f26c50167 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial006.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial006.py @@ -251,6 +251,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial007.py b/tests/test_tutorial/test_body_nested_models/test_tutorial007.py index a30281950..dac168e24 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial007.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial007.py @@ -326,6 +326,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial008.py b/tests/test_tutorial/test_body_nested_models/test_tutorial008.py index 32eb8ee75..2101b7bbe 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial008.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial008.py @@ -139,6 +139,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py index f2e56d40f..f7481a5f7 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -98,6 +98,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index 0401eb7d0..9c6a90576 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -168,6 +168,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_body_updates/test_tutorial002.py b/tests/test_tutorial/test_body_updates/test_tutorial002.py index 466e6af8f..7d79cf5e6 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial002.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial002.py @@ -189,6 +189,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py index ac8e7bdae..f391c569a 100644 --- a/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py @@ -151,6 +151,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py index d7c3d15f1..6583045dc 100644 --- a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py @@ -161,6 +161,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index 9b47cbc67..ab7114876 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -101,6 +101,8 @@ def test_openapi_schema(mod: ModuleType): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index 4683062f5..756eacf23 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -129,6 +129,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index a6a9fc1c7..de63a9476 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -195,6 +195,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py b/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py index 50d7c4108..15919c63f 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py @@ -170,6 +170,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py index f09d6f268..96300a259 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py @@ -161,6 +161,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial005.py b/tests/test_tutorial/test_dependencies/test_tutorial005.py index a914936ba..e595859cb 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial005.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial005.py @@ -121,6 +121,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py index 59202df3b..cdea27b7c 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -125,6 +125,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial011.py b/tests/test_tutorial/test_dependencies/test_tutorial011.py index 4868254c0..3374c54b5 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial011.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial011.py @@ -102,6 +102,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012.py b/tests/test_tutorial/test_dependencies/test_tutorial012.py index d5599ac73..f342ff842 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012.py @@ -219,6 +219,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_encoder/test_tutorial001.py b/tests/test_tutorial/test_encoder/test_tutorial001.py index 5c8ee054d..5a4edbc66 100644 --- a/tests/test_tutorial/test_encoder/test_tutorial001.py +++ b/tests/test_tutorial/test_encoder/test_tutorial001.py @@ -172,6 +172,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_events/test_tutorial001.py b/tests/test_tutorial/test_events/test_tutorial001.py index 5fe99d50d..48b838d5a 100644 --- a/tests/test_tutorial/test_events/test_tutorial001.py +++ b/tests/test_tutorial/test_events/test_tutorial001.py @@ -63,6 +63,8 @@ def test_openapi_schema(app: FastAPI): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_events/test_tutorial003.py b/tests/test_tutorial/test_events/test_tutorial003.py index 38710edfe..aed9def7a 100644 --- a/tests/test_tutorial/test_events/test_tutorial003.py +++ b/tests/test_tutorial/test_events/test_tutorial003.py @@ -76,6 +76,8 @@ def test_openapi_schema(): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py index 5479e2925..28fe68f28 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -133,6 +133,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py index 3f2f508a1..998169986 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py @@ -138,6 +138,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py index 872af5383..38e874158 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -127,6 +127,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial001.py b/tests/test_tutorial/test_generate_clients/test_tutorial001.py index bbb66b451..83ae38c5b 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial001.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial001.py @@ -135,6 +135,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial002.py b/tests/test_tutorial/test_generate_clients/test_tutorial002.py index ab8bc4c11..b9255325a 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial002.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial002.py @@ -180,6 +180,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial003.py b/tests/test_tutorial/test_generate_clients/test_tutorial003.py index bac52e4fd..d05484587 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial003.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial003.py @@ -180,6 +180,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial004.py b/tests/test_tutorial/test_generate_clients/test_tutorial004.py index e66f6d2a1..eea60e342 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial004.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial004.py @@ -82,6 +82,8 @@ def test_remove_tags(tmp_path: pathlib.Path): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial001.py b/tests/test_tutorial/test_handling_errors/test_tutorial001.py index c01850fae..e22f1dafd 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial001.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial001.py @@ -72,6 +72,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial002.py b/tests/test_tutorial/test_handling_errors/test_tutorial002.py index 09366a86f..991478a0f 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial002.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial002.py @@ -72,6 +72,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial003.py b/tests/test_tutorial/test_handling_errors/test_tutorial003.py index 51ac3e7b2..c303960bd 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial003.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial003.py @@ -73,6 +73,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index 376bc8266..f6ec59b4d 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -78,6 +78,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py index 7bd947f19..a7fa4f0b6 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py @@ -102,6 +102,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py index e95e53d5e..9cb57d857 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial006.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial006.py @@ -86,6 +86,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial001.py b/tests/test_tutorial/test_header_param_models/test_tutorial001.py index 1fa8aee46..2d14c698e 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial001.py @@ -187,6 +187,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial002.py b/tests/test_tutorial/test_header_param_models/test_tutorial002.py index 079a8f540..478ac8408 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial002.py @@ -184,6 +184,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial003.py b/tests/test_tutorial/test_header_param_models/test_tutorial003.py index 4c89d80ee..00636c2b5 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial003.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial003.py @@ -224,6 +224,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_header_params/test_tutorial001.py b/tests/test_tutorial/test_header_params/test_tutorial001.py index 88591b822..60342f70a 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001.py @@ -93,6 +93,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py index 229f96c1f..f1ced99b1 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -104,6 +104,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py index cf067ccf9..382c3ae19 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -112,6 +112,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index 6fde96cb5..e8c98e806 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -195,6 +195,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py index 27619489f..c58e0fd02 100644 --- a/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py @@ -98,6 +98,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index a95540731..75b08a4e7 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -118,6 +118,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py index 085d1f5e1..de81251d0 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py @@ -150,6 +150,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py index c7414d756..28e5e7d8d 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py @@ -187,6 +187,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py index 791db2462..e42c3e2b7 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py @@ -172,6 +172,8 @@ def test_openapi_schema(client: TestClient, mod_name: str): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index c5a3aec1d..b684c9f5c 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -117,6 +117,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_path_params/test_tutorial001.py b/tests/test_tutorial/test_path_params/test_tutorial001.py index a898e386f..f54626f33 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial001.py +++ b/tests/test_tutorial/test_path_params/test_tutorial001.py @@ -80,6 +80,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_path_params/test_tutorial002.py b/tests/test_tutorial/test_path_params/test_tutorial002.py index 0bfc9f807..46da41b48 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial002.py +++ b/tests/test_tutorial/test_path_params/test_tutorial002.py @@ -88,6 +88,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_path_params/test_tutorial003.py b/tests/test_tutorial/test_path_params/test_tutorial003.py index cd2c39ab0..6ac92c87e 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial003.py +++ b/tests/test_tutorial/test_path_params/test_tutorial003.py @@ -97,6 +97,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_path_params/test_tutorial004.py b/tests/test_tutorial/test_path_params/test_tutorial004.py index f7f233ccf..8f460fb69 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial004.py +++ b/tests/test_tutorial/test_path_params/test_tutorial004.py @@ -73,6 +73,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index 86ccce7b6..3e3766e84 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -110,6 +110,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py index f1e304103..a4d68d01b 100644 --- a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py @@ -128,6 +128,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py index 467c915dc..37533bd22 100644 --- a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py @@ -134,6 +134,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py index d3593c984..a9c111a59 100644 --- a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py @@ -149,6 +149,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py index 296192593..e0e976d6f 100644 --- a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py @@ -166,6 +166,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py index 9dc7d7aac..2004ad1d2 100644 --- a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py @@ -185,6 +185,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial001.py b/tests/test_tutorial/test_query_param_models/test_tutorial001.py index d3ce57121..38b767154 100644 --- a/tests/test_tutorial/test_query_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_query_param_models/test_tutorial001.py @@ -207,6 +207,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial002.py b/tests/test_tutorial/test_query_param_models/test_tutorial002.py index 96abce6ab..b173a2df4 100644 --- a/tests/test_tutorial/test_query_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_query_param_models/test_tutorial002.py @@ -213,6 +213,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_query_params/test_tutorial001.py b/tests/test_tutorial/test_query_params/test_tutorial001.py index 4c92b57b8..84e455727 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial001.py +++ b/tests/test_tutorial/test_query_params/test_tutorial001.py @@ -108,6 +108,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params/test_tutorial002.py b/tests/test_tutorial/test_query_params/test_tutorial002.py index ae3ee7613..f725c80b3 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial002.py +++ b/tests/test_tutorial/test_query_params/test_tutorial002.py @@ -109,6 +109,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params/test_tutorial003.py b/tests/test_tutorial/test_query_params/test_tutorial003.py index c0b7e3b13..9f1f2e6e4 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial003.py +++ b/tests/test_tutorial/test_query_params/test_tutorial003.py @@ -130,6 +130,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params/test_tutorial004.py b/tests/test_tutorial/test_query_params/test_tutorial004.py index 9be18b74d..e834f973a 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial004.py +++ b/tests/test_tutorial/test_query_params/test_tutorial004.py @@ -138,6 +138,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params/test_tutorial005.py b/tests/test_tutorial/test_query_params/test_tutorial005.py index 103078147..36129dbc9 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params/test_tutorial005.py @@ -86,6 +86,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params/test_tutorial006.py b/tests/test_tutorial/test_query_params/test_tutorial006.py index 157322c7e..473dc3366 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006.py @@ -137,6 +137,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py index f1af7e08c..069921629 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py @@ -103,6 +103,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py index 62018b80b..a043b5b2e 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py @@ -124,6 +124,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py index a4ad7a63b..68c6e6174 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py @@ -135,6 +135,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py index 585989a82..79538f952 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py @@ -129,6 +129,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py index 52462fe33..fafbd0a7d 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py @@ -113,6 +113,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py index 640cedce1..1d01492c6 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py @@ -118,6 +118,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py index f287b5dcd..d31cb5036 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py @@ -130,6 +130,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py index b17bc2771..e03090245 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py @@ -118,6 +118,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py index c63111574..186de5e06 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py @@ -120,6 +120,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py index 7e9d69d41..b242a75c9 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py @@ -105,6 +105,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { 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 00889c5bf..6a39130af 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 @@ -136,6 +136,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { 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 11de33ae1..6ab279bf3 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 @@ -98,6 +98,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { 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 182692861..41bfeb3a7 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 @@ -93,6 +93,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { 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 46c367c86..52c8147ff 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 @@ -93,6 +93,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { 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 0feaccfa4..bb168f0fc 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 @@ -93,6 +93,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py index 82bb606a9..32a990e74 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py @@ -122,6 +122,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index e0e1bbe63..db9b83b31 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -183,6 +183,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { 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 18948c544..feeb5363e 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -173,6 +173,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", 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 53a7a0cf8..903452ac7 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -163,6 +163,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index 03772419a..4d9ff0e93 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -223,6 +223,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial003.py b/tests/test_tutorial/test_request_files/test_tutorial003.py index fa4bfd569..c9f7f0994 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003.py @@ -206,6 +206,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001.py b/tests/test_tutorial/test_request_form_models/test_tutorial001.py index 0c43dd7b2..c4740ee72 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001.py @@ -159,6 +159,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002.py b/tests/test_tutorial/test_request_form_models/test_tutorial002.py index 238f8fa2e..b07fce432 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002.py @@ -177,6 +177,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001.py b/tests/test_tutorial/test_request_forms/test_tutorial001.py index 4276414fc..f5f76306e 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001.py @@ -161,6 +161,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index 7fa4c3de5..cd05a1ccf 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py @@ -216,6 +216,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_response_directly/test_tutorial001.py b/tests/test_tutorial/test_response_directly/test_tutorial001.py index 2d0c38719..76e7143bd 100644 --- a/tests/test_tutorial/test_response_directly/test_tutorial001.py +++ b/tests/test_tutorial/test_response_directly/test_tutorial001.py @@ -130,6 +130,8 @@ def test_openapi_schema_pv2(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py b/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py index 10692f990..265162f15 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py @@ -175,6 +175,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial002.py b/tests/test_tutorial/test_response_model/test_tutorial002.py index 216d4c420..17027d3c1 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial002.py +++ b/tests/test_tutorial/test_response_model/test_tutorial002.py @@ -111,6 +111,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003.py b/tests/test_tutorial/test_response_model/test_tutorial003.py index 35ed5572d..a1477b7df 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003.py @@ -126,6 +126,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py index fa1eb6277..a60a14ae8 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -139,6 +139,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_02.py b/tests/test_tutorial/test_response_model/test_tutorial003_02.py index b7507b711..fcd5f9a1d 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_02.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_02.py @@ -86,6 +86,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05.py b/tests/test_tutorial/test_response_model/test_tutorial003_05.py index 19a7c601b..e64ed1a80 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_05.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_05.py @@ -101,6 +101,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py index 9c0d95ebd..d40bce261 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -117,6 +117,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index 63e8535db..55b2334d4 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -135,6 +135,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index 08ab65952..5d6f542b5 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -135,6 +135,8 @@ def test_openapi_schema(client: TestClient): "required": ["loc", "msg", "type"], "type": "object", "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "title": "Location", "type": "array", diff --git a/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py index ddf55a045..8b6213e33 100644 --- a/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py @@ -78,6 +78,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py index 82f69fd46..7f0105a26 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py @@ -120,6 +120,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py index 4f5240860..32707c299 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py @@ -122,6 +122,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py index 3529a9bf0..4f8f1394c 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py @@ -124,6 +124,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index 9326e0629..3a0a7704b 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -140,6 +140,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py index 2d0dee48c..b10f25e26 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py @@ -149,6 +149,8 @@ def test_openapi_schema(client: TestClient) -> None: }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, } diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index 6a786348c..924b36b3a 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -182,6 +182,8 @@ def test_openapi_schema(client: TestClient): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial004.py b/tests/test_tutorial/test_security/test_tutorial004.py index b5e3d39ef..2b0df66a2 100644 --- a/tests/test_tutorial/test_security/test_tutorial004.py +++ b/tests/test_tutorial/test_security/test_tutorial004.py @@ -334,6 +334,8 @@ def test_openapi_schema(mod: ModuleType): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index 25b47f0ad..76b08860f 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -379,6 +379,8 @@ def test_openapi_schema(mod: ModuleType): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py index 275b23487..d0a0d5d38 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py @@ -121,6 +121,8 @@ def test_openapi_schema(client: TestClient) -> None: }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py index 8230e3922..a2fa56f93 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py @@ -121,6 +121,8 @@ def test_openapi_schema(client: TestClient) -> None: }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_sql_databases/test_tutorial001.py index 2c628f525..aec20e42e 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial001.py @@ -335,6 +335,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial002.py b/tests/test_tutorial/test_sql_databases/test_tutorial002.py index c72c16e9a..4ea7d5f64 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial002.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial002.py @@ -416,6 +416,8 @@ def test_openapi_schema(client: TestClient): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_tutorial/test_using_request_directly/test_tutorial001.py b/tests/test_tutorial/test_using_request_directly/test_tutorial001.py index 33e661b16..b55bfb456 100644 --- a/tests/test_tutorial/test_using_request_directly/test_tutorial001.py +++ b/tests/test_tutorial/test_using_request_directly/test_tutorial001.py @@ -76,6 +76,8 @@ def test_openapi(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [ diff --git a/tests/test_union_body.py b/tests/test_union_body.py index ee7fcc423..ee56bb6eb 100644 --- a/tests/test_union_body.py +++ b/tests/test_union_body.py @@ -111,6 +111,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_union_body_discriminator.py b/tests/test_union_body_discriminator.py index 6c31649bc..4afe7be4b 100644 --- a/tests/test_union_body_discriminator.py +++ b/tests/test_union_body_discriminator.py @@ -154,6 +154,8 @@ def test_discriminator_pydantic_v2() -> None: }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_union_body_discriminator_annotated.py b/tests/test_union_body_discriminator_annotated.py index 42a6aed24..6644d106c 100644 --- a/tests/test_union_body_discriminator_annotated.py +++ b/tests/test_union_body_discriminator_annotated.py @@ -181,6 +181,8 @@ def test_openapi_schema(client: TestClient) -> None: }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_union_forms.py b/tests/test_union_forms.py index 018949f0c..d90d0753a 100644 --- a/tests/test_union_forms.py +++ b/tests/test_union_forms.py @@ -136,6 +136,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] diff --git a/tests/test_union_inherited_body.py b/tests/test_union_inherited_body.py index 3c062e7f5..6b284c68c 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -117,6 +117,8 @@ def test_openapi_schema(): }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, + "input": {"title": "Input"}, + "ctx": {"title": "Context", "type": "object"}, }, }, "HTTPValidationError": { diff --git a/tests/test_webhooks_security.py b/tests/test_webhooks_security.py index 982ae1e21..c2c2809b2 100644 --- a/tests/test_webhooks_security.py +++ b/tests/test_webhooks_security.py @@ -106,6 +106,8 @@ def test_openapi_schema(): }, "ValidationError": { "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, "loc": { "items": { "anyOf": [{"type": "string"}, {"type": "integer"}] From 4a3a71f1c15c415c60faab3de59e35b97dd632e3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 14:34:39 +0000 Subject: [PATCH 172/243] =?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 8d90983d4..b4fd20de2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Fixes +* 🐛 Update `ValidationError` schema to include `input` and `ctx`. PR [#14791](https://github.com/fastapi/fastapi/pull/14791) by [@jonathan-fulton](https://github.com/jonathan-fulton). * 🐛 Fix TYPE_CHECKING annotations for Python 3.14 (PEP 649). PR [#14789](https://github.com/fastapi/fastapi/pull/14789) by [@mgu](https://github.com/mgu). * 🐛 Strip whitespaces from `Authorization` header credentials. PR [#14786](https://github.com/fastapi/fastapi/pull/14786) by [@WaveTheory1](https://github.com/WaveTheory1). * 🐛 Fix OpenAPI duplication of `anyOf` refs for app-level responses with specified `content` and `model` as `Union`. PR [#14463](https://github.com/fastapi/fastapi/pull/14463) by [@DJMcoder](https://github.com/DJMcoder). From ee0c12521fa0817cc19a4cbbd949c1e9d5fec1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 4 Feb 2026 06:35:17 -0800 Subject: [PATCH 173/243] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20co?= =?UTF-8?q?ntributing=20translations,=20simplify=20title=20(#14817)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 1505dfd1e..5e6d821b3 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -181,7 +181,7 @@ Help with translations is VERY MUCH appreciated! And it can't be done without th Here are the steps to help with translations. -#### Review Translation PRs +#### Translation PRs Translation pull requests are made by LLMs guided with prompts designed by the FastAPI team together with the community of native speakers for each supported language. From fe5b617aecaeba0261dc8c7a1b82afe483814cd2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 14:36:09 +0000 Subject: [PATCH 174/243] =?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 b4fd20de2..937e2930d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -27,6 +27,7 @@ hide: ### Docs +* 📝 Update docs for contributing translations, simplify title. PR [#14817](https://github.com/fastapi/fastapi/pull/14817) by [@tiangolo](https://github.com/tiangolo). * 📝 Fix typing issue in `docs_src/app_testing/app_b` code example. PR [#14573](https://github.com/fastapi/fastapi/pull/14573) by [@timakaa](https://github.com/timakaa). * 📝 Fix example of license identifier in documentation. PR [#14492](https://github.com/fastapi/fastapi/pull/14492) by [@johnson-earls](https://github.com/johnson-earls). * 📝 Add banner to translated pages. PR [#14809](https://github.com/fastapi/fastapi/pull/14809) by [@YuriiMotov](https://github.com/YuriiMotov). From aea61373ae0c97b32611fc2dc993e84a9729edf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 4 Feb 2026 06:49:18 -0800 Subject: [PATCH 175/243] =?UTF-8?q?=F0=9F=90=9B=20Fix=20translation=20scri?= =?UTF-8?q?pt=20commit=20in=20place=20(#14818)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/translate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/translate.py b/scripts/translate.py index 9eda7b390..072383514 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -411,7 +411,8 @@ def make_pr( print(f"Creating a new branch {branch_name}") subprocess.run(["git", "checkout", "-b", branch_name], check=True) else: - print(f"Committing in place on branch {current_branch}") + branch_name = current_branch + print(f"Committing in place on branch {branch_name}") print("Adding updated files") git_path = Path("docs") subprocess.run(["git", "add", str(git_path)], check=True) From cec4be00ba524427cc52e9894112199c27fce9f1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 14:49:41 +0000 Subject: [PATCH 176/243] =?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 937e2930d..1a56e5eca 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -68,6 +68,7 @@ hide: ### Internal +* 🐛 Fix translation script commit in place. PR [#14818](https://github.com/fastapi/fastapi/pull/14818) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update translation script to retry if LLM-response doesn't pass validation with Translation Fixer tool. PR [#14749](https://github.com/fastapi/fastapi/pull/14749) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Run tests only on relevant code changes (not on docs). PR [#14813](https://github.com/fastapi/fastapi/pull/14813) by [@tiangolo](https://github.com/tiangolo). * 👷 Run mypy by pre-commit. PR [#14806](https://github.com/fastapi/fastapi/pull/14806) by [@YuriiMotov](https://github.com/YuriiMotov). From b0e99d66e80426f3f13dd4bcbcb78d2a39ec10b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 4 Feb 2026 08:44:21 -0800 Subject: [PATCH 177/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20ja=20(update-outdated)=20(#14588)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] Co-authored-by: Yurii Motov Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: Maruo.S --- docs/en/mkdocs.yml | 2 + .../docs/advanced/additional-status-codes.md | 34 +- docs/ja/docs/advanced/custom-response.md | 224 ++++++--- docs/ja/docs/advanced/index.md | 24 +- .../path-operation-advanced-configuration.md | 157 +++++- docs/ja/docs/advanced/response-directly.md | 14 +- docs/ja/docs/advanced/websockets.md | 72 ++- docs/ja/docs/benchmarks.md | 36 +- docs/ja/docs/deployment/concepts.md | 92 ++-- docs/ja/docs/deployment/docker.md | 440 ++++++----------- docs/ja/docs/deployment/https.md | 86 ++-- docs/ja/docs/deployment/index.md | 24 +- docs/ja/docs/deployment/server-workers.md | 182 +++---- docs/ja/docs/deployment/versions.md | 104 ++-- docs/ja/docs/environment-variables.md | 115 +++-- docs/ja/docs/how-to/conditional-openapi.md | 12 +- docs/ja/docs/index.md | 453 +++++++++++------- docs/ja/docs/learn/index.md | 4 +- docs/ja/docs/project-generation.md | 102 +--- docs/ja/docs/python-types.md | 366 +++++++++----- docs/ja/docs/tutorial/background-tasks.md | 46 +- docs/ja/docs/tutorial/body-fields.md | 22 +- docs/ja/docs/tutorial/body-multiple-params.md | 63 +-- docs/ja/docs/tutorial/body-nested-models.md | 108 ++--- docs/ja/docs/tutorial/body-updates.md | 38 +- docs/ja/docs/tutorial/body.md | 138 +++--- docs/ja/docs/tutorial/cookie-param-models.md | 25 +- docs/ja/docs/tutorial/cookie-params.md | 24 +- docs/ja/docs/tutorial/cors.md | 42 +- docs/ja/docs/tutorial/debugging.md | 14 +- .../dependencies/classes-as-dependencies.md | 150 +++++- ...pendencies-in-path-operation-decorators.md | 62 ++- .../dependencies/dependencies-with-yield.md | 185 ++++--- docs/ja/docs/tutorial/dependencies/index.md | 128 +++-- .../tutorial/dependencies/sub-dependencies.md | 49 +- docs/ja/docs/tutorial/encoder.md | 10 +- docs/ja/docs/tutorial/extra-data-types.md | 42 +- docs/ja/docs/tutorial/extra-models.md | 90 ++-- docs/ja/docs/tutorial/first-steps.md | 214 +++++---- docs/ja/docs/tutorial/handling-errors.md | 103 ++-- docs/ja/docs/tutorial/header-params.md | 36 +- docs/ja/docs/tutorial/index.md | 90 ++-- docs/ja/docs/tutorial/metadata.md | 105 ++-- docs/ja/docs/tutorial/middleware.md | 47 +- .../tutorial/path-operation-configuration.md | 58 ++- .../path-params-numeric-validations.md | 111 +++-- docs/ja/docs/tutorial/path-params.md | 125 ++--- docs/ja/docs/tutorial/query-param-models.md | 22 +- .../tutorial/query-params-str-validations.md | 360 ++++++++++---- docs/ja/docs/tutorial/query-params.md | 64 +-- .../docs/tutorial/request-forms-and-files.md | 20 +- docs/ja/docs/tutorial/request-forms.md | 24 +- docs/ja/docs/tutorial/response-model.md | 297 ++++++++---- docs/ja/docs/tutorial/response-status-code.md | 40 +- docs/ja/docs/tutorial/schema-extra-example.md | 201 ++++++-- docs/ja/docs/tutorial/security/first-steps.md | 66 +-- .../tutorial/security/get-current-user.md | 47 +- docs/ja/docs/tutorial/security/oauth2-jwt.md | 88 ++-- docs/ja/docs/tutorial/static-files.md | 18 +- docs/ja/docs/tutorial/testing.md | 99 +++- docs/ja/docs/virtual-environments.md | 102 ++-- docs/ja/llm-prompt.md | 3 +- scripts/docs.py | 1 + 63 files changed, 3658 insertions(+), 2462 deletions(-) diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 1c2954619..34d489d92 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -317,6 +317,8 @@ extra: name: de - Deutsch - link: /es/ name: es - español + - link: /ja/ + name: ja - 日本語 - link: /ko/ name: ko - 한국어 - link: /pt/ diff --git a/docs/ja/docs/advanced/additional-status-codes.md b/docs/ja/docs/advanced/additional-status-codes.md index 33457f591..14b7e8ba8 100644 --- a/docs/ja/docs/advanced/additional-status-codes.md +++ b/docs/ja/docs/advanced/additional-status-codes.md @@ -1,41 +1,41 @@ -# 追加のステータスコード +# 追加のステータスコード { #additional-status-codes } -デフォルトでは、 **FastAPI** は `JSONResponse` を使ってレスポンスを返します。その `JSONResponse` の中には、 *path operation* が返した内容が入ります。 +デフォルトでは、 **FastAPI** は `JSONResponse` を使ってレスポンスを返し、*path operation* から返した内容をその `JSONResponse` の中に入れます。 -それは、デフォルトのステータスコードか、 *path operation* でセットしたものを利用します。 +デフォルトのステータスコード、または *path operation* で設定したステータスコードが使用されます。 -## 追加のステータスコード +## 追加のステータスコード { #additional-status-codes_1 } -メインのステータスコードとは別に、他のステータスコードを返したい場合は、`Response` (`JSONResponse` など) に追加のステータスコードを設定して直接返します。 +メインのステータスコードとは別に追加のステータスコードを返したい場合は、`JSONResponse` のような `Response` を直接返し、追加のステータスコードを直接設定できます。 -例えば、itemを更新し、成功した場合は200 "OK"のHTTPステータスコードを返す *path operation* を作りたいとします。 +たとえば、item を更新でき、成功時に HTTP ステータスコード 200 "OK" を返す *path operation* を作りたいとします。 -しかし、新しいitemも許可したいです。itemが存在しない場合は、それらを作成して201 "Created"を返します。 +しかし、新しい item も受け付けたいとします。そして、item が以前存在しなかった場合には作成し、HTTP ステータスコード 201「Created」を返します。 -これを達成するには、 `JSONResponse` をインポートし、 `status_code` を設定して直接内容を返します。 +これを実現するには、`JSONResponse` をインポートし、望む `status_code` を設定して、そこで内容を直接返します。 -{* ../../docs_src/additional_status_codes/tutorial001.py hl[4,25] *} +{* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *} /// warning | 注意 -上記の例のように `Response` を明示的に返す場合、それは直接返されます。 +上の例のように `Response` を直接返すと、それはそのまま返されます。 -モデルなどはシリアライズされません。 +モデルなどによってシリアライズされません。 -必要なデータが含まれていることや、値が有効なJSONであること (`JSONResponse` を使う場合) を確認してください。 +必要なデータが含まれていること、そして(`JSONResponse` を使用している場合)値が有効な JSON であることを確認してください。 /// /// note | 技術詳細 -`from starlette.responses import JSONResponse` を利用することもできます。 +`from starlette.responses import JSONResponse` を使うこともできます。 -**FastAPI** は `fastapi.responses` と同じ `starlette.responses` を、開発者の利便性のために提供しています。しかし有効なレスポンスはほとんどStarletteから来ています。 `status` についても同じです。 +**FastAPI** は開発者の利便性のために、`fastapi.responses` と同じ `starlette.responses` を提供しています。しかし、利用可能なレスポンスのほとんどは Starlette から直接提供されています。`status` も同様です。 /// -## OpenAPIとAPIドキュメント +## OpenAPI と API ドキュメント { #openapi-and-api-docs } -ステータスコードとレスポンスを直接返す場合、それらはOpenAPIスキーマ (APIドキュメント) には含まれません。なぜなら、FastAPIは何が返されるのか事前に知ることができないからです。 +追加のステータスコードとレスポンスを直接返す場合、それらは OpenAPI スキーマ(API ドキュメント)には含まれません。FastAPI には、事前に何が返されるかを知る方法がないからです。 -しかし、 [Additional Responses](additional-responses.md){.internal-link target=_blank} を使ってコードの中にドキュメントを書くことができます。 +しかし、[Additional Responses](additional-responses.md){.internal-link target=_blank} を使ってコード内にドキュメント化できます。 diff --git a/docs/ja/docs/advanced/custom-response.md b/docs/ja/docs/advanced/custom-response.md index 1b2cd914d..9d881c013 100644 --- a/docs/ja/docs/advanced/custom-response.md +++ b/docs/ja/docs/advanced/custom-response.md @@ -1,34 +1,40 @@ -# カスタムレスポンス - HTML、ストリーム、ファイル、その他のレスポンス +# カスタムレスポンス - HTML、ストリーム、ファイル、その他のレスポンス { #custom-response-html-stream-file-others } デフォルトでは、**FastAPI** は `JSONResponse` を使ってレスポンスを返します。 [レスポンスを直接返す](response-directly.md){.internal-link target=_blank}で見たように、 `Response` を直接返すことでこの挙動をオーバーライドできます。 -しかし、`Response` を直接返すと、データは自動的に変換されず、ドキュメントも自動生成されません (例えば、生成されるOpenAPIの一部としてHTTPヘッダー `Content-Type` に特定の「メディアタイプ」を含めるなど) 。 +しかし、`Response` を直接返すと(または `JSONResponse` のような任意のサブクラスを返すと)、データは自動的に変換されず(`response_model` を宣言していても)、ドキュメントも自動生成されません(例えば、生成されるOpenAPIの一部としてHTTPヘッダー `Content-Type` に特定の「メディアタイプ」を含めるなど)。 -しかし、*path operationデコレータ* に、使いたい `Response` を宣言することもできます。 +`response_class` パラメータを使用して、*path operation デコレータ* で使用したい `Response`(任意の `Response` サブクラス)を宣言することもできます。 -*path operation関数* から返されるコンテンツは、その `Response` に含まれます。 +*path operation 関数* から返されるコンテンツは、その `Response` に含まれます。 -そしてもし、`Response` が、`JSONResponse` や `UJSONResponse` の場合のようにJSONメディアタイプ (`application/json`) ならば、データは *path operationデコレータ* に宣言したPydantic `response_model` により自動的に変換 (もしくはフィルタ) されます。 +そしてその `Response` が、`JSONResponse` や `UJSONResponse` の場合のようにJSONメディアタイプ(`application/json`)なら、関数の返り値は *path operationデコレータ* に宣言した任意のPydantic `response_model` により自動的に変換(およびフィルタ)されます。 /// note | 備考 -メディアタイプを指定せずにレスポンスクラスを利用すると、FastAPIは何もコンテンツがないことを期待します。そのため、生成されるOpenAPIドキュメントにレスポンスフォーマットが記載されません。 +メディアタイプを指定せずにレスポンスクラスを利用すると、FastAPIはレスポンスにコンテンツがないことを期待します。そのため、生成されるOpenAPIドキュメントにレスポンスフォーマットが記載されません。 /// -## `ORJSONResponse` を使う +## `ORJSONResponse` を使う { #use-orjsonresponse } -例えば、パフォーマンスを出したい場合は、`orjson`をインストールし、`ORJSONResponse`をレスポンスとしてセットすることができます。 +例えば、パフォーマンスを絞り出したい場合は、`orjson`をインストールし、レスポンスとして `ORJSONResponse` をセットできます。 -使いたい `Response` クラス (サブクラス) をインポートし、 *path operationデコレータ* に宣言します。 +使いたい `Response` クラス(サブクラス)をインポートし、*path operationデコレータ* に宣言します。 -{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} +大きなレスポンスの場合、`Response` を直接返すほうが、辞書を返すよりもはるかに高速です。 + +これは、デフォルトではFastAPIがチュートリアルで説明した同じ[JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}を使って、内部の各アイテムを検査し、JSONとしてシリアライズ可能であることを確認するためです。これにより、例えばデータベースモデルのような**任意のオブジェクト**を返せます。 + +しかし、返そうとしているコンテンツが **JSONでシリアライズ可能**であることが確実なら、それを直接レスポンスクラスに渡して、FastAPIがレスポンスクラスへ渡す前に返却コンテンツを `jsonable_encoder` に通すことで発生する追加のオーバーヘッドを回避できます。 + +{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *} /// info | 情報 -パラメータ `response_class` は、レスポンスの「メディアタイプ」を定義するために利用することもできます。 +パラメータ `response_class` は、レスポンスの「メディアタイプ」を定義するためにも利用されます。 この場合、HTTPヘッダー `Content-Type` には `application/json` がセットされます。 @@ -38,70 +44,70 @@ /// tip | 豆知識 -`ORJSONResponse` は、現在はFastAPIのみで利用可能で、Starletteでは利用できません。 +`ORJSONResponse` はFastAPIでのみ利用可能で、Starletteでは利用できません。 /// -## HTMLレスポンス +## HTMLレスポンス { #html-response } **FastAPI** からHTMLを直接返す場合は、`HTMLResponse` を使います。 * `HTMLResponse` をインポートする。 -* *path operation* のパラメータ `content_type` に `HTMLResponse` を渡す。 +* *path operation デコレータ* のパラメータ `response_class` に `HTMLResponse` を渡す。 -{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} +{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *} /// info | 情報 -パラメータ `response_class` は、レスポンスの「メディアタイプ」を定義するために利用されます。 +パラメータ `response_class` は、レスポンスの「メディアタイプ」を定義するためにも利用されます。 この場合、HTTPヘッダー `Content-Type` には `text/html` がセットされます。 -そして、OpenAPIにはそのようにドキュメント化されます。 +そして、OpenAPIにはそのようにドキュメントされます。 /// -### `Response` を返す +### `Response` を返す { #return-a-response } -[レスポンスを直接返す](response-directly.md){.internal-link target=_blank}で見たように、レスポンスを直接返すことで、*path operation* の中でレスポンスをオーバーライドできます。 +[レスポンスを直接返す](response-directly.md){.internal-link target=_blank}で見たように、レスポンスを返すことで、*path operation* の中でレスポンスを直接オーバーライドすることもできます。 上記と同じ例において、 `HTMLResponse` を返すと、このようになります: -{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} +{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *} /// warning | 注意 -*path operation関数* から直接返される `Response` は、OpenAPIにドキュメントされず (例えば、 `Content-Type` がドキュメントされない) 、自動的な対話的ドキュメントからも閲覧できません。 +*path operation関数* から直接返される `Response` は、OpenAPIにドキュメントされず(例えば、`Content-Type` がドキュメントされない)、自動的な対話的ドキュメントでも表示されません。 /// /// info | 情報 -もちろん、実際の `Content-Type` ヘッダーやステータスコードなどは、返された `Response` オブジェクトに由来しています。 +もちろん、実際の `Content-Type` ヘッダーやステータスコードなどは、返された `Response` オブジェクトに由来します。 /// -### OpenAPIドキュメントと `Response` のオーバーライド +### OpenAPIドキュメントと `Response` のオーバーライド { #document-in-openapi-and-override-response } -関数の中でレスポンスをオーバーライドしつつも、OpenAPI に「メディアタイプ」をドキュメント化したいなら、 `response_class` パラメータを使い、 `Response` オブジェクトを返します。 +関数の中でレスポンスをオーバーライドしつつも、OpenAPI に「メディアタイプ」をドキュメント化したいなら、`response_class` パラメータを使用し、かつ `Response` オブジェクトを返します。 -`response_class` はOpenAPIの *path operation* ドキュメントにのみ使用されますが、 `Response` はそのまま使用されます。 +`response_class` はOpenAPIの*path operation*のドキュメント化のためにのみ使用され、`Response` はそのまま使用されます。 -#### `HTMLResponse` を直接返す +#### `HTMLResponse` を直接返す { #return-an-htmlresponse-directly } 例えば、このようになります: -{* ../../docs_src/custom_response/tutorial004.py hl[7,21,23] *} +{* ../../docs_src/custom_response/tutorial004_py39.py hl[7,21,23] *} -この例では、関数 `generate_html_response()` は、`str` のHTMLを返すのではなく `Response` を生成して返しています。 +この例では、関数 `generate_html_response()` は、`str` のHTMLを返すのではなく、`Response` を生成して返しています。 -`generate_html_response()` を呼び出した結果を返すことにより、**FastAPI** の振る舞いを上書きする `Response` が既に返されています。 +`generate_html_response()` を呼び出した結果を返すことにより、デフォルトの **FastAPI** の挙動をオーバーライドする `Response` をすでに返しています。 -しかし、一方では `response_class` に `HTMLResponse` を渡しているため、 **FastAPI** はOpenAPIや対話的ドキュメントでHTMLとして `text/html` でドキュメント化する方法を知っています。 +しかし、`response_class` にも `HTMLResponse` を渡しているため、**FastAPI** はOpenAPIと対話的ドキュメントで、`text/html` のHTMLとしてどのようにドキュメント化すればよいかを理解できます: -## 利用可能なレスポンス +## 利用可能なレスポンス { #available-responses } 以下が利用可能なレスポンスの一部です。 @@ -111,11 +117,11 @@ `from starlette.responses import HTMLResponse` も利用できます。 -**FastAPI** は開発者の利便性のために `fastapi.responses` として `starlette.responses` と同じものを提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接提供されます。 +**FastAPI** は開発者の利便性のために、`starlette.responses` と同じものを `fastapi.responses` として提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接提供されます。 /// -### `Response` +### `Response` { #response } メインの `Response` クラスで、他の全てのレスポンスはこれを継承しています。 @@ -128,41 +134,53 @@ * `headers` - 文字列の `dict` 。 * `media_type` - メディアタイプを示す `str` 。例えば `"text/html"` 。 -FastAPI (実際にはStarlette) は自動的にContent-Lengthヘッダーを含みます。また、media_typeに基づいたContent-Typeヘッダーを含み、テキストタイプのためにcharsetを追加します。 +FastAPI(実際にはStarlette)は自動的にContent-Lengthヘッダーを含みます。また、`media_type` に基づいたContent-Typeヘッダーを含み、テキストタイプのためにcharsetを追加します。 -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} -### `HTMLResponse` +### `HTMLResponse` { #htmlresponse } 上で読んだように、テキストやバイトを受け取り、HTMLレスポンスを返します。 -### `PlainTextResponse` +### `PlainTextResponse` { #plaintextresponse } テキストやバイトを受け取り、プレーンテキストのレスポンスを返します。 -{* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *} +{* ../../docs_src/custom_response/tutorial005_py39.py hl[2,7,9] *} -### `JSONResponse` +### `JSONResponse` { #jsonresponse } -データを受け取り、 `application/json` としてエンコードされたレスポンスを返します。 +データを受け取り、`application/json` としてエンコードされたレスポンスを返します。 上で読んだように、**FastAPI** のデフォルトのレスポンスとして利用されます。 -### `ORJSONResponse` +### `ORJSONResponse` { #orjsonresponse } 上で読んだように、`orjson`を使った、高速な代替のJSONレスポンスです。 -### `UJSONResponse` +/// info | 情報 -`ujson`を使った、代替のJSONレスポンスです。 - -/// warning | 注意 - -`ujson` は、いくつかのエッジケースの取り扱いについて、Pythonにビルトインされた実装よりも作りこまれていません。 +これは、例えば `pip install orjson` で `orjson` をインストールする必要があります。 /// -{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} +### `UJSONResponse` { #ujsonresponse } + +`ujson`を使った、代替のJSONレスポンスです。 + +/// info | 情報 + +これは、例えば `pip install ujson` で `ujson` をインストールする必要があります。 + +/// + +/// warning | 注意 + +`ujson` は、いくつかのエッジケースの取り扱いについて、Pythonにビルトインされた実装ほど注意深くありません。 + +/// + +{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *} /// tip | 豆知識 @@ -170,33 +188,61 @@ FastAPI (実際にはStarlette) は自動的にContent-Lengthヘッダーを含 /// -### `RedirectResponse` +### `RedirectResponse` { #redirectresponse } -HTTPリダイレクトを返します。デフォルトでは307ステータスコード (Temporary Redirect) となります。 +HTTPリダイレクトを返します。デフォルトでは307ステータスコード(Temporary Redirect)となります。 -{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} +`RedirectResponse` を直接返せます: -### `StreamingResponse` +{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *} -非同期なジェネレータか通常のジェネレータ・イテレータを受け取り、レスポンスボディをストリームします。 +--- -{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} +または、`response_class` パラメータで使用できます: -#### `StreamingResponse` をファイルライクなオブジェクトとともに使う +{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *} -ファイルライクなオブジェクト (例えば、 `open()` で返されたオブジェクト) がある場合、 `StreamingResponse` に含めて返すことができます。 +その場合、*path operation*関数からURLを直接返せます。 -これにはクラウドストレージとの連携や映像処理など、多くのライブラリが含まれています。 +この場合に使用される `status_code` は `RedirectResponse` のデフォルトである `307` になります。 -{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *} +--- + +また、`status_code` パラメータを `response_class` パラメータと組み合わせて使うこともできます: + +{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *} + +### `StreamingResponse` { #streamingresponse } + +非同期ジェネレータ、または通常のジェネレータ/イテレータを受け取り、レスポンスボディをストリームします。 + +{* ../../docs_src/custom_response/tutorial007_py39.py hl[2,14] *} + +#### ファイルライクオブジェクトで `StreamingResponse` を使う { #using-streamingresponse-with-file-like-objects } + +file-like オブジェクト(例: `open()` で返されるオブジェクト)がある場合、そのfile-likeオブジェクトを反復処理するジェネレータ関数を作れます。 + +そうすれば、最初にすべてをメモリへ読み込む必要はなく、そのジェネレータ関数を `StreamingResponse` に渡して返せます。 + +これにはクラウドストレージとの連携、映像処理など、多くのライブラリが含まれます。 + +{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *} + +1. これはジェネレータ関数です。内部に `yield` 文を含むため「ジェネレータ関数」です。 +2. `with` ブロックを使うことで、ジェネレータ関数が終わった後(つまりレスポンスの送信が完了した後)にfile-likeオブジェクトが確実にクローズされるようにします。 +3. この `yield from` は、`file_like` という名前のものを反復処理するように関数へ指示します。そして反復された各パートについて、そのパートをこのジェネレータ関数(`iterfile`)から来たものとして `yield` します。 + + つまり、内部的に「生成」の作業を別のものへ移譲するジェネレータ関数です。 + + このようにすることで `with` ブロックに入れられ、完了後にfile-likeオブジェクトが確実にクローズされます。 /// tip | 豆知識 -ここでは `async` や `await` をサポートしていない標準の `open()` を使っているので、通常の `def` でpath operationを宣言していることに注意してください。 +ここでは `async` と `await` をサポートしていない標準の `open()` を使っているため、通常の `def` でpath operationを宣言している点に注意してください。 /// -### `FileResponse` +### `FileResponse` { #fileresponse } レスポンスとしてファイルを非同期的にストリームします。 @@ -204,29 +250,63 @@ HTTPリダイレクトを返します。デフォルトでは307ステータス * `path` - ストリームするファイルのファイルパス。 * `headers` - 含めたい任意のカスタムヘッダーの辞書。 -* `media_type` - メディアタイプを示す文字列。セットされなかった場合は、ファイル名やパスからメディアタイプが推察されます。 -* `filename` - セットされた場合、レスポンスの `Content-Disposition` に含まれます。 +* `media_type` - メディアタイプを示す文字列。未設定の場合、ファイル名やパスからメディアタイプが推測されます。 +* `filename` - 設定した場合、レスポンスの `Content-Disposition` に含まれます。 -ファイルレスポンスには、適切な `Content-Length` 、 `Last-Modified` 、 `ETag` ヘッダーが含まれます。 +ファイルレスポンスには、適切な `Content-Length`、`Last-Modified`、`ETag` ヘッダーが含まれます。 -{* ../../docs_src/custom_response/tutorial009.py hl[2,10] *} +{* ../../docs_src/custom_response/tutorial009_py39.py hl[2,10] *} -## デフォルトレスポンスクラス +`response_class` パラメータを使うこともできます: -**FastAPI** クラスのインスタンスか `APIRouter` を生成するときに、デフォルトのレスポンスクラスを指定できます。 +{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *} -定義するためのパラメータは、 `default_response_class` です。 +この場合、*path operation*関数からファイルパスを直接返せます。 -以下の例では、 **FastAPI** は、全ての *path operation* で `JSONResponse` の代わりに `ORJSONResponse` をデフォルトとして利用します。 +## カスタムレスポンスクラス { #custom-response-class } -{* ../../docs_src/custom_response/tutorial010.py hl[2,4] *} +`Response` を継承した独自のカスタムレスポンスクラスを作成して利用できます。 + +例えば、`orjson`を使いたいが、同梱の `ORJSONResponse` クラスで使われていないカスタム設定も使いたいとします。 + +例えば、インデントされ整形されたJSONを返したいので、orjsonオプション `orjson.OPT_INDENT_2` を使いたいとします。 + +`CustomORJSONResponse` を作れます。主に必要なのは、コンテンツを `bytes` として返す `Response.render(content)` メソッドを作ることです: + +{* ../../docs_src/custom_response/tutorial009c_py39.py hl[9:14,17] *} + +これまでは次のように返していたものが: + +```json +{"message": "Hello World"} +``` + +...このレスポンスでは次のように返されます: + +```json +{ + "message": "Hello World" +} +``` + +もちろん、JSONの整形よりも、これを活用するもっと良い方法が見つかるはずです。 😉 + +## デフォルトレスポンスクラス { #default-response-class } + +**FastAPI** クラスのインスタンス、または `APIRouter` を作成する際に、デフォルトで使用するレスポンスクラスを指定できます。 + +これを定義するパラメータは `default_response_class` です。 + +以下の例では、**FastAPI** はすべての*path operation*で、`JSONResponse` の代わりに `ORJSONResponse` をデフォルトとして使います。 + +{* ../../docs_src/custom_response/tutorial010_py39.py hl[2,4] *} /// tip | 豆知識 -前に見たように、 *path operation* の中で `response_class` をオーバーライドできます。 +これまでと同様に、*path operation*で `response_class` をオーバーライドできます。 /// -## その他のドキュメント +## その他のドキュメント { #additional-documentation } -また、OpenAPIでは `responses` を使ってメディアタイプやその他の詳細を宣言することもできます: [Additional Responses in OpenAPI](additional-responses.md){.internal-link target=_blank} +OpenAPIでは `responses` を使ってメディアタイプやその他の詳細を宣言することもできます: [Additional Responses in OpenAPI](additional-responses.md){.internal-link target=_blank}。 diff --git a/docs/ja/docs/advanced/index.md b/docs/ja/docs/advanced/index.md index 22eaf6eb8..1d0f7566c 100644 --- a/docs/ja/docs/advanced/index.md +++ b/docs/ja/docs/advanced/index.md @@ -1,27 +1,21 @@ -# 高度なユーザーガイド +# 高度なユーザーガイド { #advanced-user-guide } -## さらなる機能 +## さらなる機能 { #additional-features } -[チュートリアル - ユーザーガイド](../tutorial/index.md){.internal-link target=_blank}により、**FastAPI**の主要な機能は十分に理解できたことでしょう。 +メインの[チュートリアル - ユーザーガイド](../tutorial/index.md){.internal-link target=_blank}だけで、**FastAPI**の主要な機能を一通り把握するには十分なはずです。 -以降のセクションでは、チュートリアルでは説明しきれなかったオプションや設定、および機能について説明します。 +以降のセクションでは、その他のオプション、設定、追加機能を見ていきます。 /// tip | 豆知識 -以降のセクションは、 **必ずしも"応用編"ではありません**。 +以降のセクションは、**必ずしも「高度」ではありません**。 -ユースケースによっては、その中から解決策を見つけられるかもしれません。 +また、あなたのユースケースに対する解決策が、その中のどれかにある可能性もあります。 /// -## 先にチュートリアルを読む +## 先にチュートリアルを読む { #read-the-tutorial-first } -[チュートリアル - ユーザーガイド](../tutorial/index.md){.internal-link target=_blank}の知識があれば、**FastAPI**の主要な機能を利用することができます。 +メインの[チュートリアル - ユーザーガイド](../tutorial/index.md){.internal-link target=_blank}で得た知識があれば、**FastAPI**の機能の多くは引き続き利用できます。 -以降のセクションは、すでにチュートリアルを読んで、その主要なアイデアを理解できていることを前提としています。 - -## テスト駆動開発のコース - -このセクションの内容を補完するために脱初心者用コースを受けたい場合は、**TestDriven.io**による、Test-Driven Development with FastAPI and Dockerを確認するのがよいかもしれません。 - -現在、このコースで得られた利益の10%が**FastAPI**の開発のために寄付されています。🎉 😄 +また、以降のセクションでは、すでにそれを読んでいて、主要な考え方を理解していることを前提としています。 diff --git a/docs/ja/docs/advanced/path-operation-advanced-configuration.md b/docs/ja/docs/advanced/path-operation-advanced-configuration.md index 05188d5b2..a78c3cb02 100644 --- a/docs/ja/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/ja/docs/advanced/path-operation-advanced-configuration.md @@ -1,30 +1,30 @@ -# Path Operationの高度な設定 +# Path Operationの高度な設定 { #path-operation-advanced-configuration } -## OpenAPI operationId +## OpenAPI operationId { #openapi-operationid } /// warning | 注意 -あなたがOpenAPIの「エキスパート」でなければ、これは必要ないかもしれません。 +OpenAPIの「エキスパート」でなければ、これはおそらく必要ありません。 /// *path operation* で `operation_id` パラメータを利用することで、OpenAPIの `operationId` を設定できます。 -`operation_id` は各オペレーションで一意にする必要があります。 +各オペレーションで一意になるようにする必要があります。 -{* ../../docs_src/path_operation_advanced_configuration/tutorial001.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} -### *path operation関数* の名前をoperationIdとして使用する +### *path operation関数* の名前をoperationIdとして使用する { #using-the-path-operation-function-name-as-the-operationid } -APIの関数名を `operationId` として利用したい場合、すべてのAPIの関数をイテレーションし、各 *path operation* の `operationId` を `APIRoute.name` で上書きすれば可能です。 +APIの関数名を `operationId` として利用したい場合、すべてのAPI関数をイテレーションし、各 *path operation* の `operation_id` を `APIRoute.name` で上書きすれば可能です。 -そうする場合は、すべての *path operation* を追加した後に行う必要があります。 +すべての *path operation* を追加した後に行うべきです。 -{* ../../docs_src/path_operation_advanced_configuration/tutorial002.py hl[2,12:21,24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *} /// tip | 豆知識 -`app.openapi()` を手動でコールする場合、その前に`operationId`を更新する必要があります。 +`app.openapi()` を手動で呼び出す場合、その前に `operationId` を更新するべきです。 /// @@ -32,22 +32,141 @@ APIの関数名を `operationId` として利用したい場合、すべてのAP この方法をとる場合、各 *path operation関数* が一意な名前である必要があります。 -それらが異なるモジュール (Pythonファイル) にあるとしてもです。 +異なるモジュール(Pythonファイル)にある場合でも同様です。 /// -## OpenAPIから除外する +## OpenAPIから除外する { #exclude-from-openapi } -生成されるOpenAPIスキーマ (つまり、自動ドキュメント生成の仕組み) から *path operation* を除外するには、 `include_in_schema` パラメータを `False` にします。 +生成されるOpenAPIスキーマ(つまり、自動ドキュメント生成の仕組み)から *path operation* を除外するには、`include_in_schema` パラメータを使用して `False` に設定します。 -{* ../../docs_src/path_operation_advanced_configuration/tutorial003.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *} -## docstringによる説明の高度な設定 +## docstringによる説明の高度な設定 { #advanced-description-from-docstring } -*path operation関数* のdocstringからOpenAPIに使用する行を制限することができます。 +*path operation関数* のdocstringからOpenAPIに使用する行を制限できます。 -`\f` (「書式送り (Form Feed)」のエスケープ文字) を付与することで、**FastAPI** はOpenAPIに使用される出力をその箇所までに制限します。 +`\f`(エスケープされた「書式送り(form feed)」文字)を追加すると、**FastAPI** はその地点でOpenAPIに使用される出力を切り詰めます。 -ドキュメントには表示されませんが、他のツール (例えばSphinx) では残りの部分を利用できるでしょう。 +ドキュメントには表示されませんが、他のツール(Sphinxなど)は残りの部分を利用できます。 -{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} + +## 追加レスポンス { #additional-responses } + +*path operation* に対して `response_model` と `status_code` を宣言する方法はすでに見たことがあるでしょう。 + +それにより、*path operation* のメインのレスポンスに関するメタデータが定義されます。 + +追加のレスポンスについても、モデルやステータスコードなどとともに宣言できます。 + +これについてはドキュメントに章全体があります。 [OpenAPIの追加レスポンス](additional-responses.md){.internal-link target=_blank} で読めます。 + +## OpenAPI Extra { #openapi-extra } + +アプリケーションで *path operation* を宣言すると、**FastAPI** はOpenAPIスキーマに含めるために、その *path operation* に関連するメタデータを自動的に生成します。 + +/// note | 技術詳細 + +OpenAPI仕様では Operation Object と呼ばれています。 + +/// + +これには *path operation* に関するすべての情報が含まれ、自動ドキュメントを生成するために使われます。 + +`tags`、`parameters`、`requestBody`、`responses` などが含まれます。 + +この *path operation* 固有のOpenAPIスキーマは通常 **FastAPI** により自動生成されますが、拡張することもできます。 + +/// tip | 豆知識 + +これは低レベルな拡張ポイントです。 + +追加レスポンスを宣言するだけなら、より便利な方法として [OpenAPIの追加レスポンス](additional-responses.md){.internal-link target=_blank} を使うことができます。 + +/// + +`openapi_extra` パラメータを使って、*path operation* のOpenAPIスキーマを拡張できます。 + +### OpenAPI Extensions { #openapi-extensions } + +この `openapi_extra` は、例えば [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions) を宣言するのに役立ちます。 + +{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *} + +自動APIドキュメントを開くと、その拡張は特定の *path operation* の下部に表示されます。 + + + +そして(APIの `/openapi.json` にある)生成されたOpenAPIを見ると、その拡張も特定の *path operation* の一部として確認できます。 + +```JSON hl_lines="22" +{ + "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": {} + } + } + } + }, + "x-aperture-labs-portal": "blue" + } + } + } +} +``` + +### カスタムOpenAPI *path operation* スキーマ { #custom-openapi-path-operation-schema } + +`openapi_extra` 内の辞書は、*path operation* 用に自動生成されたOpenAPIスキーマと深くマージされます。 + +そのため、自動生成されたスキーマに追加データを加えることができます。 + +例えば、Pydanticを使ったFastAPIの自動機能を使わずに独自のコードでリクエストを読み取り・検証することを選べますが、それでもOpenAPIスキーマでリクエストを定義したい場合があります。 + +それは `openapi_extra` で行えます。 + +{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} + +この例では、Pydanticモデルを一切宣言していません。実際、リクエストボディはJSONとして parsed されず、直接 `bytes` として読み取られます。そして `magic_data_reader()` 関数が、何らかの方法でそれをパースする責務を担います。 + +それでも、リクエストボディに期待されるスキーマを宣言できます。 + +### カスタムOpenAPI content type { #custom-openapi-content-type } + +同じトリックを使って、PydanticモデルでJSON Schemaを定義し、それを *path operation* 用のカスタムOpenAPIスキーマセクションに含めることができます。 + +また、リクエスト内のデータ型がJSONでない場合でもこれを行えます。 + +例えばこのアプリケーションでは、PydanticモデルからJSON Schemaを抽出するFastAPIの統合機能や、JSONの自動バリデーションを使っていません。実際、リクエストのcontent typeをJSONではなくYAMLとして宣言しています。 + +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} + +それでも、デフォルトの統合機能を使っていないにもかかわらず、YAMLで受け取りたいデータのために、Pydanticモデルを使って手動でJSON Schemaを生成しています。 + +そしてリクエストを直接使い、ボディを `bytes` として抽出します。これは、FastAPIがリクエストペイロードをJSONとしてパースしようとすらしないことを意味します。 + +その後、コード内でそのYAMLコンテンツを直接パースし、さらに同じPydanticモデルを使ってYAMLコンテンツを検証しています。 + +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} + +/// tip | 豆知識 + +ここでは同じPydanticモデルを再利用しています。 + +ただし同様に、別の方法で検証することもできます。 + +/// diff --git a/docs/ja/docs/advanced/response-directly.md b/docs/ja/docs/advanced/response-directly.md index 42412d507..7e83b9ffb 100644 --- a/docs/ja/docs/advanced/response-directly.md +++ b/docs/ja/docs/advanced/response-directly.md @@ -1,4 +1,4 @@ -# レスポンスを直接返す +# レスポンスを直接返す { #return-a-response-directly } **FastAPI** の *path operation* では、通常は任意のデータを返すことができます: 例えば、 `dict`、`list`、Pydanticモデル、データベースモデルなどです。 @@ -10,7 +10,7 @@ これは例えば、カスタムヘッダーやcookieを返すときに便利です。 -## `Response` を返す +## `Response` を返す { #return-a-response } 実際は、`Response` やそのサブクラスを返すことができます。 @@ -26,7 +26,7 @@ これは多くの柔軟性を提供します。任意のデータ型を返したり、任意のデータ宣言やバリデーションをオーバーライドできます。 -## `jsonable_encoder` を `Response` の中で使う +## `jsonable_encoder` を `Response` の中で使う { #using-the-jsonable-encoder-in-a-response } **FastAPI** はあなたが返す `Response` に対して何も変更を加えないので、コンテンツが準備できていることを保証しなければなりません。 @@ -34,7 +34,7 @@ このようなケースでは、レスポンスにデータを含める前に `jsonable_encoder` を使ってデータを変換できます。 -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} /// note | 技術詳細 @@ -44,7 +44,7 @@ /// -## カスタム `Response` を返す +## カスタム `Response` を返す { #returning-a-custom-response } 上記の例では必要な部分を全て示していますが、あまり便利ではありません。`item` を直接返すことができるし、**FastAPI** はそれを `dict` に変換して `JSONResponse` に含めてくれるなど。すべて、デフォルトの動作です。 @@ -54,9 +54,9 @@ XMLを文字列にし、`Response` に含め、それを返します。 -{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} -## 備考 +## 備考 { #notes } `Response` を直接返す場合、バリデーションや、変換 (シリアライズ) や、自動ドキュメントは行われません。 diff --git a/docs/ja/docs/advanced/websockets.md b/docs/ja/docs/advanced/websockets.md index 2517530ab..6c68c9f0b 100644 --- a/docs/ja/docs/advanced/websockets.md +++ b/docs/ja/docs/advanced/websockets.md @@ -1,10 +1,10 @@ -# WebSocket +# WebSockets { #websockets } -**FastAPI**でWebSocketが使用できます。 +**FastAPI**でWebSocketsが使用できます。 -## `WebSockets`のインストール +## `websockets`のインストール { #install-websockets } -まず `WebSockets`のインストールが必要です。 +[仮想環境](../virtual-environments.md){.internal-link target=_blank}を作成し、それを有効化してから、「WebSocket」プロトコルを簡単に使えるようにするPythonライブラリの`websockets`をインストールしてください。
@@ -16,13 +16,13 @@ $ pip install websockets
-## WebSocket クライアント +## WebSockets クライアント { #websockets-client } -### 本番環境 +### 本番環境 { #in-production } 本番環境では、React、Vue.js、Angularなどの最新のフレームワークで作成されたフロントエンドを使用しているでしょう。 -そして、バックエンドとWebSocketを使用して通信するために、おそらくフロントエンドのユーティリティを使用することになるでしょう。 +そして、バックエンドとWebSocketsを使用して通信するために、おそらくフロントエンドのユーティリティを使用することになるでしょう。 または、ネイティブコードでWebSocketバックエンドと直接通信するネイティブモバイルアプリケーションがあるかもしれません。 @@ -30,21 +30,21 @@ $ pip install websockets --- -ただし、この例では非常にシンプルなHTML文書といくつかのJavaScriptを、すべてソースコードの中に入れて使用することにします。 +ただし、この例では非常にシンプルなHTML文書といくつかのJavaScriptを、すべて長い文字列の中に入れて使用することにします。 もちろん、これは最適な方法ではありませんし、本番環境で使うことはないでしょう。 本番環境では、上記の方法のいずれかの選択肢を採用することになるでしょう。 -しかし、これはWebSocketのサーバーサイドに焦点を当て、実用的な例を示す最も簡単な方法です。 +しかし、これはWebSocketsのサーバーサイドに焦点を当て、動作する例を示す最も簡単な方法です。 -{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[2,6:38,41:43] *} -## `websocket` を作成する +## `websocket` を作成する { #create-a-websocket } **FastAPI** アプリケーションで、`websocket` を作成します。 -{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[1,46:47] *} /// note | 技術詳細 @@ -54,22 +54,22 @@ $ pip install websockets /// -## メッセージの送受信 +## メッセージを待機して送信する { #await-for-messages-and-send-messages } -WebSocketルートでは、 `await` を使ってメッセージの送受信ができます。 +WebSocketルートでは、メッセージを待機して送信するために `await` を使用できます。 -{* ../../docs_src/websockets/tutorial001.py hl[48:52] *} +{* ../../docs_src/websockets/tutorial001_py39.py hl[48:52] *} バイナリやテキストデータ、JSONデータを送受信できます。 -## 試してみる +## 試してみる { #try-it } -ファイル名が `main.py` である場合、以下の方法でアプリケーションを実行します。 +ファイル名が `main.py` である場合、以下でアプリケーションを実行します。
```console -$ uvicorn main:app --reload +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -86,7 +86,7 @@ $ uvicorn main:app --reload -そして、 WebSocketを使用した**FastAPI**アプリケーションが応答します。 +そして、 WebSocketsを使用した**FastAPI**アプリケーションが応答します。 @@ -96,7 +96,7 @@ $ uvicorn main:app --reload そして、これらの通信はすべて同じWebSocket接続を使用します。 -## 依存関係 +## `Depends` などの使用 { #using-depends-and-others } WebSocketエンドポイントでは、`fastapi` から以下をインポートして使用できます。 @@ -107,28 +107,26 @@ WebSocketエンドポイントでは、`fastapi` から以下をインポート * `Path` * `Query` -これらは、他のFastAPI エンドポイント/*path operation* の場合と同じように機能します。 +これらは、他のFastAPI エンドポイント/*path operations* の場合と同じように機能します。 -{* ../../docs_src/websockets/tutorial002.py hl[58:65,68:83] *} +{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} /// info | 情報 -WebSocket で `HTTPException` を発生させることはあまり意味がありません。したがって、WebSocketの接続を直接閉じる方がよいでしょう。 +これはWebSocketであるため、`HTTPException` を発生させることはあまり意味がありません。代わりに `WebSocketException` を発生させます。 クロージングコードは、仕様で定義された有効なコードの中から使用することができます。 -将来的には、どこからでも `raise` できる `WebSocketException` が用意され、専用の例外ハンドラを追加できるようになる予定です。これは、Starlette の PR #527 に依存するものです。 - /// -### 依存関係を用いてWebSocketsを試してみる +### 依存関係を用いてWebSocketsを試してみる { #try-the-websockets-with-dependencies } -ファイル名が `main.py` である場合、以下の方法でアプリケーションを実行します。 +ファイル名が `main.py` である場合、以下でアプリケーションを実行します。
```console -$ uvicorn main:app --reload +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -137,14 +135,14 @@ $ uvicorn main:app --reload ブラウザで http://127.0.0.1:8000 を開きます。 -クライアントが設定できる項目は以下の通りです。 +そこで、以下を設定できます。 * パスで使用される「Item ID」 * クエリパラメータとして使用される「Token」 /// tip | 豆知識 -クエリ `token` は依存パッケージによって処理されることに注意してください。 +クエリ `token` は依存関係によって処理されることに注意してください。 /// @@ -152,11 +150,11 @@ $ uvicorn main:app --reload -## 切断や複数クライアントへの対応 +## 切断や複数クライアントの処理 { #handling-disconnections-and-multiple-clients } WebSocket接続が閉じられると、 `await websocket.receive_text()` は例外 `WebSocketDisconnect` を発生させ、この例のようにキャッチして処理することができます。 -{* ../../docs_src/websockets/tutorial003.py hl[81:83] *} +{* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *} 試してみるには、 @@ -174,15 +172,15 @@ Client #1596980209979 left the chat 上記のアプリは、複数の WebSocket 接続に対してメッセージを処理し、ブロードキャストする方法を示すための最小限のシンプルな例です。 -しかし、すべての接続がメモリ内の単一のリストで処理されるため、プロセスの実行中にのみ機能し、単一のプロセスでのみ機能することに注意してください。 +しかし、すべてがメモリ内の単一のリストで処理されるため、プロセスの実行中にのみ機能し、単一のプロセスでのみ機能することに注意してください。 -もしFastAPIと簡単に統合できて、RedisやPostgreSQLなどでサポートされている、より堅牢なものが必要なら、encode/broadcaster を確認してください。 +FastAPIと簡単に統合できて、RedisやPostgreSQLなどでサポートされている、より堅牢なものが必要なら、encode/broadcaster を確認してください。 /// -## その他のドキュメント +## 詳細情報 { #more-info } オプションの詳細については、Starletteのドキュメントを確認してください。 -* `WebSocket` クラス -* クラスベースのWebSocket処理 +* `WebSocket` クラス. +* クラスベースのWebSocket処理. diff --git a/docs/ja/docs/benchmarks.md b/docs/ja/docs/benchmarks.md index 966d199c5..fbfba2e63 100644 --- a/docs/ja/docs/benchmarks.md +++ b/docs/ja/docs/benchmarks.md @@ -1,34 +1,34 @@ -# ベンチマーク +# ベンチマーク { #benchmarks } -TechEmpowerの独立したベンチマークでは、Uvicornの下で動作する**FastAPI**アプリケーションは、利用可能な最速のPythonフレームワークの1つであり、下回っているのはStarletteとUvicorn自体 (FastAPIによって内部で使用される) のみだと示されています。 +TechEmpowerの独立したベンチマークでは、Uvicornの下で動作する**FastAPI**アプリケーションは、利用可能な最速のPythonフレームワークの1つであり、下回っているのはStarletteとUvicorn自体(FastAPIによって内部で使用される)のみだと示されています。 ただし、ベンチマークを確認し、比較する際には下記の内容に気を付けてください。 -## ベンチマークと速度 +## ベンチマークと速度 { #benchmarks-and-speed } -ベンチマークを確認する時、異なるツールを同等なものと比較するのが一般的です。 +ベンチマークを確認する時、異なるタイプの複数のツールが同等のものとして比較されているのを目にするのが一般的です。 -具体的には、Uvicorn、Starlette、FastAPIを (他の多くのツールと) 比較しました。 +具体的には、Uvicorn、Starlette、FastAPIを(他の多くのツールの中で)まとめて比較しているのを目にすることがあります。 ツールで解決する問題がシンプルなほど、パフォーマンスが向上します。また、ほとんどのベンチマークは、ツールから提供される追加機能をテストしていません。 階層関係はこのようになります。 * **Uvicorn**: ASGIサーバー - * **Starlette**: (Uvicornを使用) WEBマイクロフレームワーク - * **FastAPI**: (Starletteを使用) データバリデーションなどの、APIを構築する追加機能を備えたAPIマイクロフレームワーク + * **Starlette**: (Uvicornを使用)webマイクロフレームワーク + * **FastAPI**: (Starletteを使用)データバリデーションなど、APIを構築するためのいくつかの追加機能を備えたAPIマイクロフレームワーク * **Uvicorn**: - * サーバー自体に余分なコードが少ないので、最高のパフォーマンスが得られます。 - * Uvicornにアプリケーションを直接書くことはできません。つまり、あなたのコードには、Starlette (または** FastAPI **) が提供するコードを、多かれ少なかれ含める必要があります。そうすると、最終的なアプリケーションは、フレームワークを使用してアプリのコードとバグを最小限に抑えた場合と同じオーバーヘッドになります。 - * もしUvicornを比較する場合は、Daphne、Hypercorn、uWSGIなどのアプリケーションサーバーと比較してください。 + * サーバー自体以外に余分なコードがあまりないため、最高のパフォーマンスになります。 + * Uvicornにアプリケーションを直接書くことはないでしょう。それは、あなたのコードに、Starlette(または**FastAPI**)が提供するコードを、少なくとも多かれ少なかれ含める必要があるということです。そして、もしそうした場合、最終的なアプリケーションは、フレームワークを使用してアプリのコードとバグを最小限に抑えた場合と同じオーバーヘッドになります。 + * Uvicornを比較する場合は、Daphne、Hypercorn、uWSGIなどのアプリケーションサーバーと比較してください。 * **Starlette**: - * Uvicornに次ぐ性能を持つでしょう。実際、StarletteはUvicornを使用しています。だから、より多くのコードを実行する必要があり、Uvicornよりも「遅く」なってしまうだけなのです。 - * しかし、パスベースのルーティングなどのシンプルなWEBアプリケーションを構築する機能を提供します。 - * もしStarletteを比較する場合は、Sanic、Flask、DjangoなどのWEBフレームワーク (もしくはマイクロフレームワーク) と比較してください。 + * Uvicornに次ぐ性能になるでしょう。実際、Starletteは実行にUvicornを使用しています。そのため、おそらく、より多くのコードを実行しなければならない分だけ、Uvicornより「遅く」なるだけです。 + * しかし、パスに基づくルーティングなどを使って、シンプルなwebアプリケーションを構築するためのツールを提供します。 + * Starletteを比較する場合は、Sanic、Flask、Djangoなどのwebフレームワーク(またはマイクロフレームワーク)と比較してください。 * **FastAPI**: - * StarletteがUvicornを使っているのと同じで、**FastAPI**はStarletteを使っており、それより速くできません。 - * FastAPIはStarletteの上にさらに多くの機能を提供します。データの検証やシリアライゼーションなど、APIを構築する際に常に必要な機能です。また、それを使用することで、自動ドキュメント化を無料で取得できます (ドキュメントは実行中のアプリケーションにオーバーヘッドを追加せず、起動時に生成されます) 。 - * FastAPIを使用せず、直接Starlette (またはSanic, Flask, Responderなど) を使用した場合、データの検証とシリアライズをすべて自分で実装する必要があります。そのため、最終的なアプリケーションはFastAPIを使用して構築した場合と同じオーバーヘッドが発生します。そして、多くの場合、このデータ検証とシリアライズは、アプリケーションのコードの中で最大の記述量になります。 - * FastAPIを使用することで、開発時間、バグ、コード行数を節約でき、使用しない場合 (あなたが全ての機能を実装し直した場合) と同じかそれ以上のパフォーマンスを得られます。 - * もしFastAPIを比較する場合は、Flask-apispec、NestJS、Moltenなどのデータ検証や、シリアライズの機能を提供するWEBフレームワーク (や機能のセット) と比較してください。これらはデータの自動検証や、シリアライズ、ドキュメント化が統合されたフレームワークです。 + * StarletteがUvicornを使用しており、それより速くできないのと同じように、**FastAPI**はStarletteを使用しているため、それより速くできません。 + * FastAPIはStarletteの上に、より多くの機能を提供します。データバリデーションやシリアライゼーションのように、APIを構築する際にほとんど常に必要な機能です。また、それを使用することで、自動ドキュメント化を無料で利用できます(自動ドキュメントは実行中のアプリケーションにオーバーヘッドを追加せず、起動時に生成されます)。 + * FastAPIを使用せず、Starletteを直接(またはSanic、Flask、Responderなど別のツールを)使用した場合、データバリデーションとシリアライゼーションをすべて自分で実装する必要があります。そのため、最終的なアプリケーションはFastAPIを使用して構築した場合と同じオーバーヘッドが発生します。そして多くの場合、このデータバリデーションとシリアライゼーションは、アプリケーションで書かれるコードの大部分になります。 + * そのため、FastAPIを使用することで、開発時間、バグ、コード行数を節約でき、使用しない場合(あなたがそれをすべて自分のコードで実装する必要があるため)と比べて、同じパフォーマンス(またはそれ以上)を得られる可能性があります。 + * FastAPIを比較する場合は、Flask-apispec、NestJS、Moltenなど、データバリデーション、シリアライゼーション、ドキュメント化を提供するwebアプリケーションフレームワーク(またはツール群)と比較してください。自動データバリデーション、シリアライゼーション、ドキュメント化が統合されたフレームワークです。 diff --git a/docs/ja/docs/deployment/concepts.md b/docs/ja/docs/deployment/concepts.md index a0d4fb35b..787eb2e73 100644 --- a/docs/ja/docs/deployment/concepts.md +++ b/docs/ja/docs/deployment/concepts.md @@ -1,4 +1,4 @@ -# デプロイメントのコンセプト +# デプロイメントのコンセプト { #deployments-concepts } **FastAPI**を用いたアプリケーションをデプロイするとき、もしくはどのようなタイプのWeb APIであっても、おそらく気になるコンセプトがいくつかあります。 @@ -10,12 +10,12 @@ * 起動時の実行 * 再起動 * レプリケーション(実行中のプロセス数) -* メモリー +* メモリ * 開始前の事前のステップ これらが**デプロイメント**にどのような影響を与えるかを見ていきましょう。 -最終的な目的は、**安全な方法で**APIクライアントに**サービスを提供**し、**中断を回避**するだけでなく、**計算リソース**(例えばリモートサーバー/仮想マシン)を可能な限り効率的に使用することです。🚀 +最終的な目的は、**安全な方法で**APIクライアントに**サービスを提供**し、**中断を回避**するだけでなく、**計算リソース**(例えばリモートサーバー/仮想マシン)を可能な限り効率的に使用することです。 🚀 この章では前述した**コンセプト**についてそれぞれ説明します。 @@ -27,16 +27,16 @@ しかし、今はこれらの重要な**コンセプトに基づくアイデア**を確認しましょう。これらのコンセプトは、他のどのタイプのWeb APIにも当てはまります。💡 -## セキュリティ - HTTPS +## セキュリティ - HTTPS { #security-https } [前チャプターのHTTPSについて](https.md){.internal-link target=_blank}では、HTTPSがどのようにAPIを暗号化するのかについて学びました。 通常、アプリケーションサーバにとって**外部の**コンポーネントである**TLS Termination Proxy**によって提供されることが一般的です。このプロキシは通信の暗号化を担当します。 -さらにセキュアな通信において、HTTPS証明書の定期的な更新を行いますが、これはTLS Termination Proxyと同じコンポーネントが担当することもあれば、別のコンポーネントが担当することもあります。 +さらに、HTTPS証明書の更新を担当するものが必要で、同じコンポーネントが担当することもあれば、別のコンポーネントが担当することもあります。 -### HTTPS 用ツールの例 +### HTTPS 用ツールの例 { #example-tools-for-https } TLS Termination Proxyとして使用できるツールには以下のようなものがあります: * Traefik @@ -59,11 +59,11 @@ TLS Termination Proxyとして使用できるツールには以下のような 次に考慮すべきコンセプトは、実際のAPIを実行するプログラム(例:Uvicorn)に関連するものすべてです。 -## プログラム と プロセス +## プログラム と プロセス { #program-and-process } 私たちは「**プロセス**」という言葉についてたくさん話すので、その意味や「**プログラム**」という言葉との違いを明確にしておくと便利です。 -### プログラムとは何か +### プログラムとは何か { #what-is-a-program } **プログラム**という言葉は、一般的にいろいろなものを表現するのに使われます: @@ -71,7 +71,7 @@ TLS Termination Proxyとして使用できるツールには以下のような * OSによって実行することができるファイル(例: `python`, `python.exe` or `uvicorn`) * OS上で**実行**している間、CPUを使用し、メモリ上に何かを保存する特定のプログラム(**プロセス**とも呼ばれる) -### プロセスとは何か +### プロセスとは何か { #what-is-a-process } **プロセス**という言葉は通常、より具体的な意味で使われ、OSで実行されているものだけを指します(先ほどの最後の説明のように): @@ -92,27 +92,29 @@ OSの「タスク・マネージャー」や「システム・モニター」( さて、**プロセス**と**プログラム**という用語の違いを確認したところで、デプロイメントについて話を続けます。 -## 起動時の実行 +## 起動時の実行 { #running-on-startup } ほとんどの場合、Web APIを作成するときは、クライアントがいつでもアクセスできるように、**常に**中断されることなく**実行される**ことを望みます。もちろん、特定の状況でのみ実行させたい特別な理由がある場合は別ですが、その時間のほとんどは、常に実行され、**利用可能**であることを望みます。 -### リモートサーバー上での実行 +### リモートサーバー上での実行 { #in-a-remote-server } -リモートサーバー(クラウドサーバー、仮想マシンなど)をセットアップするときにできる最も簡単なことは、ローカルで開発するときと同じように、Uvicorn(または同様のもの)を手動で実行することです。 この方法は**開発中**には役に立つと思われます。 +リモートサーバー(クラウドサーバー、仮想マシンなど)をセットアップするときにできる最も簡単なことは、ローカルで開発するときと同じように、`fastapi run`(Uvicornを使用します)や同様のものを手動で実行することです。 + +そしてこれは動作し、**開発中**には役に立つでしょう。 しかし、サーバーへの接続が切れた場合、**実行中のプロセス**はおそらくダウンしてしまうでしょう。 そしてサーバーが再起動された場合(アップデートやクラウドプロバイダーからのマイグレーションの後など)、おそらくあなたはそれに**気づかないでしょう**。そのため、プロセスを手動で再起動しなければならないことすら気づかないでしょう。つまり、APIはダウンしたままなのです。😱 -### 起動時に自動的に実行 +### 起動時に自動的に実行 { #run-automatically-on-startup } 一般的に、サーバープログラム(Uvicornなど)はサーバー起動時に自動的に開始され、**人の介入**を必要とせずに、APIと一緒にプロセスが常に実行されるようにしたいと思われます(UvicornがFastAPIアプリを実行するなど)。 -### 別のプログラムの用意 +### 別のプログラムの用意 { #separate-program } これを実現するために、通常は**別のプログラム**を用意し、起動時にアプリケーションが実行されるようにします。そして多くの場合、他のコンポーネントやアプリケーション、例えばデータベースも実行されるようにします。 -### 起動時に実行するツールの例 +### 起動時に実行するツールの例 { #example-tools-to-run-at-startup } 実行するツールの例をいくつか挙げます: @@ -127,31 +129,33 @@ OSの「タスク・マネージャー」や「システム・モニター」( 次の章で、より具体的な例を挙げていきます。 -## 再起動 +## 再起動 { #restarts } 起動時にアプリケーションが実行されることを確認するのと同様に、失敗後にアプリケーションが**再起動**されることも確認したいと思われます。 -### 我々は間違いを犯す +### 我々は間違いを犯す { #we-make-mistakes } 私たち人間は常に**間違い**を犯します。ソフトウェアには、ほとんど常に**バグ**があらゆる箇所に隠されています。🐛 -### 小さなエラーは自動的に処理される +そして私たち開発者は、それらのバグを見つけたり新しい機能を実装したりしながらコードを改善し続けます(新しいバグも追加してしまうかもしれません😅)。 + +### 小さなエラーは自動的に処理される { #small-errors-automatically-handled } FastAPIでWeb APIを構築する際に、コードにエラーがある場合、FastAPIは通常、エラーを引き起こした単一のリクエストにエラーを含めます。🛡 クライアントはそのリクエストに対して**500 Internal Server Error**を受け取りますが、アプリケーションは完全にクラッシュするのではなく、次のリクエストのために動作を続けます。 -### 重大なエラー - クラッシュ +### 重大なエラー - クラッシュ { #bigger-errors-crashes } しかしながら、**アプリケーション全体をクラッシュさせるようなコードを書いて**UvicornとPythonをクラッシュさせるようなケースもあるかもしれません。💥 -それでも、ある箇所でエラーが発生したからといって、アプリケーションを停止させたままにしたくないでしょう。 少なくとも壊れていない*パスオペレーション*については、**実行し続けたい**はずです。 +それでも、ある箇所でエラーが発生したからといって、アプリケーションを停止させたままにしたくないでしょう。 少なくとも壊れていない*path operation*については、**実行し続けたい**はずです。 -### クラッシュ後の再起動 +### クラッシュ後の再起動 { #restart-after-crash } しかし、実行中の**プロセス**をクラッシュさせるような本当にひどいエラーの場合、少なくとも2〜3回ほどプロセスを**再起動**させる外部コンポーネントが必要でしょう。 -/// tip +/// tip | 豆知識 ...とはいえ、アプリケーション全体が**すぐにクラッシュする**のであれば、いつまでも再起動し続けるのは意味がないでしょう。しかし、その場合はおそらく開発中か少なくともデプロイ直後に気づくと思われます。 @@ -161,7 +165,7 @@ FastAPIでWeb APIを構築する際に、コードにエラーがある場合、 あなたはおそらく**外部コンポーネント**がアプリケーションの再起動を担当することを望むと考えます。 なぜなら、その時点でUvicornとPythonを使った同じアプリケーションはすでにクラッシュしており、同じアプリケーションの同じコードに対して何もできないためです。 -### 自動的に再起動するツールの例 +### 自動的に再起動するツールの例 { #example-tools-to-restart-automatically } ほとんどの場合、前述した**起動時にプログラムを実行する**ために使用されるツールは、自動で**再起動**することにも利用されます。 @@ -176,19 +180,19 @@ FastAPIでWeb APIを構築する際に、コードにエラーがある場合、 * クラウドプロバイダーがサービスの一部として内部的に処理 * そのほか... -## レプリケーション - プロセスとメモリー +## レプリケーション - プロセスとメモリ { #replication-processes-and-memory } -FastAPI アプリケーションでは、Uvicorn のようなサーバープログラムを使用し、**1つのプロセス**で1度に複数のクライアントに同時に対応できます。 +FastAPI アプリケーションでは、Uvicorn を実行する `fastapi` コマンドのようなサーバープログラムを使用し、**1つのプロセス**で1度に複数のクライアントに同時に対応できます。 しかし、多くの場合、複数のワーカー・プロセスを同時に実行したいと考えるでしょう。 -### 複数のプロセス - Worker +### 複数のプロセス - Worker { #multiple-processes-workers } クライアントの数が単一のプロセスで処理できる数を超えており(たとえば仮想マシンがそれほど大きくない場合)、かつサーバーの CPU に**複数のコア**がある場合、同じアプリケーションで同時に**複数のプロセス**を実行させ、すべてのリクエストを分散させることができます。 同じAPIプログラムの**複数のプロセス**を実行する場合、それらは一般的に**Worker/ワーカー**と呼ばれます。 -### ワーカー・プロセス と ポート +### ワーカー・プロセス と ポート { #worker-processes-and-ports } [HTTPSについて](https.md){.internal-link target=_blank}のドキュメントで、1つのサーバーで1つのポートとIPアドレスの組み合わせでリッスンできるのは1つのプロセスだけであることを覚えていますでしょうか? @@ -197,13 +201,13 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ そのため、**複数のプロセス**を同時に持つには**ポートでリッスンしている単一のプロセス**が必要であり、それが何らかの方法で各ワーカー・プロセスに通信を送信することが求められます。 -### プロセスあたりのメモリー +### プロセスあたりのメモリ { #memory-per-process } さて、プログラムがメモリにロードする際には、例えば機械学習モデルや大きなファイルの内容を変数に入れたりする場合では、**サーバーのメモリ(RAM)**を少し消費します。 そして複数のプロセスは通常、**メモリを共有しません**。これは、実行中の各プロセスがそれぞれ独自の変数やメモリ等を持っていることを意味します。つまり、コード内で大量のメモリを消費している場合、**各プロセス**は同等の量のメモリを消費することになります。 -### サーバーメモリー +### サーバーメモリ { #server-memory } 例えば、あなたのコードが **1GBのサイズの機械学習モデル**をロードする場合、APIで1つのプロセスを実行すると、少なくとも1GBのRAMを消費します。 @@ -211,7 +215,7 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ リモートサーバーや仮想マシンのRAMが3GBしかない場合、4GB以上のRAMをロードしようとすると問題が発生します。🚨 -### 複数プロセス - 例 +### 複数プロセス - 例 { #multiple-processes-an-example } この例では、2つの**ワーカー・プロセス**を起動し制御する**マネージャー・ プロセス**があります。 @@ -227,7 +231,7 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ 毎回同程度の計算を行うAPIがあり、多くのクライアントがいるのであれば、**CPU使用率**もおそらく**安定**するでしょう(常に急激に上下するのではなく)。 -### レプリケーション・ツールと戦略の例 +### レプリケーション・ツールと戦略の例 { #examples-of-replication-tools-and-strategies } これを実現するにはいくつかのアプローチがありますが、具体的な戦略については次の章(Dockerやコンテナの章など)で詳しく説明します。 @@ -237,25 +241,22 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ 考えられる組み合わせと戦略をいくつか紹介します: -* **Gunicorn**が**Uvicornワーカー**を管理 - * Gunicornは**IP**と**ポート**をリッスンする**プロセスマネージャ**で、レプリケーションは**複数のUvicornワーカー・プロセス**を持つことによって行われる。 -* **Uvicorn**が**Uvicornワーカー**を管理 +* `--workers` を指定した **Uvicorn** * 1つのUvicornの**プロセスマネージャー**が**IP**と**ポート**をリッスンし、**複数のUvicornワーカー・プロセス**を起動する。 * **Kubernetes**やその他の分散**コンテナ・システム** * **Kubernetes**レイヤーの何かが**IP**と**ポート**をリッスンする。レプリケーションは、**複数のコンテナ**にそれぞれ**1つのUvicornプロセス**を実行させることで行われる。 * **クラウド・サービス**によるレプリケーション * クラウド・サービスはおそらく**あなたのためにレプリケーションを処理**します。**実行するプロセス**や使用する**コンテナイメージ**を定義できるかもしれませんが、いずれにせよ、それはおそらく**単一のUvicornプロセス**であり、クラウドサービスはそのレプリケーションを担当するでしょう。 -/// tip +/// tip | 豆知識 これらの**コンテナ**やDockerそしてKubernetesに関する項目が、まだあまり意味をなしていなくても心配しないでください。 - -コンテナ・イメージ、Docker、Kubernetesなどについては、次の章で詳しく説明します: [コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}. +コンテナ・イメージ、Docker、Kubernetesなどについては、将来の章で詳しく説明します: [コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}. /// -## 開始前の事前のステップ +## 開始前の事前のステップ { #previous-steps-before-starting } アプリケーションを**開始する前**に、いくつかのステップを実行したい場合が多くあります。 @@ -271,7 +272,7 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ もちろん、事前のステップを何度も実行しても問題がない場合もあり、その際は対処がかなり楽になります。 -/// tip +/// tip | 豆知識 また、セットアップによっては、アプリケーションを開始する前の**事前のステップ**が必要ない場合もあることを覚えておいてください。 @@ -279,7 +280,7 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ /// -### 事前ステップの戦略例 +### 事前ステップの戦略例 { #examples-of-previous-steps-strategies } これは**システムを**デプロイする方法に**大きく依存**するだろうし、おそらくプログラムの起動方法や再起動の処理などにも関係してくるでしょう。 @@ -289,14 +290,13 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ * 事前のステップを実行し、アプリケーションを起動するbashスクリプト * 利用するbashスクリプトを起動/再起動したり、エラーを検出したりする方法は以前として必要になるでしょう。 -/// tip +/// tip | 豆知識 - -コンテナを使った具体的な例については、次の章で紹介します: [コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}. +コンテナを使った具体的な例については、将来の章で紹介します: [コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}. /// -## リソースの利用 +## リソースの利用 { #resource-utilization } あなたのサーバーは**リソース**であり、プログラムを実行しCPUの計算時間や利用可能なRAMメモリを消費または**利用**することができます。 @@ -319,7 +319,7 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ `htop`のような単純なツールを使って、サーバーで使用されているCPUやRAM、あるいは各プロセスで使用されている量を見ることができます。あるいは、より複雑な監視ツールを使って、サーバに分散して使用することもできます。 -## まとめ +## まとめ { #recap } アプリケーションのデプロイ方法を決定する際に、考慮すべきであろう主要なコンセプトのいくつかを紹介していきました: @@ -327,7 +327,7 @@ FastAPI アプリケーションでは、Uvicorn のようなサーバープロ * 起動時の実行 * 再起動 * レプリケーション(実行中のプロセス数) -* メモリー +* メモリ * 開始前の事前ステップ これらの考え方とその適用方法を理解することで、デプロイメントを設定したり調整したりする際に必要な直感的な判断ができるようになるはずです。🤓 diff --git a/docs/ja/docs/deployment/docker.md b/docs/ja/docs/deployment/docker.md index 53fc851f1..6c182448c 100644 --- a/docs/ja/docs/deployment/docker.md +++ b/docs/ja/docs/deployment/docker.md @@ -1,20 +1,17 @@ -# コンテナ内のFastAPI - Docker +# コンテナ内のFastAPI - Docker { #fastapi-in-containers-docker } -FastAPIアプリケーションをデプロイする場合、一般的なアプローチは**Linuxコンテナ・イメージ**をビルドすることです。 - -基本的には **Docker**を用いて行われます。生成されたコンテナ・イメージは、いくつかの方法のいずれかでデプロイできます。 +FastAPIアプリケーションをデプロイする場合、一般的なアプローチは**Linuxコンテナ・イメージ**をビルドすることです。基本的には **Docker**を用いて行われます。生成されたコンテナ・イメージは、いくつかの方法のいずれかでデプロイできます。 Linuxコンテナの使用には、**セキュリティ**、**反復可能性(レプリカビリティ)**、**シンプリシティ**など、いくつかの利点があります。 -/// tip +/// tip | 豆知識 -TODO: なぜか遷移できない お急ぎで、すでにこれらの情報をご存じですか? [以下の`Dockerfile`の箇所👇](#build-a-docker-image-for-fastapi)へジャンプしてください。 ///
-Dockerfile プレビュー 👀 +Dockerfile Preview 👀 ```Dockerfile FROM python:3.9 @@ -27,15 +24,15 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./app /code/app -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +CMD ["fastapi", "run", "app/main.py", "--port", "80"] # If running behind a proxy like Nginx or Traefik add --proxy-headers -# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"] +# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"] ```
-## コンテナとは何か +## コンテナとは何か { #what-is-a-container } コンテナ(主にLinuxコンテナ)は、同じシステム内の他のコンテナ(他のアプリケーションやコンポーネント)から隔離された状態を保ちながら、すべての依存関係や必要なファイルを含むアプリケーションをパッケージ化する非常に**軽量**な方法です。 @@ -45,7 +42,7 @@ Linuxコンテナは、ホスト(マシン、仮想マシン、クラウドサ コンテナはまた、独自の**分離された**実行プロセス(通常は1つのプロセスのみ)や、ファイルシステム、ネットワークを持ちます。 このことはデプロイ、セキュリティ、開発などを簡素化させます。 -## コンテナ・イメージとは何か +## コンテナ・イメージとは何か { #what-is-a-container-image } **コンテナ**は、**コンテナ・イメージ**から実行されます。 @@ -53,23 +50,17 @@ Linuxコンテナは、ホスト(マシン、仮想マシン、クラウドサ 保存された静的コンテンツである「**コンテナイメージ**」とは対照的に、「**コンテナ**」は通常、実行中のインスタンス、つまり**実行**されているものを指します。 -**コンテナ**が起動され実行されるとき(**コンテナイメージ**から起動されるとき)、ファイルや環境変数などが作成されたり変更されたりする可能性があります。 - -これらの変更はそのコンテナ内にのみ存在しますが、基盤となるコンテナ・イメージには残りません(ディスクに保存されません)。 +**コンテナ**が起動され実行されるとき(**コンテナイメージ**から起動されるとき)、ファイルや環境変数などが作成されたり変更されたりする可能性があります。これらの変更はそのコンテナ内にのみ存在しますが、基盤となるコンテナ・イメージには残りません(ディスクに保存されません)。 コンテナイメージは **プログラム** ファイルやその内容、例えば `python` と `main.py` ファイルに匹敵します。 -そして、**コンテナ**自体は(**コンテナイメージ**とは対照的に)イメージをもとにした実際の実行中のインスタンスであり、**プロセス**に匹敵します。 +そして、**コンテナ**自体は(**コンテナイメージ**とは対照的に)イメージをもとにした実際の実行中のインスタンスであり、**プロセス**に匹敵します。実際、コンテナが実行されているのは、**プロセスが実行されている**ときだけです(通常は単一のプロセスだけです)。 コンテナ内で実行中のプロセスがない場合、コンテナは停止します。 -実際、コンテナが実行されているのは、**プロセスが実行されている**ときだけです(通常は単一のプロセスだけです)。 コンテナ内で実行中のプロセスがない場合、コンテナは停止します。 - -## コンテナ・イメージ +## コンテナ・イメージ { #container-images } Dockerは、**コンテナ・イメージ**と**コンテナ**を作成・管理するための主要なツールの1つです。 -そして、DockerにはDockerイメージ(コンテナ)を共有するDocker Hubというものがあります。 - -Docker Hubは 多くのツールや環境、データベース、アプリケーションに対応している予め作成された**公式のコンテナ・イメージ**をパブリックに提供しています。 +そして、多くのツールや環境、データベース、アプリケーションに対応している予め作成された**公式のコンテナ・イメージ**をパブリックに提供しているDocker Hubというものがあります。 例えば、公式イメージの1つにPython Imageがあります。 @@ -88,7 +79,7 @@ Docker Hubは 多くのツールや環境、データベース、アプリケー すべてのコンテナ管理システム(DockerやKubernetesなど)には、こうしたネットワーキング機能が統合されています。 -## コンテナとプロセス +## コンテナとプロセス { #containers-and-processes } 通常、**コンテナ・イメージ**はそのメタデータに**コンテナ**の起動時に実行されるデフォルトのプログラムまたはコマンドと、そのプログラムに渡されるパラメータを含みます。コマンドラインでの操作とよく似ています。 @@ -100,7 +91,7 @@ Docker Hubは 多くのツールや環境、データベース、アプリケー しかし、**少なくとも1つの実行中のプロセス**がなければ、実行中のコンテナを持つことはできないです。メイン・プロセスが停止すれば、コンテナも停止します。 -## Build a Docker Image for FastAPI +## FastAPI用のDockerイメージをビルドする { #build-a-docker-image-for-fastapi } ということで、何か作りましょう!🚀 @@ -112,7 +103,7 @@ FastAPI用の**Dockerイメージ**を、**公式Python**イメージに基づ * **Raspberry Pi**で実行する場合 * コンテナ・イメージを実行してくれるクラウド・サービスなどを利用する場合 -### パッケージ要件(package requirements) +### パッケージ要件 { #package-requirements } アプリケーションの**パッケージ要件**は通常、何らかのファイルに記述されているはずです。 @@ -125,9 +116,8 @@ FastAPI用の**Dockerイメージ**を、**公式Python**イメージに基づ 例えば、`requirements.txt` は次のようになります: ``` -fastapi>=0.68.0,<0.69.0 -pydantic>=1.8.0,<2.0.0 -uvicorn>=0.15.0,<0.16.0 +fastapi[standard]>=0.113.0,<0.114.0 +pydantic>=2.7.0,<3.0.0 ``` そして通常、例えば `pip` を使ってこれらのパッケージの依存関係をインストールします: @@ -137,28 +127,24 @@ uvicorn>=0.15.0,<0.16.0 ```console $ pip install -r requirements.txt ---> 100% -Successfully installed fastapi pydantic uvicorn +Successfully installed fastapi pydantic ```
-/// info +/// info | 情報 パッケージの依存関係を定義しインストールするためのフォーマットやツールは他にもあります。 -Poetryを使った例は、後述するセクションでご紹介します。👇 - /// -### **FastAPI**コードを作成する +### **FastAPI**コードを作成する { #create-the-fastapi-code } -* `app` ディレクトリを作成し、その中に入ります -* 空のファイル `__init__.py` を作成します -* `main.py` ファイルを作成します: +* `app` ディレクトリを作成し、その中に入ります。 +* 空のファイル `__init__.py` を作成します。 +* 次の内容で `main.py` ファイルを作成します: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -170,32 +156,32 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` -### Dockerfile +### Dockerfile { #dockerfile } 同じプロジェクト・ディレクトリに`Dockerfile`というファイルを作成します: ```{ .dockerfile .annotate } -# (1) +# (1)! FROM python:3.9 -# (2) +# (2)! WORKDIR /code -# (3) +# (3)! COPY ./requirements.txt /code/requirements.txt -# (4) +# (4)! RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -# (5) +# (5)! COPY ./app /code/app -# (6) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +# (6)! +CMD ["fastapi", "run", "app/main.py", "--port", "80"] ``` 1. 公式のPythonベースイメージから始めます @@ -211,9 +197,10 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] このファイルは**頻繁には変更されない**ので、Dockerはこのステップではそれを検知し**キャッシュ**を使用し、次のステップでもキャッシュを有効にします。 4. 要件ファイルにあるパッケージの依存関係をインストールします + `--no-cache-dir` オプションはダウンロードしたパッケージをローカルに保存しないように `pip` に指示します。これは、同じパッケージをインストールするために `pip` を再度実行する場合にのみ有効ですが、コンテナで作業する場合はそうではないです。 - /// note + /// note | 備考 `--no-cache-dir`は`pip`に関連しているだけで、Dockerやコンテナとは何の関係もないです。 @@ -225,26 +212,56 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] このステップでキャッシュを使用すると、開発中にイメージを何度もビルドする際に、**毎回**すべての依存関係を**ダウンロードしてインストールする**代わりに多くの**時間**を**節約**できます。 -5. ./app` ディレクトリを `/code` ディレクトリの中にコピーする。 +5. `./app` ディレクトリを `/code` ディレクトリの中にコピーする。 これには**最も頻繁に変更される**すべてのコードが含まれているため、Dockerの**キャッシュ**は**これ以降のステップ**に簡単に使用されることはありません。 そのため、コンテナイメージのビルド時間を最適化するために、`Dockerfile`の **最後** にこれを置くことが重要です。 -6. `uvicorn`サーバーを実行するための**コマンド**を設定します +6. 内部でUvicornを使用する `fastapi run` を使うための**コマンド**を設定します `CMD` は文字列のリストを取り、それぞれの文字列はスペースで区切られたコマンドラインに入力するものです。 このコマンドは **現在の作業ディレクトリ**から実行され、上記の `WORKDIR /code` にて設定した `/code` ディレクトリと同じです。 - そのためプログラムは `/code` で開始しその中にあなたのコードがある `./app` ディレクトリがあるので、**Uvicorn** は `app.main` から `app` を参照し、**インポート** することができます。 +/// tip | 豆知識 -/// tip - -コード内の"+"の吹き出しをクリックして、各行が何をするのかをレビューしてください。👆 +コード内の各番号バブルをクリックして、各行が何をするのかをレビューしてください。👆 /// +/// warning | 注意 + +以下で説明する通り、`CMD` 命令は**常に** **exec形式**を使用してください。 + +/// + +#### `CMD` を使う - Exec形式 { #use-cmd-exec-form } + +Docker命令 `CMD` は2つの形式で書けます: + +✅ **Exec** 形式: + +```Dockerfile +# ✅ Do this +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +⛔️ **Shell** 形式: + +```Dockerfile +# ⛔️ Don't do this +CMD fastapi run app/main.py --port 80 +``` + +FastAPIが正常にシャットダウンでき、[lifespan events](../advanced/events.md){.internal-link target=_blank}がトリガーされるように、常に **exec** 形式を使用してください。 + +詳しくは、shell形式とexec形式に関するDockerドキュメントをご覧ください。 + +これは `docker compose` を使用する場合にかなり目立つことがあります。より技術的な詳細は、このDocker ComposeのFAQセクションをご覧ください:Why do my services take 10 seconds to recreate or stop?。 + +#### ディレクトリ構造 { #directory-structure } + これで、次のようなディレクトリ構造になるはずです: ``` @@ -256,17 +273,15 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] └── requirements.txt ``` -#### TLS Termination Proxyの裏側 +#### TLS Termination Proxyの裏側 { #behind-a-tls-termination-proxy } -Nginx や Traefik のような TLS Termination Proxy (ロードバランサ) の後ろでコンテナを動かしている場合は、`--proxy-headers`オプションを追加します。 - -このオプションは、Uvicornにプロキシ経由でHTTPSで動作しているアプリケーションに対して、送信されるヘッダを信頼するよう指示します。 +Nginx や Traefik のような TLS Termination Proxy (ロードバランサ) の後ろでコンテナを動かしている場合は、`--proxy-headers`オプションを追加します。これにより、(FastAPI CLI経由で)Uvicornに対して、そのプロキシから送信されるヘッダを信頼し、アプリケーションがHTTPSの裏で実行されていることなどを示すよう指示します。 ```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] ``` -#### Dockerキャッシュ +#### Dockerキャッシュ { #docker-cache } この`Dockerfile`には重要なトリックがあり、まず**依存関係だけのファイル**をコピーします。その理由を説明します。 @@ -300,11 +315,11 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./app /code/app ``` -### Dockerイメージをビルドする +### Dockerイメージをビルドする { #build-the-docker-image } すべてのファイルが揃ったので、コンテナ・イメージをビルドしましょう。 -* プロジェクトディレクトリに移動します(`Dockerfile`がある場所で、`app`ディレクトリがあります) +* プロジェクトディレクトリに移動します(`Dockerfile`がある場所で、`app`ディレクトリがあります)。 * FastAPI イメージをビルドします:
@@ -317,7 +332,7 @@ $ docker build -t myimage .
-/// tip +/// tip | 豆知識 末尾の `.` に注目してほしいです。これは `./` と同じ意味です。 これはDockerにコンテナイメージのビルドに使用するディレクトリを指示します。 @@ -325,7 +340,7 @@ $ docker build -t myimage . /// -### Dockerコンテナの起動する +### Dockerコンテナの起動する { #start-the-docker-container } * イメージに基づいてコンテナを実行します: @@ -337,7 +352,7 @@ $ docker run -d --name mycontainer -p 80:80 myimage
-## 確認する +## 確認する { #check-it } Dockerコンテナのhttp://192.168.99.100/items/5?q=somequeryhttp://127.0.0.1/items/5?q=somequery (またはそれに相当するDockerホストを使用したもの)といったURLで確認できるはずです。 @@ -347,7 +362,7 @@ Dockerコンテナのhttp://192.168.99.100/docshttp://127.0.0.1/docs (またはそれに相当するDockerホストを使用したもの) @@ -355,7 +370,7 @@ Dockerコンテナのhttp://192.168.99.100/redochttp://127.0.0.1/redoc (またはそれに相当するDockerホストを使用したもの)にもアクセスできます。 @@ -363,9 +378,10 @@ DockerコンテナのTraefikのように、**HTTPS**と**証明書**の**自動**取得を扱う別のコンテナである可能性もあります。 -/// tip +/// tip | 豆知識 TraefikはDockerやKubernetesなどと統合されているので、コンテナ用のHTTPSの設定や構成はとても簡単です。 @@ -428,7 +444,7 @@ TraefikはDockerやKubernetesなどと統合されているので、コンテナ あるいは、(コンテナ内でアプリケーションを実行しながら)クラウド・プロバイダーがサービスの1つとしてHTTPSを処理することもできます。 -## 起動時および再起動時の実行 +## 起動時および再起動時の実行 { #running-on-startup-and-restarts } 通常、コンテナの**起動と実行**を担当する別のツールがあります。 @@ -438,21 +454,21 @@ TraefikはDockerやKubernetesなどと統合されているので、コンテナ コンテナを使わなければ、アプリケーションを起動時や再起動時に実行させるのは面倒で難しいかもしれません。しかし、**コンテナ**で作業する場合、ほとんどのケースでその機能はデフォルトで含まれています。✨ -## レプリケーション - プロセス数 +## レプリケーション - プロセス数 { #replication-number-of-processes } -**Kubernetes** や Docker Swarm モード、Nomad、あるいは複数のマシン上で分散コンテナを管理するための同様の複雑なシステムを使ってマシンのクラスターを構成している場合、 各コンテナで(Workerを持つGunicornのような)**プロセスマネージャ**を使用する代わりに、**クラスター・レベル**で**レプリケーション**を処理したいと思うでしょう。 +**Kubernetes** や Docker Swarm モード、Nomad、あるいは複数のマシン上で分散コンテナを管理するための同様の複雑なシステムを使ってマシンのclusterを構成している場合、 各コンテナで(Workerを持つUvicornのような)**プロセスマネージャ**を使用する代わりに、**クラスター・レベル**で**レプリケーション**を処理したいと思うでしょう。 Kubernetesのような分散コンテナ管理システムの1つは通常、入ってくるリクエストの**ロードバランシング**をサポートしながら、**コンテナのレプリケーション**を処理する統合された方法を持っています。このことはすべて**クラスタレベル**にてです。 -そのような場合、UvicornワーカーでGunicornのようなものを実行するのではなく、[上記の説明](#dockerfile)のように**Dockerイメージをゼロから**ビルドし、依存関係をインストールして、**単一のUvicornプロセス**を実行したいでしょう。 +そのような場合、[上記の説明](#dockerfile)のように**Dockerイメージをゼロから**ビルドし、依存関係をインストールして、**単一のUvicornプロセス**を実行したいでしょう。複数のUvicornワーカーを使う代わりにです。 -### ロードバランサー +### ロードバランサー { #load-balancer } コンテナを使用する場合、通常はメイン・ポート**でリスニング**しているコンポーネントがあるはずです。それはおそらく、**HTTPS**を処理するための**TLS Termination Proxy**でもある別のコンテナであったり、同様のツールであったりするでしょう。 このコンポーネントはリクエストの **負荷** を受け、 (うまくいけば) その負荷を**バランスよく** ワーカーに分配するので、一般に **ロードバランサ** とも呼ばれます。 -/// tip +/// tip | 豆知識 HTTPSに使われるものと同じ**TLS Termination Proxy**コンポーネントは、おそらく**ロードバランサー**にもなるでしょう。 @@ -460,7 +476,7 @@ HTTPSに使われるものと同じ**TLS Termination Proxy**コンポーネン そしてコンテナで作業する場合、コンテナの起動と管理に使用する同じシステムには、**ロードバランサー**(**TLS Termination Proxy**の可能性もある)から**ネットワーク通信**(HTTPリクエストなど)をアプリのあるコンテナ(複数可)に送信するための内部ツールが既にあるはずです。 -### 1つのロードバランサー - 複数のワーカーコンテナー +### 1つのロードバランサー - 複数のワーカーコンテナー { #one-load-balancer-multiple-worker-containers } **Kubernetes**や同様の分散コンテナ管理システムで作業する場合、その内部のネットワーキングのメカニズムを使用することで、メインの**ポート**でリッスンしている単一の**ロードバランサー**が、アプリを実行している可能性のある**複数のコンテナ**に通信(リクエスト)を送信できるようになります。 @@ -470,56 +486,61 @@ HTTPSに使われるものと同じ**TLS Termination Proxy**コンポーネン そして通常、この**ロードバランサー**は、クラスタ内の*他の*アプリケーション(例えば、異なるドメインや異なるURLパスのプレフィックスの配下)へのリクエストを処理することができ、その通信をクラスタ内で実行されている*他の*アプリケーションのための適切なコンテナに送信します。 -### 1コンテナにつき1プロセス +### 1コンテナにつき1プロセス { #one-process-per-container } この種のシナリオでは、すでにクラスタ・レベルでレプリケーションを処理しているため、おそらくコンテナごとに**単一の(Uvicorn)プロセス**を持ちたいでしょう。 -この場合、Uvicornワーカーを持つGunicornのようなプロセスマネージャーや、Uvicornワーカーを使うUvicornは**避けたい**でしょう。**コンテナごとにUvicornのプロセスは1つだけ**にしたいでしょう(おそらく複数のコンテナが必要でしょう)。 +この場合、例えばコマンドラインオプションの `--workers` で、コンテナ内に複数のワーカーを持つことは**避けたい**でしょう。**コンテナごとにUvicornのプロセスは1つだけ**にしたいでしょう(おそらく複数のコンテナが必要でしょう)。 -(GunicornやUvicornがUvicornワーカーを管理するように)コンテナ内に別のプロセスマネージャーを持つことは、クラスターシステムですでに対処しているであろう**不要な複雑さ**を追加するだけです。 +(複数のワーカーの場合のように)コンテナ内に別のプロセスマネージャーを持つことは、クラスターシステムですでに対処しているであろう**不要な複雑さ**を追加するだけです。 -### Containers with Multiple Processes and Special Cases +### 複数プロセスのコンテナと特殊なケース { #containers-with-multiple-processes-and-special-cases } -もちろん、**特殊なケース**として、**Gunicornプロセスマネージャ**を持つ**コンテナ**内で複数の**Uvicornワーカープロセス**を起動させたい場合があります。 +もちろん、**特殊なケース**として、**コンテナ**内で複数の**Uvicornワーカープロセス**を起動させたい場合があります。 -このような場合、**公式のDockerイメージ**を使用することができます。このイメージには、複数の**Uvicornワーカープロセス**を実行するプロセスマネージャとして**Gunicorn**が含まれており、現在のCPUコアに基づいてワーカーの数を自動的に調整するためのデフォルト設定がいくつか含まれています。詳しくは後述の[Gunicornによる公式Dockerイメージ - Uvicorn](#gunicorndocker-uvicorn)で説明します。 +そのような場合、`--workers` コマンドラインオプションを使って、実行したいワーカー数を設定できます: + +```{ .dockerfile .annotate } +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app + +# (1)! +CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"] +``` + +1. ここでは `--workers` コマンドラインオプションを使って、ワーカー数を4に設定しています。 以下は、それが理にかなっている場合の例です: -#### シンプルなアプリケーション +#### シンプルなアプリ { #a-simple-app } -アプリケーションを**シンプル**な形で実行する場合、プロセス数の細かい調整が必要ない場合、自動化されたデフォルトを使用するだけで、コンテナ内にプロセスマネージャが必要かもしれません。例えば、公式Dockerイメージでシンプルな設定が可能です。 +アプリケーションが、クラスタではなく**単一サーバ**で実行できるほど**シンプル**である場合、コンテナ内にプロセスマネージャが欲しくなることがあります。 -#### Docker Compose +#### Docker Compose { #docker-compose } -Docker Composeで**シングルサーバ**(クラスタではない)にデプロイすることもできますので、共有ネットワークと**ロードバランシング**を維持しながら(Docker Composeで)コンテナのレプリケーションを管理する簡単な方法はないでしょう。 +Docker Composeで**単一サーバ**(クラスタではない)にデプロイすることもできますので、共有ネットワークと**ロードバランシング**を維持しながら(Docker Composeで)コンテナのレプリケーションを管理する簡単な方法はないでしょう。 その場合、**単一のコンテナ**で、**プロセスマネージャ**が内部で**複数のワーカープロセス**を起動するようにします。 -#### Prometheusとその他の理由 - -また、**1つのコンテナ**に**1つのプロセス**を持たせるのではなく、**1つのコンテナ**に**複数のプロセス**を持たせる方が簡単だという**他の理由**もあるでしょう。 - -例えば、(セットアップにもよりますが)Prometheusエクスポーターのようなツールを同じコンテナ内に持つことができます。 - -この場合、**複数のコンテナ**があると、デフォルトでは、Prometheusが**メトリクスを**読みに来たとき、すべてのレプリケートされたコンテナの**蓄積されたメトリクス**を取得するのではなく、毎回**単一のコンテナ**(その特定のリクエストを処理したコンテナ)のものを取得することになります。 - -その場合、**複数のプロセス**を持つ**1つのコンテナ**を用意し、同じコンテナ上のローカルツール(例えばPrometheusエクスポーター)がすべての内部プロセスのPrometheusメトリクスを収集し、その1つのコンテナ上でそれらのメトリクスを公開する方がシンプルかもしれません。 - --- -重要なのは、盲目的に従わなければならない普遍のルールはないということです。 - -これらのアイデアは、**あなた自身のユースケース**を評価し、あなたのシステムに最適なアプローチを決定するために使用することができます: +重要なのは、これらのどれも、盲目的に従わなければならない「**絶対的なルール**」ではないということです。これらのアイデアは、**あなた自身のユースケース**を評価し、あなたのシステムに最適なアプローチを決定するために使用できます。次の概念をどう管理するかを確認してください: * セキュリティ - HTTPS * 起動時の実行 * 再起動 -* **レプリケーション(実行中のプロセス数)** +* レプリケーション(実行中のプロセス数) * メモリ * 開始前の事前ステップ -## メモリー +## メモリ { #memory } コンテナごとに**単一のプロセスを実行する**と、それらのコンテナ(レプリケートされている場合は1つ以上)によって消費される多かれ少なかれ明確に定義された、安定し制限された量のメモリを持つことになります。 @@ -531,109 +552,47 @@ Docker Composeで**シングルサーバ**(クラスタではない)にデ しかし、**多くのメモリを使用**している場合(たとえば**機械学習**モデルなど)、どれだけのメモリを消費しているかを確認し、**各マシンで実行するコンテナの数**を調整する必要があります(そしておそらくクラスタにマシンを追加します)。 -**コンテナごとに複数のプロセス**を実行する場合(たとえば公式のDockerイメージで)、起動するプロセスの数が**利用可能なメモリ以上に消費しない**ようにする必要があります。 +**コンテナごとに複数のプロセス**を実行する場合、起動するプロセスの数が**利用可能なメモリ以上に消費しない**ようにする必要があります。 -## 開始前の事前ステップとコンテナ +## 開始前の事前ステップとコンテナ { #previous-steps-before-starting-and-containers } コンテナ(DockerやKubernetesなど)を使っている場合、主に2つのアプローチがあります。 -### 複数のコンテナ +### 複数のコンテナ { #multiple-containers } -複数の**コンテナ**があり、おそらくそれぞれが**単一のプロセス**を実行している場合(**Kubernetes**クラスタなど)、レプリケートされたワーカーコンテナを実行する**前に**、単一のコンテナで**事前のステップ**の作業を行う**別のコンテナ**を持ちたいと思うでしょう。 +複数の**コンテナ**があり、おそらくそれぞれが**単一のプロセス**を実行している場合(例えば、**Kubernetes**クラスタなど)、レプリケートされたワーカーコンテナを実行する**前に**、単一のコンテナで**事前のステップ**の作業を行う**別のコンテナ**を持ちたいと思うでしょう。 -/// info +/// info | 情報 -もしKubernetesを使用している場合, これはおそらくInit コンテナでしょう。 +もしKubernetesを使用している場合, これはおそらくInit Containerでしょう。 /// -ユースケースが事前のステップを**並列で複数回**実行するのに問題がない場合(例:データベースの準備チェック)、メインプロセスを開始する前に、それらのステップを各コンテナに入れることが可能です。 +ユースケースが事前のステップを**並列で複数回**実行するのに問題がない場合(例:データベースマイグレーションを実行するのではなく、データベースの準備ができたかをチェックするだけの場合)、メインプロセスを開始する直前に、それらのステップを各コンテナに入れることも可能です。 -### 単一コンテナ +### 単一コンテナ { #single-container } -単純なセットアップで、**単一のコンテナ**で複数の**ワーカー・プロセス**(または1つのプロセスのみ)を起動する場合、アプリでプロセスを開始する直前に、同じコンテナで事前のステップを実行できます。公式Dockerイメージは、内部的にこれをサポートしています。 +単純なセットアップで、**単一のコンテナ**で複数の**ワーカープロセス**(または1つのプロセスのみ)を起動する場合、アプリでプロセスを開始する直前に、同じコンテナで事前のステップを実行できます。 -## Gunicornによる公式Dockerイメージ - Uvicorn +### ベースDockerイメージ { #base-docker-image } -前の章で詳しく説明したように、Uvicornワーカーで動作するGunicornを含む公式のDockerイメージがあります: [Server Workers - Gunicorn と Uvicorn](server-workers.md){.internal-link target=_blank}で詳しく説明しています。 +以前は、公式のFastAPI Dockerイメージがありました:tiangolo/uvicorn-gunicorn-fastapi。しかし、現在は非推奨です。⛔️ -このイメージは、主に上記で説明した状況で役に立つでしょう: [複数のプロセスと特殊なケースを持つコンテナ(Containers with Multiple Processes and Special Cases)](#containers-with-multiple-processes-and-special-cases) +おそらく、このベースDockerイメージ(またはその他の類似のもの)は**使用しない**方がよいでしょう。 -* tiangolo/uvicorn-gunicorn-fastapi. +すでに**Kubernetes**(または他のもの)を使用していて、複数の**コンテナ**で、クラスタレベルで**レプリケーション**を設定している場合。そのような場合は、上記で説明したように**ゼロから**イメージを構築する方がよいでしょう:[FastAPI用のDockerイメージをビルドする](#build-a-docker-image-for-fastapi)。 -/// warning +また、複数のワーカーが必要な場合は、単純に `--workers` コマンドラインオプションを使用できます。 -このベースイメージや類似のイメージは**必要ない**可能性が高いので、[上記の: FastAPI用のDockerイメージをビルドする(Build a Docker Image for FastAPI)](#build-a-docker-image-for-fastapi)のようにゼロからイメージをビルドする方が良いでしょう。 +/// note | 技術詳細 + +このDockerイメージは、Uvicornが停止したワーカーの管理と再起動をサポートしていなかった頃に作成されたため、Uvicornと一緒にGunicornを使う必要がありました。これは、GunicornにUvicornワーカープロセスの管理と再起動をさせるだけのために、かなりの複雑さを追加していました。 + +しかし現在は、Uvicorn(および `fastapi` コマンド)が `--workers` をサポートしているため、自分でビルドする代わりにベースDockerイメージを使う理由はありません(コード量もだいたい同じです 😅)。 /// -このイメージには、利用可能なCPUコアに基づいて**ワーカー・プロセスの数**を設定する**オートチューニング**メカニズムが含まれています。 - -これは**賢明なデフォルト**を備えていますが、**環境変数**や設定ファイルを使ってすべての設定を変更したり更新したりすることができます。 - -また、スクリプトで**開始前の事前ステップ**を実行することもサポートしている。 - -/// tip - -すべての設定とオプションを見るには、Dockerイメージのページをご覧ください: tiangolo/uvicorn-gunicorn-fastapi - -/// - -### 公式Dockerイメージのプロセス数 - -このイメージの**プロセス数**は、利用可能なCPU**コア**から**自動的に計算**されます。 - -つまり、CPUから可能な限り**パフォーマンス**を**引き出そう**とします。 - -また、**環境変数**などを使った設定で調整することもできます。 - -しかし、プロセスの数はコンテナが実行しているCPUに依存するため、**消費されるメモリの量**もそれに依存することになります。 - -そのため、(機械学習モデルなどで)大量のメモリを消費するアプリケーションで、サーバーのCPUコアが多いが**メモリが少ない**場合、コンテナは利用可能なメモリよりも多くのメモリを使おうとすることになります。 - -その結果、パフォーマンスが大幅に低下する(あるいはクラッシュする)可能性があります。🚨 - -### Dockerfileを作成する - -この画像に基づいて`Dockerfile`を作成する方法を以下に示します: - -```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app -``` - -### より大きなアプリケーション - -[複数のファイルを持つ大きなアプリケーション](../tutorial/bigger-applications.md){.internal-link target=_blank}を作成するセクションに従った場合、`Dockerfile`は次のようになります: - -```Dockerfile hl_lines="7" -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app/app -``` - -### いつ使うのか - -おそらく、**Kubernetes**(または他のもの)を使用していて、すでにクラスタレベルで複数の**コンテナ**で**レプリケーション**を設定している場合は、この公式ベースイメージ(または他の類似のもの)は**使用すべきではありません**。 - -そのような場合は、上記のように**ゼロから**イメージを構築する方がよいでしょう: [FastAPI用のDockerイメージをビルドする(Build a Docker Image for FastAPI)](#build-a-docker-image-for-fastapi) を参照してください。 - -このイメージは、主に上記の[複数のプロセスと特殊なケースを持つコンテナ(Containers with Multiple Processes and Special Cases)](#containers-with-multiple-processes-and-special-cases)で説明したような特殊なケースで役に立ちます。 - -例えば、アプリケーションが**シンプル**で、CPUに応じたデフォルトのプロセス数を設定すればうまくいく場合や、クラスタレベルでレプリケーションを手動で設定する手間を省きたい場合、アプリで複数のコンテナを実行しない場合などです。 - -または、**Docker Compose**でデプロイし、単一のサーバで実行している場合などです。 - -## コンテナ・イメージのデプロイ +## コンテナ・イメージのデプロイ { #deploy-the-container-image } コンテナ(Docker)イメージを手に入れた後、それをデプロイするにはいくつかの方法があります。 @@ -645,104 +604,21 @@ COPY ./app /app/app * Nomadのような別のツール * コンテナ・イメージをデプロイするクラウド・サービス -## Poetryを利用したDockerイメージ +## `uv` を使ったDockerイメージ { #docker-image-with-uv } -もしプロジェクトの依存関係を管理するためにPoetryを利用する場合、マルチステージビルドを使うと良いでしょう。 +uv を使ってプロジェクトのインストールと管理をしている場合は、uv Docker guideに従ってください。 -```{ .dockerfile .annotate } -# (1) -FROM python:3.9 as requirements-stage - -# (2) -WORKDIR /tmp - -# (3) -RUN pip install poetry - -# (4) -COPY ./pyproject.toml ./poetry.lock* /tmp/ - -# (5) -RUN poetry export -f requirements.txt --output requirements.txt --without-hashes - -# (6) -FROM python:3.9 - -# (7) -WORKDIR /code - -# (8) -COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt - -# (9) -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt - -# (10) -COPY ./app /code/app - -# (11) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] -``` - -1. これは最初のステージで、`requirements-stage`と名付けられます -2. `/tmp` を現在の作業ディレクトリに設定します - ここで `requirements.txt` というファイルを生成します。 - -3. このDockerステージにPoetryをインストールします - -4. pyproject.toml`と`poetry.lock`ファイルを`/tmp` ディレクトリにコピーします - - `./poetry.lock*`(末尾に`*`)を使用するため、そのファイルがまだ利用できない場合でもクラッシュすることはないです。 -5. requirements.txt`ファイルを生成します - -6. これは最後のステージであり、ここにあるものはすべて最終的なコンテナ・イメージに保存されます -7. 現在の作業ディレクトリを `/code` に設定します -8. `requirements.txt`ファイルを `/code` ディレクトリにコピーします - このファイルは前のDockerステージにしか存在しないため、`--from-requirements-stage`を使ってコピーします。 -9. 生成された `requirements.txt` ファイルにあるパッケージの依存関係をインストールします -10. app` ディレクトリを `/code` ディレクトリにコピーします -11. uvicorn` コマンドを実行して、`app.main` からインポートした `app` オブジェクトを使用するように指示します -/// tip - -"+"の吹き出しをクリックすると、それぞれの行が何をするのかを見ることができます - -/// - -**Dockerステージ**は`Dockerfile`の一部で、**一時的なコンテナイメージ**として動作します。 - -最初のステージは **Poetryのインストール**と Poetry の `pyproject.toml` ファイルからプロジェクトの依存関係を含む**`requirements.txt`を生成**するためだけに使用されます。 - -この `requirements.txt` ファイルは後半の **次のステージ**で `pip` と共に使用されます。 - -最終的なコンテナイメージでは、**最終ステージ**のみが保存されます。前のステージは破棄されます。 - -Poetryを使用する場合、**Dockerマルチステージビルド**を使用することは理にかなっています。 - -なぜなら、最終的なコンテナイメージにPoetryとその依存関係がインストールされている必要はなく、**必要なのは**プロジェクトの依存関係をインストールするために生成された `requirements.txt` ファイルだけだからです。 - -そして次の(そして最終的な)ステージでは、前述とほぼ同じ方法でイメージをビルドします。 - -### TLS Termination Proxyの裏側 - Poetry - -繰り返しになりますが、NginxやTraefikのようなTLS Termination Proxy(ロードバランサー)の後ろでコンテナを動かしている場合は、`--proxy-headers`オプションをコマンドに追加します: - -```Dockerfile -CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] -``` - -## まとめ +## まとめ { #recap } コンテナ・システム(例えば**Docker**や**Kubernetes**など)を使えば、すべての**デプロイメントのコンセプト**を扱うのがかなり簡単になります: -* セキュリティ - HTTPS +* HTTPS * 起動時の実行 * 再起動 -* **レプリケーション(実行中のプロセス数)** +* レプリケーション(実行中のプロセス数) * メモリ * 開始前の事前ステップ ほとんどの場合、ベースとなるイメージは使用せず、公式のPython Dockerイメージをベースにした**コンテナイメージをゼロからビルド**します。 -`Dockerfile`と**Dockerキャッシュ**内の命令の**順番**に注意することで、**ビルド時間を最小化**することができ、生産性を最大化することができます(そして退屈を避けることができます)。😎 - -特別なケースでは、FastAPI用の公式Dockerイメージを使いたいかもしれません。🤓 +`Dockerfile`と**Dockerキャッシュ**内の命令の**順番**に注意することで、**ビルド時間を最小化**し、生産性を最大化できます(そして退屈を避けることができます)。😎 diff --git a/docs/ja/docs/deployment/https.md b/docs/ja/docs/deployment/https.md index 7b0f567aa..d5a6daf0c 100644 --- a/docs/ja/docs/deployment/https.md +++ b/docs/ja/docs/deployment/https.md @@ -1,10 +1,10 @@ -# HTTPS について +# HTTPS について { #about-https } HTTPSは単に「有効」か「無効」かで決まるものだと思いがちです。 しかし、それよりもはるかに複雑です。 -/// tip +/// tip | 豆知識 もし急いでいたり、HTTPSの仕組みについて気にしないのであれば、次のセクションに進み、さまざまなテクニックを使ってすべてをセットアップするステップ・バイ・ステップの手順をご覧ください。 @@ -22,25 +22,19 @@ HTTPSは単に「有効」か「無効」かで決まるものだと思いがち * 接続の暗号化は**TCPレベル**で行われます。 * それは**HTTPの1つ下**のレイヤーです。 * つまり、**証明書と暗号化**の処理は、**HTTPの前**に行われます。 -* **TCPは "ドメイン "について知りません**。IPアドレスについてのみ知っています。 +* **TCPは「ドメイン」について知りません**。IPアドレスについてのみ知っています。 * 要求された**特定のドメイン**に関する情報は、**HTTPデータ**に入ります。 * **HTTPS証明書**は、**特定のドメイン**を「証明」しますが、プロトコルと暗号化はTCPレベルで行われ、どのドメインが扱われているかを**知る前**に行われます。 * **デフォルトでは**、**IPアドレスごとに1つのHTTPS証明書**しか持てないことになります。 * これは、サーバーの規模やアプリケーションの規模に寄りません。 * しかし、これには**解決策**があります。 -* **TLS**プロトコル(HTTPの前に、TCPレベルで暗号化を処理するもの)には、**SNI**と呼ばれる**拡張**があります。 +* **TLS**プロトコル(HTTPの前に、TCPレベルで暗号化を処理するもの)には、**SNI**と呼ばれる**拡張**があります。 * このSNI拡張機能により、1つのサーバー(**単一のIPアドレス**を持つ)が**複数のHTTPS証明書**を持ち、**複数のHTTPSドメイン/アプリケーション**にサービスを提供できるようになります。 * これが機能するためには、**パブリックIPアドレス**でリッスンしている、サーバー上で動作している**単一の**コンポーネント(プログラム)が、サーバー内の**すべてのHTTPS証明書**を持っている必要があります。 - * セキュアな接続を取得した**後**でも、通信プロトコルは**HTTPのまま**です。 * コンテンツは**HTTPプロトコル**で送信されているにもかかわらず、**暗号化**されています。 - -サーバー(マシン、ホストなど)上で**1つのプログラム/HTTPサーバー**を実行させ、**HTTPSに関する全てのこと**を管理するのが一般的です。 - -**暗号化された HTTPS リクエスト** を受信し、**復号化された HTTP リクエスト** を同じサーバーで実行されている実際の HTTP アプリケーション(この場合は **FastAPI** アプリケーション)に送信し、アプリケーションから **HTTP レスポンス** を受け取り、適切な **HTTPS 証明書** を使用して **暗号化** し、そして**HTTPS** を使用してクライアントに送り返します。 - -このサーバーはしばしば **TLS Termination Proxy**と呼ばれます。 +サーバー(マシン、ホストなど)上で**1つのプログラム/HTTPサーバー**を実行させ、**HTTPSに関する全てのこと**を管理するのが一般的です。**暗号化された HTTPS リクエスト** を受信し、**復号化された HTTP リクエスト** を同じサーバーで実行されている実際の HTTP アプリケーション(この場合は **FastAPI** アプリケーション)に送信し、アプリケーションから **HTTP レスポンス** を受け取り、適切な **HTTPS 証明書** を使用して **暗号化** し、そして**HTTPS** を使用してクライアントに送り返します。このサーバーはしばしば **TLS Termination Proxy**と呼ばれます。 TLS Termination Proxyとして使えるオプションには、以下のようなものがあります: @@ -50,7 +44,7 @@ TLS Termination Proxyとして使えるオプションには、以下のよう * HAProxy -## Let's Encrypt +## Let's Encrypt { #lets-encrypt } Let's Encrypt以前は、これらの**HTTPS証明書**は信頼できる第三者によって販売されていました。 @@ -64,27 +58,27 @@ Let's Encrypt以前は、これらの**HTTPS証明書**は信頼できる第三 このアイデアは、これらの証明書の取得と更新を自動化することで、**安全なHTTPSを、無料で、永遠に**利用できるようにすることです。 -## 開発者のための HTTPS +## 開発者のための HTTPS { #https-for-developers } ここでは、HTTPS APIがどのように見えるかの例を、主に開発者にとって重要なアイデアに注意を払いながら、ステップ・バイ・ステップで説明します。 -### ドメイン名 +### ドメイン名 { #domain-name } ステップの初めは、**ドメイン名**を**取得すること**から始まるでしょう。その後、DNSサーバー(おそらく同じクラウドプロバイダー)に設定します。 -おそらくクラウドサーバー(仮想マシン)かそれに類するものを手に入れ、固定の **パブリックIPアドレス**を持つことになるでしょう。 +おそらくクラウドサーバー(仮想マシン)かそれに類するものを手に入れ、fixed **パブリックIPアドレス**を持つことになるでしょう。 -DNSサーバーでは、**取得したドメイン**をあなたのサーバーのパプリック**IPアドレス**に向けるレコード(「`Aレコード`」)を設定します。 +DNSサーバーでは、**取得したドメイン**をあなたのサーバーのパプリック**IPアドレス**に向けるレコード(「`A record`」)を設定します。 これはおそらく、最初の1回だけあり、すべてをセットアップするときに行うでしょう。 -/// tip +/// tip | 豆知識 ドメイン名の話はHTTPSに関する話のはるか前にありますが、すべてがドメインとIPアドレスに依存するため、ここで言及する価値があります。 /// -### DNS +### DNS { #dns } では、実際のHTTPSの部分に注目してみよう。 @@ -94,7 +88,7 @@ DNSサーバーは、ブラウザに特定の**IPアドレス**を使用する -### TLS Handshake の開始 +### TLS Handshake の開始 { #tls-handshake-start } ブラウザはIPアドレスと**ポート443**(HTTPSポート)で通信します。 @@ -104,7 +98,7 @@ DNSサーバーは、ブラウザに特定の**IPアドレス**を使用する TLS接続を確立するためのクライアントとサーバー間のこのやりとりは、**TLSハンドシェイク**と呼ばれます。 -### SNI拡張機能付きのTLS +### SNI拡張機能付きのTLS { #tls-with-sni-extension } サーバー内の**1つのプロセス**だけが、特定 の**IPアドレス**の特定の**ポート** で待ち受けることができます。 @@ -112,7 +106,7 @@ TLS接続を確立するためのクライアントとサーバー間のこの TLS(HTTPS)はデフォルトで`443`という特定のポートを使用する。つまり、これが必要なポートです。 -このポートをリッスンできるのは1つのプロセスだけなので、これを実行するプロセスは**TLS Termination Proxy**となります。 +このポートをリクエストできるのは1つのプロセスだけなので、これを実行するプロセスは**TLS Termination Proxy**となります。 TLS Termination Proxyは、1つ以上の**TLS証明書**(HTTPS証明書)にアクセスできます。 @@ -130,13 +124,13 @@ TLS Termination Proxyは、1つ以上の**TLS証明書**(HTTPS証明書)に これが**HTTPS**であり、純粋な(暗号化されていない)TCP接続ではなく、**セキュアなTLS接続**の中に**HTTP**があるだけです。 -/// tip +/// tip | 豆知識 通信の暗号化は、HTTPレベルではなく、**TCPレベル**で行われることに注意してください。 /// -### HTTPS リクエスト +### HTTPS リクエスト { #https-request } これでクライアントとサーバー(具体的にはブラウザとTLS Termination Proxy)は**暗号化されたTCP接続**を持つことになり、**HTTP通信**を開始することができます。 @@ -144,19 +138,19 @@ TLS Termination Proxyは、1つ以上の**TLS証明書**(HTTPS証明書)に -### リクエストの復号化 +### リクエストの復号化 { #decrypt-the-request } TLS Termination Proxy は、合意が取れている暗号化を使用して、**リクエストを復号化**し、**プレーン (復号化された) HTTP リクエスト** をアプリケーションを実行しているプロセス (例えば、FastAPI アプリケーションを実行している Uvicorn を持つプロセス) に送信します。 -### HTTP レスポンス +### HTTP レスポンス { #http-response } アプリケーションはリクエストを処理し、**プレーン(暗号化されていない)HTTPレスポンス** をTLS Termination Proxyに送信します。 -### HTTPS レスポンス +### HTTPS レスポンス { #https-response } TLS Termination Proxyは次に、事前に合意が取れている暗号(`someapp.example.com`の証明書から始まる)を使って**レスポンスを暗号化し**、ブラウザに送り返す。 @@ -166,7 +160,7 @@ TLS Termination Proxyは次に、事前に合意が取れている暗号(`someap クライアント(ブラウザ)は、レスポンスが正しいサーバーから来たことを知ることができます。 なぜなら、そのサーバーは、以前に**HTTPS証明書**を使って合意した暗号を使っているからです。 -### 複数のアプリケーション +### 複数のアプリケーション { #multiple-applications } 同じサーバー(または複数のサーバー)に、例えば他のAPIプログラムやデータベースなど、**複数のアプリケーション**が存在する可能性があります。 @@ -176,7 +170,7 @@ TLS Termination Proxyは次に、事前に合意が取れている暗号(`someap そうすれば、TLS Termination Proxy は、**複数のドメイン**や複数のアプリケーションのHTTPSと証明書を処理し、それぞれのケースで適切なアプリケーションにリクエストを送信することができます。 -### 証明書の更新 +### 証明書の更新 { #certificate-renewal } 将来のある時点で、各証明書は(取得後約3ヶ月で)**失効**します。 @@ -200,10 +194,42 @@ TLS Termination Proxyは次に、事前に合意が取れている暗号(`someap アプリを提供しながらこのような更新処理を行うことは、アプリケーション・サーバー(Uvicornなど)でTLS証明書を直接使用するのではなく、TLS Termination Proxyを使用して**HTTPSを処理する別のシステム**を用意したくなる主な理由の1つです。 -## まとめ +## プロキシ転送ヘッダー { #proxy-forwarded-headers } + +プロキシを使ってHTTPSを処理する場合、**アプリケーションサーバー**(たとえばFastAPI CLI経由のUvicorn)はHTTPS処理について何も知らず、**TLS Termination Proxy**とはプレーンなHTTPで通信します。 + +この**プロキシ**は通常、リクエストを**アプリケーションサーバー**に転送する前に、その場でいくつかのHTTPヘッダーを設定し、リクエストがプロキシによって**転送**されていることをアプリケーションサーバーに知らせます。 + +/// note | 技術詳細 + +プロキシヘッダーは次のとおりです: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +それでも、**アプリケーションサーバー**は信頼できる**プロキシ**の背後にあることを知らないため、デフォルトではそれらのヘッダーを信頼しません。 + +しかし、**アプリケーションサーバー**が**プロキシ**から送信される*forwarded*ヘッダーを信頼するように設定できます。FastAPI CLIを使用している場合は、*CLI Option* `--forwarded-allow-ips` を使って、どのIPからの*forwarded*ヘッダーを信頼すべきかを指定できます。 + +たとえば、**アプリケーションサーバー**が信頼できる**プロキシ**からの通信のみを受け取っている場合、`--forwarded-allow-ips="*"` に設定して、受信するすべてのIPを信頼するようにできます。受け取るリクエストは、**プロキシ**が使用するIPからのものだけになるためです。 + +こうすることで、アプリケーションは、HTTPSを使用しているかどうか、ドメインなど、自身のパブリックURLが何であるかを把握できるようになります。 + +これは、たとえばリダイレクトを適切に処理するのに便利です。 + +/// tip | 豆知識 + +これについては、[Behind a Proxy - Enable Proxy Forwarded Headers](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} のドキュメントで詳しく学べます。 + +/// + +## まとめ { #recap } **HTTPS**を持つことは非常に重要であり、ほとんどの場合、かなり**クリティカル**です。開発者として HTTPS に関わる労力のほとんどは、これらの**概念とその仕組みを理解する**ことです。 しかし、ひとたび**開発者向けHTTPS**の基本的な情報を知れば、簡単な方法ですべてを管理するために、さまざまなツールを組み合わせて設定することができます。 -次の章では、**FastAPI** アプリケーションのために **HTTPS** をセットアップする方法について、いくつかの具体例を紹介します。🔒 +次の章のいくつかでは、**FastAPI** アプリケーションのために **HTTPS** をセットアップする方法について、いくつかの具体例を紹介します。🔒 diff --git a/docs/ja/docs/deployment/index.md b/docs/ja/docs/deployment/index.md index 897956e38..eba6eae6e 100644 --- a/docs/ja/docs/deployment/index.md +++ b/docs/ja/docs/deployment/index.md @@ -1,7 +1,23 @@ -# デプロイ +# デプロイ { #deployment } -**FastAPI** 製のアプリケーションは比較的容易にデプロイできます。 +**FastAPI** アプリケーションのデプロイは比較的簡単です。 -ユースケースや使用しているツールによっていくつかの方法に分かれます。 +## デプロイとは { #what-does-deployment-mean } -次のセクションでより詳しくそれらの方法について説明します。 +アプリケーションを**デプロイ**するとは、**ユーザーが利用できるようにする**ために必要な手順を実行することを意味します。 + +**Web API** の場合、通常は **リモートマシン** 上に配置し、優れたパフォーマンス、安定性などを提供する **サーバープログラム** と組み合わせて、**ユーザー** が中断や問題なく効率的にアプリケーションへ**アクセス**できるようにします。 + +これは **開発** 段階とは対照的です。開発では、コードを常に変更し、壊しては直し、開発サーバーを停止したり再起動したりします。 + +## デプロイ戦略 { #deployment-strategies } + +具体的なユースケースや使用するツールによって、いくつかの方法があります。 + +複数のツールを組み合わせて自分で**サーバーをデプロイ**することもできますし、作業の一部を代行してくれる **クラウドサービス** を使うこともできます。ほかにも選択肢があります。 + +たとえば、FastAPI の開発チームである私たちは、クラウドへの FastAPI アプリのデプロイを可能な限り合理化し、FastAPI を使って開発するのと同じ開発者体験を提供するために、**FastAPI Cloud** を構築しました。 + +**FastAPI** アプリケーションをデプロイする際に、おそらく念頭に置くべき主要な概念をいくつか紹介します(ただし、そのほとんどは他の種類の Web アプリケーションにも当てはまります)。 + +次のセクションでは、留意すべき点の詳細や、それを実現するためのいくつかの手法を確認します。 ✨ diff --git a/docs/ja/docs/deployment/server-workers.md b/docs/ja/docs/deployment/server-workers.md index 38ceab017..933b875d7 100644 --- a/docs/ja/docs/deployment/server-workers.md +++ b/docs/ja/docs/deployment/server-workers.md @@ -1,4 +1,4 @@ -# Server Workers - Gunicorn と Uvicorn +# Server Workers - ワーカー付きUvicorn { #server-workers-uvicorn-with-workers } 前回のデプロイメントのコンセプトを振り返ってみましょう: @@ -9,124 +9,79 @@ * メモリ * 開始前の事前ステップ -ここまでのドキュメントのチュートリアルでは、おそらくUvicornのような**サーバープログラム**を**単一のプロセス**で実行しています。 +ここまでのドキュメントのチュートリアルでは、おそらく `fastapi` コマンドなど(Uvicornを実行するもの)を使って、**単一のプロセス**として動作する**サーバープログラム**を実行してきたはずです。 アプリケーションをデプロイする際には、**複数のコア**を利用し、そしてより多くのリクエストを処理できるようにするために、プロセスの**レプリケーション**を持つことを望むでしょう。 前のチャプターである[デプロイメントのコンセプト](concepts.md){.internal-link target=_blank}にて見てきたように、有効な戦略がいくつかあります。 -ここでは**Gunicorn**が**Uvicornのワーカー・プロセス**を管理する場合の使い方について紹介していきます。 +ここでは、`fastapi` コマンド、または `uvicorn` コマンドを直接使って、**ワーカープロセス**付きの **Uvicorn** を使う方法を紹介します。 -/// info +/// info | 情報 - -DockerやKubernetesなどのコンテナを使用している場合は、次の章で詳しく説明します: [コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank} +DockerやKubernetesなどのコンテナを使用している場合は、次の章で詳しく説明します: [コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}。 -特に**Kubernetes**上で実行する場合は、おそらく**Gunicornを使用せず**、**コンテナごとに単一のUvicornプロセス**を実行することになりますが、それについてはこの章の後半で説明します。 +特に**Kubernetes**上で実行する場合は、おそらくワーカーは使わず、代わりに**コンテナごとに単一のUvicornプロセス**を実行したいはずですが、それについてはその章の後半で説明します。 /// -## GunicornによるUvicornのワーカー・プロセスの管理 +## 複数ワーカー { #multiple-workers } -**Gunicorn**は**WSGI標準**のアプリケーションサーバーです。このことは、GunicornはFlaskやDjangoのようなアプリケーションにサービスを提供できることを意味します。Gunicornそれ自体は**FastAPI**と互換性がないですが、というのもFastAPIは最新の**ASGI 標準**を使用しているためです。 +`--workers` コマンドラインオプションで複数のワーカーを起動できます。 -しかし、Gunicornは**プロセスマネージャー**として動作し、ユーザーが特定の**ワーカー・プロセスクラス**を使用するように指示することができます。するとGunicornはそのクラスを使い1つ以上の**ワーカー・プロセス**を開始します。 +//// tab | `fastapi` -そして**Uvicorn**には**Gunicorn互換のワーカークラス**があります。 - -この組み合わせで、Gunicornは**プロセスマネージャー**として動作し、**ポート**と**IP**をリッスンします。そして、**Uvicornクラス**を実行しているワーカー・プロセスに通信を**転送**します。 - -そして、Gunicorn互換の**Uvicornワーカー**クラスが、FastAPIが使えるように、Gunicornから送られてきたデータをASGI標準に変換する役割を担います。 - -## GunicornとUvicornをインストールする +`fastapi` コマンドを使う場合:
```console -$ pip install "uvicorn[standard]" gunicorn +$ fastapi run --workers 4 main.py ----> 100% + FastAPI Starting production 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://0.0.0.0:8000 + server Documentation at http://0.0.0.0:8000/docs + + Logs: + + INFO Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to + quit) + INFO Started parent process [27365] + INFO Started server process [27368] + INFO Started server process [27369] + INFO Started server process [27370] + INFO Started server process [27367] + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Application startup complete. + INFO Application startup complete. + INFO Application startup complete. + INFO Application startup complete. ```
-これによりUvicornと(高性能を得るための)標準(`standard`)の追加パッケージとGunicornの両方がインストールされます。 +//// -## UvicornのワーカーとともにGunicornを実行する +//// tab | `uvicorn` -Gunicornを以下のように起動させることができます: - -
- -```console -$ gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 - -[19499] [INFO] Starting gunicorn 20.1.0 -[19499] [INFO] Listening at: http://0.0.0.0:80 (19499) -[19499] [INFO] Using worker: uvicorn.workers.UvicornWorker -[19511] [INFO] Booting worker with pid: 19511 -[19513] [INFO] Booting worker with pid: 19513 -[19514] [INFO] Booting worker with pid: 19514 -[19515] [INFO] Booting worker with pid: 19515 -[19511] [INFO] Started server process [19511] -[19511] [INFO] Waiting for application startup. -[19511] [INFO] Application startup complete. -[19513] [INFO] Started server process [19513] -[19513] [INFO] Waiting for application startup. -[19513] [INFO] Application startup complete. -[19514] [INFO] Started server process [19514] -[19514] [INFO] Waiting for application startup. -[19514] [INFO] Application startup complete. -[19515] [INFO] Started server process [19515] -[19515] [INFO] Waiting for application startup. -[19515] [INFO] Application startup complete. -``` - -
- -それぞれのオプションの意味を見てみましょう: - -* `main:app`: `main`は"`main`"という名前のPythonモジュール、つまりファイル`main.py`を意味します。そして `app` は **FastAPI** アプリケーションの変数名です。 - * main:app`はPythonの`import`文と同じようなものだと想像できます: - - ```Python - from main import app - ``` - - * つまり、`main:app`のコロンは、`from main import app`のPythonの`import`の部分と同じになります。 - -* `--workers`: 使用するワーカー・プロセスの数で、それぞれがUvicornのワーカーを実行します。 - -* `--worker-class`: ワーカー・プロセスで使用するGunicorn互換のワーカークラスです。 - * ここではGunicornがインポートして使用できるクラスを渡します: - - ```Python - import uvicorn.workers.UvicornWorker - ``` - -* `--bind`: GunicornにリッスンするIPとポートを伝えます。コロン(`:`)でIPとポートを区切ります。 - * Uvicornを直接実行している場合は、`--bind 0.0.0.0:80` (Gunicornのオプション)の代わりに、`--host 0.0.0.0`と `--port 80`を使います。 - -出力では、各プロセスの**PID**(プロセスID)が表示されているのがわかります(単なる数字です)。 - -以下の通りです: - -* Gunicornの**プロセス・マネージャー**はPID `19499`(あなたの場合は違う番号でしょう)で始まります。 -* 次に、`Listening at: http://0.0.0.0:80`を開始します。 -* それから `uvicorn.workers.UvicornWorker` でワーカークラスを使用することを検出します。 -* そして、**4つのワーカー**を起動します。それぞれのワーカーのPIDは、`19511`、`19513`、`19514`、`19515`です。 - -Gunicornはまた、ワーカーの数を維持するために必要であれば、**ダウンしたプロセス**を管理し、**新しいプロセスを**再起動**させます。そのため、上記のリストにある**再起動**の概念に一部役立ちます。 - -しかしながら、必要であればGunicornを**再起動**させ、**起動時に実行**させるなど、外部のコンポーネントを持たせることも必要かもしれません。 - -## Uvicornとワーカー - -Uvicornには複数の**ワーカー・プロセス**を起動し実行するオプションもあります。 - -とはいうものの、今のところUvicornのワーカー・プロセスを扱う機能はGunicornよりも制限されています。そのため、このレベル(Pythonレベル)でプロセスマネージャーを持ちたいのであれば、Gunicornをプロセスマネージャーとして使ってみた方が賢明かもしれないです。 - -どんな場合であれ、以下のように実行します: +`uvicorn` コマンドを直接使いたい場合:
@@ -150,36 +105,35 @@ $ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4
-ここで唯一の新しいオプションは `--workers` で、Uvicornに4つのワーカー・プロセスを起動するように指示しています。 +//// -各プロセスの **PID** が表示され、親プロセスの `27365` (これは **プロセスマネージャ**) と、各ワーカー・プロセスの **PID** が表示されます: `27368`、`27369`、`27370`、`27367`になります。 +ここで唯一の新しいオプションは `--workers` で、Uvicornに4つのワーカープロセスを起動するように指示しています。 -## デプロイメントのコンセプト +各プロセスの **PID** も表示されていて、親プロセス(これは**プロセスマネージャー**)が `27365`、各ワーカープロセスがそれぞれ `27368`、`27369`、`27370`、`27367` です。 -ここでは、アプリケーションの実行を**並列化**し、CPUの**マルチコア**を活用し、**より多くのリクエスト**に対応できるようにするために、**Gunicorn**(またはUvicorn)を使用して**Uvicornワーカー・プロセス**を管理する方法を見ていきました。 +## デプロイメントのコンセプト { #deployment-concepts } -上記のデプロイのコンセプトのリストから、ワーカーを使うことは主に**レプリケーション**の部分と、**再起動**を少し助けてくれます: +ここでは、複数の **ワーカー** を使ってアプリケーションの実行を**並列化**し、CPUの**複数コア**を活用して、**より多くのリクエスト**を処理できるようにする方法を見てきました。 -* セキュリティ - HTTPS -* 起動時の実行 -* 再起動 +上のデプロイメントのコンセプトのリストから、ワーカーを使うことは主に**レプリケーション**の部分と、**再起動**を少し助けてくれますが、それ以外については引き続き対処が必要です: + +* **セキュリティ - HTTPS** +* **起動時の実行** +* ***再起動*** * レプリケーション(実行中のプロセス数) -* メモリー -* 開始前の事前のステップ +* **メモリ** +* **開始前の事前ステップ** +## コンテナとDocker { #containers-and-docker } -## コンテナとDocker - -次章の[コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}では、その他の**デプロイのコンセプト**を扱うために実施するであろう戦略をいくつか紹介します。 +次章の[コンテナ内のFastAPI - Docker](docker.md){.internal-link target=_blank}では、その他の**デプロイメントのコンセプト**を扱うために使える戦略をいくつか説明します。 -また、**GunicornとUvicornワーカー**を含む**公式Dockerイメージ**と、簡単なケースに役立ついくつかのデフォルト設定も紹介します。 +単一のUvicornプロセスを実行するために、**ゼロから独自のイメージを構築する**方法も紹介します。これは簡単なプロセスで、**Kubernetes**のような分散コンテナ管理システムを使う場合に、おそらくやりたいことでしょう。 -また、(Gunicornを使わずに)Uvicornプロセスを1つだけ実行するために、**ゼロから独自のイメージを**構築する方法も紹介します。これは簡単なプロセスで、おそらく**Kubernetes**のような分散コンテナ管理システムを使うときにやりたいことでしょう。 +## まとめ { #recap } -## まとめ +`fastapi` または `uvicorn` コマンドで `--workers` CLIオプションを使うことで、**マルチコアCPU**を活用し、**複数のプロセスを並列実行**できるように複数のワーカープロセスを利用できます。 -Uvicornワーカーを使ったプロセスマネージャとして**Gunicorn**(またはUvicorn)を使えば、**マルチコアCPU**を活用して**複数のプロセスを並列実行**できます。 +他のデプロイメントのコンセプトを自分で対応しながら、**独自のデプロイシステム**を構築している場合にも、これらのツールやアイデアを使えます。 -これらのツールやアイデアは、**あなた自身のデプロイシステム**をセットアップしながら、他のデプロイコンセプトを自分で行う場合にも使えます。 - -次の章では、コンテナ(DockerやKubernetesなど)を使った**FastAPI**について学んでいきましょう。これらのツールには、他の**デプロイのコンセプト**も解決する簡単な方法があることがわかるでしょう。✨ +次の章で、コンテナ(例:DockerやKubernetes)を使った **FastAPI** について学びましょう。これらのツールにも、他の**デプロイメントのコンセプト**を解決する簡単な方法があることがわかります。✨ diff --git a/docs/ja/docs/deployment/versions.md b/docs/ja/docs/deployment/versions.md index 7575fc4f7..7980b8be2 100644 --- a/docs/ja/docs/deployment/versions.md +++ b/docs/ja/docs/deployment/versions.md @@ -1,93 +1,93 @@ -# FastAPIのバージョンについて +# FastAPIのバージョンについて { #about-fastapi-versions } -**FastAPI** は既に多くのアプリケーションやシステムに本番環境で使われています。また、100%のテストカバレッジを維持しています。しかし、活発な開発が続いています。 +**FastAPI** はすでに多くのアプリケーションやシステムで本番環境にて使われています。また、テストカバレッジは 100% に維持されています。しかし、開発は依然として急速に進んでいます。 -高頻度で新機能が追加され、定期的にバグが修正され、実装は継続的に改善されています。 +新機能が高頻度で追加され、定期的にバグが修正され、コードは継続的に改善されています。 これが現在のバージョンがいまだに `0.x.x` な理由であり、それぞれのバージョンは破壊的な変更がなされる可能性があります。これは、セマンティック バージョニングの規則に則っています。 -**FastAPI** を使用すると本番用アプリケーションをすぐに作成できますが (すでに何度も経験しているかもしれませんが)、残りのコードが正しく動作するバージョンなのか確認しなければいけません。 +**FastAPI** を使用すると本番用アプリケーションを今すぐ作成できます(そして、おそらくあなたはしばらく前からそうしているはずです)。必要なのは、残りのコードと正しく動作するバージョンを使用していることを確認することだけです。 -## `fastapi` のバージョンを固定 +## `fastapi` のバージョンを固定 { #pin-your-fastapi-version } -最初にすべきことは、アプリケーションが正しく動作する **FastAPI** のバージョンを固定することです。 +最初にすべきことは、使用している **FastAPI** のバージョンを、アプリケーションで正しく動作することが分かっている特定の最新バージョンに「固定(pin)」することです。 -例えば、バージョン `0.45.0` を使っているとしましょう。 +例えば、アプリでバージョン `0.112.0` を使っているとしましょう。 -`requirements.txt` を使っているなら、以下の様にバージョンを指定できます: +`requirements.txt` ファイルを使う場合は、以下のようにバージョンを指定できます: ```txt -fastapi==0.45.0 +fastapi[standard]==0.112.0 ``` -これは、厳密にバージョン `0.45.0` だけを使うことを意味します。 +これは、厳密にバージョン `0.112.0` だけを使うことを意味します。 -または、以下の様に固定することもできます: +または、以下のように固定することもできます: + +```txt +fastapi[standard]>=0.112.0,<0.113.0 +``` + +これは `0.112.0` 以上、`0.113.0` 未満のバージョンを使うことを意味します。例えば、バージョン `0.112.2` は使用可能です。 + +`uv`、Poetry、Pipenv など、他のインストール管理ツールを使用している場合でも、いずれもパッケージの特定バージョンを定義する方法があります。 + +## 利用可能なバージョン { #available-versions } + +利用可能なバージョン(例: 現在の最新が何かを確認するため)は、[Release Notes](../release-notes.md){.internal-link target=_blank} で確認できます。 + +## バージョンについて { #about-versions } + +セマンティック バージョニングの規約に従って、`1.0.0` 未満のバージョンは破壊的な変更が加わる可能性があります。 + +FastAPI では「PATCH」バージョンの変更はバグ修正と非破壊的な変更に使う、という規約にも従っています。 + +/// tip | 豆知識 + +「PATCH」は最後の数字です。例えば、`0.2.3` では PATCH バージョンは `3` です。 + +/// + +従って、以下のようなバージョンの固定ができるはずです: ```txt fastapi>=0.45.0,<0.46.0 ``` -これは `0.45.0` 以上、`0.46.0` 未満のバージョンを使うことを意味します。例えば、バージョン `0.45.2` は使用可能です。 - -PoetryやPipenvなど、他のインストール管理ツールを使用している場合でも、それぞれパッケージのバージョンを指定する機能があります。 - -## 利用可能なバージョン - -[Release Notes](../release-notes.md){.internal-link target=_blank}で利用可能なバージョンが確認できます (現在の最新版の確認などのため)。 - -## バージョンについて - -セマンティック バージョニングの規約に従って、`1.0.0` 未満の全てのバージョンは破壊的な変更が加わる可能性があります。 - -FastAPIでは「パッチ」バージョンはバグ修正と非破壊的な変更に留めるという規約に従っています。 +破壊的な変更と新機能は「MINOR」バージョンで追加されます。 /// tip | 豆知識 -「パッチ」は最後の数字を指します。例えば、`0.2.3` ではパッチバージョンは `3` です。 +「MINOR」は真ん中の数字です。例えば、`0.2.3` では MINOR バージョンは `2` です。 /// -従って、以下の様なバージョンの固定が望ましいです: +## FastAPIのバージョンのアップグレード { #upgrading-the-fastapi-versions } -```txt -fastapi>=0.45.0,<0.46.0 -``` +アプリケーションにテストを追加すべきです。 -破壊的な変更と新機能実装は「マイナー」バージョンで加えられます。 +**FastAPI** では非常に簡単に実現できます(Starlette のおかげです)。ドキュメントを確認して下さい: [テスト](../tutorial/testing.md){.internal-link target=_blank} -/// tip | 豆知識 +テストを追加したら、**FastAPI** のバージョンをより新しいものにアップグレードし、テストを実行することで全てのコードが正しく動作するか確認できます。 -「マイナー」は真ん中の数字です。例えば、`0.2.3` ではマイナーバージョンは `2` です。 +全てが動作する、または必要な変更を行った後に全てのテストが通るなら、その新しいバージョンに `fastapi` を固定できます。 -/// +## Starletteについて { #about-starlette } -## FastAPIのバージョンのアップグレード +`starlette` のバージョンは固定すべきではありません。 -アプリケーションにテストを加えるべきです。 +**FastAPI** のバージョンが異なれば、Starlette の特定のより新しいバージョンが使われます。 -**FastAPI** では非常に簡単に実現できます (Starletteのおかげで)。ドキュメントを確認して下さい: [テスト](../tutorial/testing.md){.internal-link target=_blank} +そのため、正しい Starlette バージョンを **FastAPI** に任せればよいです。 -テストを加えた後で、**FastAPI** のバージョンをより最新のものにアップグレードし、テストを実行することで全てのコードが正常に動作するか確認できます。 +## Pydanticについて { #about-pydantic } -全てが動作するか、修正を行った上で全てのテストを通過した場合、使用している`fastapi` のバージョンをより最新のバージョンに固定できます。 +Pydantic は自身のテストに **FastAPI** のテストも含んでいるため、Pydantic の新しいバージョン(`1.0.0` より上)は常に FastAPI と互換性があります。 -## Starletteについて - -`Starlette` のバージョンは固定すべきではありません。 - -**FastAPI** は、バージョン毎にStarletteのより新しいバージョンを使用します。 - -よって、最適なStarletteのバージョン選択を**FastAPI** に任せることができます。 - -## Pydanticについて - -Pydanticは自身のテストだけでなく**FastAPI** のためのテストを含んでいます。なので、Pydanticの新たなバージョン ( `1.0.0` 以降) は全てFastAPIと整合性があります。 - -Pydanticのバージョンを、動作が保証できる`1.0.0`以降のいずれかのバージョンから`2.0.0` 未満の間に固定できます。 +Pydantic は、自分にとって動作する `1.0.0` より上の任意のバージョンに固定できます。 例えば: ```txt -pydantic>=1.2.0,<2.0.0 +pydantic>=2.7.0,<3.0.0 ``` diff --git a/docs/ja/docs/environment-variables.md b/docs/ja/docs/environment-variables.md index 507af3a0c..45dbfc71f 100644 --- a/docs/ja/docs/environment-variables.md +++ b/docs/ja/docs/environment-variables.md @@ -1,18 +1,18 @@ -# 環境変数 +# 環境変数 { #environment-variables } -/// tip +/// tip | 豆知識 もし、「環境変数」とは何か、それをどう使うかを既に知っている場合は、このセクションをスキップして構いません。 /// -環境変数(**env var**とも呼ばれる)はPythonコードの**外側**、つまり**OS**に存在する変数で、Pythonから読み取ることができます。(他のプログラムでも同様に読み取れます。) +環境変数(「**env var**」とも呼ばれます)とは、Pythonコードの**外側**、つまり**オペレーティングシステム**に存在する変数で、Pythonコード(または他のプログラム)から読み取れます。 -環境変数は、アプリケーションの**設定**の管理や、Pythonの**インストール**などに役立ちます。 +環境変数は、アプリケーションの**設定**の扱い、Pythonの**インストール**の一部などで役立ちます。 -## 環境変数の作成と使用 +## 環境変数の作成と使用 { #create-and-use-env-vars } -環境変数は**シェル(ターミナル)**内で**作成**して使用でき、それらにPythonは不要です。 +環境変数は、Pythonを必要とせず、**シェル(ターミナル)**で**作成**して使用できます。 //// tab | Linux, macOS, Windows Bash @@ -36,7 +36,6 @@ Hello Wade Wilson
- ```console // Create an env var MY_NAME $ $Env:MY_NAME = "Wade Wilson" @@ -51,9 +50,9 @@ Hello Wade Wilson //// -## Pythonで環境変数を読み取る +## Pythonで環境変数を読み取る { #read-env-vars-in-python } -環境変数をPythonの**外側**、ターミナル(や他の方法)で作成し、**Python内で読み取る**こともできます。 +環境変数はPythonの**外側**(ターミナル、またはその他の方法)で作成し、その後に**Pythonで読み取る**こともできます。 例えば、以下のような`main.py`ファイルを用意します: @@ -64,11 +63,11 @@ name = os.getenv("MY_NAME", "World") print(f"Hello {name} from Python") ``` -/// tip +/// tip | 豆知識 -`os.getenv()` の第2引数は、デフォルトで返される値を指定します。 +`os.getenv()` の第2引数は、デフォルトで返される値です。 -この引数を省略するとデフォルト値として`None`が返されますが、ここではデフォルト値として`"World"`を指定しています。 +指定しない場合、デフォルトは`None`ですが、ここでは使用するデフォルト値として`"World"`を指定しています。 /// @@ -128,11 +127,11 @@ Hello Wade Wilson from Python //// -環境変数はコードの外側で設定し、内側から読み取ることができるので、他のファイルと一緒に(`git`に)保存する必要がありません。そのため、環境変数をコンフィグレーションや**設定**に使用することが一般的です。 +環境変数はコードの外側で設定でき、コードから読み取れ、他のファイルと一緒に(`git`に)保存(コミット)する必要がないため、設定や**settings**に使うのが一般的です。 -また、**特定のプログラムの呼び出し**のための環境変数を、そのプログラムのみ、その実行中に限定して利用できるよう作成できます。 +また、**特定のプログラムの呼び出し**のためだけに、そのプログラムでのみ、実行中の間だけ利用できる環境変数を作成することもできます。 -そのためには、プログラム起動コマンドと同じコマンドライン上の、起動コマンド直前で環境変数を作成してください。 +そのためには、同じ行で、プログラム自体の直前に作成してください。
@@ -152,25 +151,25 @@ Hello World from Python
-/// tip +/// tip | 豆知識 詳しくは The Twelve-Factor App: Config を参照してください。 /// -## 型とバリデーション +## 型とバリデーション { #types-and-validation } -環境変数は**テキスト文字列**のみを扱うことができます。これは、環境変数がPython外部に存在し、他のプログラムやシステム全体(Linux、Windows、macOS間の互換性を含む)と連携する必要があるためです。 +これらの環境変数が扱えるのは**テキスト文字列**のみです。環境変数はPythonの外部にあり、他のプログラムやシステム全体(Linux、Windows、macOSなど異なるオペレーティングシステム間も)との互換性が必要になるためです。 -つまり、Pythonが環境変数から読み取る**あらゆる値**は **`str`型となり**、他の型への変換やバリデーションはコード内で行う必要があります。 +つまり、環境変数からPythonで読み取る**あらゆる値**は **`str`になり**、他の型への変換やバリデーションはコード内で行う必要があります。 -環境変数を使用して**アプリケーション設定**を管理する方法については、[高度なユーザーガイド - Settings and Environment Variables](./advanced/settings.md){.internal-link target=_blank}で詳しく学べます。 +環境変数を使って**アプリケーション設定**を扱う方法については、[高度なユーザーガイド - Settings and Environment Variables](./advanced/settings.md){.internal-link target=_blank} で詳しく学べます。 -## `PATH`環境変数 +## `PATH`環境変数 { #path-environment-variable } -**`PATH`**という**特別な**環境変数があります。この環境変数は、OS(Linux、macOS、Windows)が実行するプログラムを発見するために使用されます。 +**`PATH`**という**特別な**環境変数があります。これはオペレーティングシステム(Linux、macOS、Windows)が実行するプログラムを見つけるために使用されます。 -`PATH`変数は、複数のディレクトリのパスから成る長い文字列です。このパスはLinuxやMacOSの場合は`:`で、Windowsの場合は`;`で区切られています。 +変数`PATH`の値は長い文字列で、LinuxとmacOSではコロン`:`、Windowsではセミコロン`;`で区切られたディレクトリで構成されます。 例えば、`PATH`環境変数は次のような文字列かもしれません: @@ -180,7 +179,7 @@ Hello World from Python /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin ``` -これは、OSはプログラムを見つけるために以下のディレクトリを探す、ということを意味します: +これは、システムが次のディレクトリでプログラムを探すことを意味します: * `/usr/local/bin` * `/usr/bin` @@ -196,7 +195,7 @@ Hello World from Python C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 ``` -これは、OSはプログラムを見つけるために以下のディレクトリを探す、ということを意味します: +これは、システムが次のディレクトリでプログラムを探すことを意味します: * `C:\Program Files\Python312\Scripts` * `C:\Program Files\Python312` @@ -204,63 +203,61 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3 //// -ターミナル上で**コマンド**を入力すると、 OSはそのプログラムを見つけるために、`PATH`環境変数のリストに記載された**それぞれのディレクトリを探し**ます。 +ターミナル上で**コマンド**を入力すると、オペレーティングシステムは`PATH`環境変数に記載された**それぞれのディレクトリ**の中からプログラムを**探し**ます。 -例えば、ターミナル上で`python`を入力すると、OSは`python`によって呼ばれるプログラムを見つけるために、そのリストの**先頭のディレクトリ**を最初に探します。 +例えば、ターミナルで`python`と入力すると、オペレーティングシステムはそのリストの**最初のディレクトリ**で`python`というプログラムを探します。 -OSは、もしそのプログラムをそこで発見すれば**実行し**ますが、そうでなければリストの**他のディレクトリ**を探していきます。 +見つかればそれを**使用**します。見つからなければ、**他のディレクトリ**を探し続けます。 -### PythonのインストールとPATH環境変数の更新 +### Pythonのインストールと`PATH`の更新 { #installing-python-and-updating-the-path } -Pythonのインストール時に`PATH`環境変数を更新したいか聞かれるかもしれません。 +Pythonのインストール時に、`PATH`環境変数を更新するかどうかを尋ねられるかもしれません。 -/// tab | Linux, macOS +//// tab | Linux, macOS -Pythonをインストールして、そのプログラムが`/opt/custompython/bin`というディレクトリに配置されたとします。 +Pythonをインストールして、その結果`/opt/custompython/bin`というディレクトリに配置されたとします。 -もし、`PATH`環境変数を更新するように答えると、`PATH`環境変数に`/opt/custompython/bin`が追加されます。 +`PATH`環境変数を更新することに同意すると、インストーラーは`PATH`環境変数に`/opt/custompython/bin`を追加します。 -`PATH`環境変数は以下のように更新されるでしょう: +例えば次のようになります: -``` plaintext +```plaintext /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin ``` -このようにして、ターミナルで`python`と入力したときに、OSは`/opt/custompython/bin`(リストの末尾のディレクトリ)にあるPythonプログラムを見つけ、使用します。 +このようにして、ターミナルで`python`と入力すると、システムは`/opt/custompython/bin`(最後のディレクトリ)にあるPythonプログラムを見つけ、それを使用します。 -/// +//// -/// tab | Windows +//// tab | Windows -Pythonをインストールして、そのプログラムが`C:\opt\custompython\bin`というディレクトリに配置されたとします。 +Pythonをインストールして、その結果`C:\opt\custompython\bin`というディレクトリに配置されたとします。 -もし、`PATH`環境変数を更新するように答えると、`PATH`環境変数に`C:\opt\custompython\bin`が追加されます。 - -`PATH`環境変数は以下のように更新されるでしょう: +`PATH`環境変数を更新することに同意すると、インストーラーは`PATH`環境変数に`C:\opt\custompython\bin`を追加します。 ```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プログラムを見つけ、使用します。 +このようにして、ターミナルで`python`と入力すると、システムは`C:\opt\custompython\bin`(最後のディレクトリ)にあるPythonプログラムを見つけ、それを使用します。 -/// +//// -つまり、ターミナルで以下のコマンドを入力すると: +つまり、ターミナルで次のように入力すると:
-``` console +```console $ python ```
-/// tab | Linux, macOS +//// tab | Linux, macOS -OSは`/opt/custompython/bin`にある`python`プログラムを**見つけ**て実行します。 +システムは`/opt/custompython/bin`にある`python`プログラムを**見つけ**て実行します。 -これは、次のコマンドを入力した場合とほとんど同等です: +これは、次のように入力するのとおおむね同等です:
@@ -270,13 +267,13 @@ $ /opt/custompython/bin/python
-/// +//// -/// tab | Windows +//// tab | Windows -OSは`C:\opt\custompython\bin\python`にある`python`プログラムを**見つけ**て実行します。 +システムは`C:\opt\custompython\bin\python`にある`python`プログラムを**見つけ**て実行します。 -これは、次のコマンドを入力した場合とほとんど同等です: +これは、次のように入力するのとおおむね同等です:
@@ -286,16 +283,16 @@ $ C:\opt\custompython\bin\python
-/// +//// -この情報は、[Virtual Environments](virtual-environments.md) について学ぶ際にも役立ちます。 +この情報は、[Virtual Environments](virtual-environments.md){.internal-link target=_blank} について学ぶ際にも役立ちます。 -## まとめ +## まとめ { #conclusion } これで、**環境変数**とは何か、Pythonでどのように使用するかについて、基本的な理解が得られたはずです。 -環境変数についての詳細は、Wikipedia: Environment Variable を参照してください。 +環境変数についての詳細は、Wikipedia for Environment Variable も参照してください。 -環境変数の用途や適用方法が最初は直感的ではないかもしれませんが、開発中のさまざまなシナリオで繰り返し登場します。そのため、基本を知っておくことが重要です。 +多くの場合、環境変数がどのように役立ち、すぐに適用できるのかはあまり明確ではありません。しかし、開発中のさまざまなシナリオで何度も登場するため、知っておくとよいでしょう。 -たとえば、この情報は次のセクションで扱う[Virtual Environments](virtual-environments.md)にも関連します。 +例えば、次のセクションの[Virtual Environments](virtual-environments.md)でこの情報が必要になります。 diff --git a/docs/ja/docs/how-to/conditional-openapi.md b/docs/ja/docs/how-to/conditional-openapi.md index bfaa9e6d7..9478f5c03 100644 --- a/docs/ja/docs/how-to/conditional-openapi.md +++ b/docs/ja/docs/how-to/conditional-openapi.md @@ -1,8 +1,8 @@ -# 条件付き OpenAPI +# 条件付き OpenAPI { #conditional-openapi } 必要であれば、設定と環境変数を利用して、環境に応じて条件付きでOpenAPIを構成することが可能です。また、完全にOpenAPIを無効にすることもできます。 -## セキュリティとAPI、およびドキュメントについて +## セキュリティとAPI、およびドキュメントについて { #about-security-apis-and-docs } 本番環境においてドキュメントのUIを非表示にすることによって、APIを保護しようと *すべきではありません*。 @@ -17,19 +17,19 @@ * リクエストボディとレスポンスのためのPydanticモデルの定義を見直す。 * 依存関係に基づきすべての必要なパーミッションとロールを設定する。 * パスワードを絶対に平文で保存しない。パスワードハッシュのみを保存する。 -* PasslibやJWTトークンに代表される、よく知られた暗号化ツールを使って実装する。 +* pwdlibやJWTトークンに代表される、よく知られた暗号化ツールを使って実装する。 * そして必要なところでは、もっと細かいパーミッション制御をOAuth2スコープを使って行う。 -* など +* ...など それでも、例えば本番環境のような特定の環境のみで、あるいは環境変数の設定によってAPIドキュメントをどうしても無効にしたいという、非常に特殊なユースケースがあるかもしれません。 -## 設定と環境変数による条件付き OpenAPI +## 設定と環境変数による条件付き OpenAPI { #conditional-openapi-from-settings-and-env-vars } 生成するOpenAPIとドキュメントUIの構成は、共通のPydanticの設定を使用して簡単に切り替えられます。 例えば、 -{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *} +{* ../../docs_src/conditional_openapi/tutorial001_py39.py hl[6,11] *} ここでは `openapi_url` の設定を、デフォルトの `"/openapi.json"` のまま宣言しています。 diff --git a/docs/ja/docs/index.md b/docs/ja/docs/index.md index 8dee1ee03..67e01ed53 100644 --- a/docs/ja/docs/index.md +++ b/docs/ja/docs/index.md @@ -1,14 +1,14 @@ -# FastAPI +# FastAPI { #fastapi }

- FastAPI + FastAPI

- FastAPI framework, high performance, easy to learn, fast to code, ready for production + FastAPI フレームワーク、高パフォーマンス、学びやすい、素早くコーディングできる、本番運用に対応

@@ -27,129 +27,138 @@ --- -**ドキュメント**: https://fastapi.tiangolo.com +**ドキュメント**: https://fastapi.tiangolo.com **ソースコード**: https://github.com/fastapi/fastapi --- -FastAPI は、Pythonの標準である型ヒントに基づいてPython 以降でAPI を構築するための、モダンで、高速(高パフォーマンス)な、Web フレームワークです。 +FastAPI は、Python の標準である型ヒントに基づいて Python で API を構築するための、モダンで、高速(高パフォーマンス)な Web フレームワークです。 主な特徴: -- **高速**: **NodeJS** や **Go** 並みのとても高いパフォーマンス (Starlette と Pydantic のおかげです)。 [最も高速な Python フレームワークの一つです](#_10). +* **高速**: **NodeJS** や **Go** 並みのとても高いパフォーマンス(Starlette と Pydantic のおかげです)。 [利用可能な最も高速な Python フレームワークの一つです](#performance)。 +* **高速なコーディング**: 開発速度を約 200%〜300% 向上させます。* +* **少ないバグ**: 開発者起因のヒューマンエラーを約 40% 削減します。* +* **直感的**: 素晴らしいエディタサポート。あらゆる場所で 補完 が使えます。デバッグ時間を削減します。 +* **簡単**: 簡単に利用・習得できるようにデザインされています。ドキュメントを読む時間を削減します。 +* **短い**: コードの重複を最小限にします。各パラメータ宣言から複数の機能を得られます。バグも減ります。 +* **堅牢性**: 自動対話型ドキュメントにより、本番環境向けのコードが得られます。 +* **Standards-based**: API のオープンスタンダードに基づいており(そして完全に互換性があります)、OpenAPI(以前は Swagger として知られていました)や JSON Schema をサポートします。 -- **高速なコーディング**: 開発速度を約 200%~300%向上させます。 \* -- **少ないバグ**: 開発者起因のヒューマンエラーを約 40%削減します。 \* -- **直感的**: 素晴らしいエディタのサポートや オートコンプリート。 デバッグ時間を削減します。 -- **簡単**: 簡単に利用、習得できるようにデザインされています。ドキュメントを読む時間を削減します。 -- **短い**: コードの重複を最小限にしています。各パラメータからの複数の機能。少ないバグ。 -- **堅牢性**: 自動対話ドキュメントを使用して、本番環境で使用できるコードを取得します。 -- **Standards-based**: API のオープンスタンダードに基づいており、完全に互換性があります: OpenAPI (以前は Swagger として知られていました) や JSON スキーマ. +* 本番アプリケーションを構築している社内開発チームのテストに基づく見積もりです。 -\* 本番アプリケーションを構築している開発チームのテストによる見積もり。 - -## Sponsors +## Sponsors { #sponsors } -{% if sponsors %} +### Keystone Sponsor { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Gold and Silver Sponsors { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} -Other sponsors +その他のスポンサー -## 評価 +## 評価 { #opinions } -"_[...] 最近 **FastAPI** を使っています。 [...] 実際に私のチームの全ての **Microsoft の機械学習サービス** で使用する予定です。 そのうちのいくつかのコアな**Windows**製品と**Office**製品に統合されつつあります。_" +"_[...] 最近 **FastAPI** を使っています。 [...] 実際に私のチームの全ての **Microsoft の機械学習サービス** で使用する予定です。 そのうちのいくつかのコアな **Windows** 製品と **Office** 製品に統合されつつあります。_"

Kabir Khan - Microsoft (ref)
--- -"_FastAPIライブラリを採用し、クエリで**予測値**を取得できる**REST**サーバを構築しました。 [for Ludwig]_" +"_FastAPIライブラリを採用し、クエリで **予測値** を取得できる **REST** サーバを構築しました。 [for Ludwig]_"
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
--- -"_**Netflix** は、**危機管理**オーケストレーションフレームワーク、**Dispatch**のオープンソースリリースを発表できることをうれしく思います。 [built with **FastAPI**]_" +"_**Netflix** は、**危機管理**オーケストレーションフレームワーク、**Dispatch** のオープンソースリリースを発表できることをうれしく思います。 [built with **FastAPI**]_"
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
--- -"_私は**FastAPI**にワクワクしています。 めちゃくちゃ楽しいです!_" +"_私は **FastAPI** にワクワクしています。 めちゃくちゃ楽しいです!_"
Brian Okken - Python Bytes podcast host (ref)
--- -"_正直、超堅実で洗練されているように見えます。いろんな意味で、それは私がハグしたかったものです。_" +"_正直、あなたが作ったものは超堅実で洗練されているように見えます。いろんな意味で、それは私が **Hug** にそうなってほしかったものです。誰かがそれを作るのを見るのは本当に刺激的です。_"
Timothy Crosley - Hug creator (ref)
--- -"_REST API を構築するための**モダンなフレームワーク**を学びたい方は、**FastAPI** [...] をチェックしてみてください。 [...] 高速で, 使用、習得が簡単です。[...]_" +"_REST API を構築するための **モダンなフレームワーク** を学びたい方は、**FastAPI** [...] をチェックしてみてください。 [...] 高速で、使用・習得が簡単です [...]_" -"_私たちの**API**は**FastAPI**に切り替えました。[...] きっと気に入ると思います。 [...]_" +"_私たちの **API** は **FastAPI** に切り替えました [...] きっと気に入ると思います [...]_"
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
--- -## **Typer**, the FastAPI of CLIs +"_本番運用の Python API を構築したい方には、**FastAPI** を強くおすすめします。**美しく設計**されており、**使いやすく**、**高いスケーラビリティ**があります。私たちの API ファースト開発戦略の **主要コンポーネント** となり、Virtual TAC Engineer などの多くの自動化やサービスを推進しています。_" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## FastAPI ミニドキュメンタリー { #fastapi-mini-documentary } + +2025 年末に公開された FastAPI ミニドキュメンタリーがあります。オンラインで視聴できます: + +FastAPI Mini Documentary + +## **Typer**、CLI 版 FastAPI { #typer-the-fastapi-of-clis } -もし Web API の代わりにターミナルで使用するCLIアプリを構築する場合は、**Typer**を確認してください。 +Web API の代わりにターミナルで使用する CLI アプリを構築する場合は、**Typer** を確認してください。 -**Typer**は FastAPI の弟分です。そして、**CLI 版 の FastAPI**を意味しています。 +**Typer** は FastAPI の弟分です。そして、**CLI 版 FastAPI** を意図しています。 ⌨️ 🚀 -## 必要条件 +## 必要条件 { #requirements } FastAPI は巨人の肩の上に立っています。 -- Web の部分はStarlette -- データの部分はPydantic +* Web の部分は Starlette +* データの部分は Pydantic -## インストール +## インストール { #installation } + +virtual environment を作成して有効化し、それから FastAPI をインストールします。
```console -$ pip install fastapi +$ pip install "fastapi[standard]" ---> 100% ```
-本番環境では、Uvicorn または、 Hypercornのような、 ASGI サーバーが必要になります。 +**注**: すべてのターミナルで動作するように、`"fastapi[standard]"` は必ずクォートで囲んでください。 -
+## アプリケーション例 { #example } -```console -$ pip install "uvicorn[standard]" +### 作成 { #create-it } ----> 100% -``` - -
- -## アプリケーション例 - -### アプリケーションの作成 - -- `main.py` を作成し、以下のコードを入力します: +`main.py` ファイルを作成し、以下のコードを入力します。 ```Python from fastapi import FastAPI @@ -163,16 +172,16 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: str = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ```
-またはasync defを使います... +または async def を使います... -`async` / `await`を使用するときは、 `async def`を使います: +コードで `async` / `await` を使用する場合は、`async def` を使います。 -```Python hl_lines="7 12" +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -184,28 +193,41 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: str = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` **注**: -わからない場合は、ドキュメントの`async` と `await`にある"In a hurry?"セクションをチェックしてください。 +わからない場合は、ドキュメントの `async` と `await` の _"In a hurry?"_ セクションを確認してください。
-### 実行 +### 実行 { #run-it } -以下のコマンドでサーバーを起動します: +以下のコマンドでサーバーを起動します。
```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. ``` @@ -213,56 +235,56 @@ INFO: Application startup complete.
-uvicorn main:app --reloadコマンドについて +fastapi dev main.py コマンドについて -`uvicorn main:app`コマンドは以下の項目を参照します: +`fastapi dev` コマンドは `main.py` ファイルを読み取り、その中の **FastAPI** アプリを検出し、Uvicorn を使用してサーバーを起動します。 -- `main`: `main.py`ファイル (Python "モジュール") -- `app`: `main.py` の`app = FastAPI()`の行で生成されたオブジェクト -- `--reload`: コードを変更したらサーバーを再起動します。このオプションは開発環境でのみ使用します +デフォルトでは、`fastapi dev` はローカル開発向けに自動リロードを有効にして起動します。 + +詳しくは FastAPI CLI docs を参照してください。
-### 動作確認 +### 動作確認 { #check-it } -ブラウザからhttp://127.0.0.1:8000/items/5?q=somequeryを開きます。 +ブラウザで http://127.0.0.1:8000/items/5?q=somequery を開きます。 -以下の JSON のレスポンスが確認できます: +以下の JSON のレスポンスが確認できます。 ```JSON {"item_id": 5, "q": "somequery"} ``` -もうすでに以下の API が作成されています: +すでに以下の API が作成されています。 -- `/` と `/items/{item_id}`のパスで HTTP リクエストを受けます。 -- どちらのパスも `GET` 操作 を取ります。(HTTP メソッドとしても知られています。) -- `/items/{item_id}` パスのパスパラメータ `item_id` は `int` でなければなりません。 -- パス `/items/{item_id}` はオプションの `str` クエリパラメータ `q` を持ちます。 +* _パス_ `/` と `/items/{item_id}` で HTTP リクエストを受け取ります。 +* 両方の _パス_ は `GET` 操作(HTTP _メソッド_ としても知られています)を取ります。 +* _パス_ `/items/{item_id}` は `int` であるべき _パスパラメータ_ `item_id` を持ちます。 +* _パス_ `/items/{item_id}` はオプションの `str` _クエリパラメータ_ `q` を持ちます。 -### 自動対話型の API ドキュメント +### 自動対話型 API ドキュメント { #interactive-api-docs } -http://127.0.0.1:8000/docsにアクセスしてみてください。 +次に、http://127.0.0.1:8000/docs にアクセスします。 -自動対話型の API ドキュメントが表示されます。 (Swagger UIが提供しています。): +自動対話型 API ドキュメントが表示されます(Swagger UI が提供しています)。 ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### 代替の API ドキュメント +### 代替 API ドキュメント { #alternative-api-docs } -http://127.0.0.1:8000/redocにアクセスしてみてください。 +次に、http://127.0.0.1:8000/redoc にアクセスします。 -代替の自動ドキュメントが表示されます。(ReDocが提供しています。): +代替の自動ドキュメントが表示されます(ReDoc が提供しています)。 ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## アップグレード例 +## アップグレード例 { #example-upgrade } -`PUT`リクエストからボディを受け取るために`main.py`を修正しましょう。 +次に、`PUT` リクエストからボディを受け取るために `main.py` ファイルを修正しましょう。 -Pydantic によって、Python の標準的な型を使ってボディを宣言します。 +Pydantic によって、標準的な Python の型を使ってボディを宣言します。 -```Python hl_lines="2 7 8 9 10 23 24 25" +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -272,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: bool = None + is_offer: bool | None = None @app.get("/") @@ -281,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: str = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -290,173 +312,248 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -サーバーは自動でリロードされます。(上述の`uvicorn`コマンドで`--reload`オプションを追加しているからです。) +`fastapi dev` サーバーは自動でリロードされるはずです。 -### 自動対話型の API ドキュメントのアップグレード +### 自動対話型 API ドキュメントのアップグレード { #interactive-api-docs-upgrade } -http://127.0.0.1:8000/docsにアクセスしましょう。 +次に、http://127.0.0.1:8000/docs にアクセスします。 -- 自動対話型の API ドキュメントが新しいボディも含めて自動でアップデートされます: +* 自動対話型 API ドキュメントは新しいボディも含めて自動でアップデートされます。 ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -- "Try it out"ボタンをクリックしてください。パラメータを入力して API と直接やりとりすることができます: +* 「Try it out」ボタンをクリックします。パラメータを入力して API と直接やりとりできます。 ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -- それから、"Execute" ボタンをクリックしてください。 ユーザーインターフェースは API と通信し、パラメータを送信し、結果を取得して画面に表示します: +* 次に、「Execute」ボタンをクリックします。ユーザーインターフェースは API と通信し、パラメータを送信し、結果を取得して画面に表示します。 ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### 代替の API ドキュメントのアップグレード +### 代替 API ドキュメントのアップグレード { #alternative-api-docs-upgrade } -http://127.0.0.1:8000/redocにアクセスしましょう。 +次に、http://127.0.0.1:8000/redoc にアクセスします。 -- 代替の API ドキュメントにも新しいクエリパラメータやボディが反映されます。 +* 代替のドキュメントにも新しいクエリパラメータやボディが反映されます。 ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### まとめ +### まとめ { #recap } -要約すると、関数のパラメータとして、パラメータやボディ などの型を**一度だけ**宣言します。 +要約すると、関数のパラメータとして、パラメータやボディなどの型を **一度だけ** 宣言します。 -標準的な最新の Python の型を使っています。 +標準的な最新の Python の型を使います。 新しい構文や特定のライブラリのメソッドやクラスなどを覚える必要はありません。 -単なる標準的な**3.8 以降の Python**です。 +単なる標準的な **Python** です。 -例えば、`int`の場合: +例えば、`int` の場合: ```Python item_id: int ``` -または、より複雑な`Item`モデルの場合: +または、より複雑な `Item` モデルの場合: ```Python item: Item ``` -...そして、この一度の宣言で、以下のようになります: +...そして、この一度の宣言で、以下のようになります。 -- 以下を含むエディタサポート: - - 補完 - - タイプチェック -- データの検証: - - データが無効な場合に自動でエラーをクリアします。 - - 深い入れ子になった JSON オブジェクトでも検証が可能です。 -- 入力データの変換: ネットワークから Python のデータや型に変換してから読み取ります: - - JSON. - - パスパラメータ - - クエリパラメータ - - クッキー - - ヘッダー - - フォーム - - ファイル -- 出力データの変換: Python のデータや型からネットワークデータへ変換します (JSON として): - - Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - - `datetime` オブジェクト - - `UUID` オブジェクト - - データベースモデル - - ...などなど -- 2 つの代替ユーザーインターフェースを含む自動インタラクティブ API ドキュメント: - - Swagger UI. - - ReDoc. +* 以下を含むエディタサポート: + * 補完。 + * 型チェック。 +* データの検証: + * データが無効な場合に自動で明確なエラーを返します。 + * 深い入れ子になった JSON オブジェクトでも検証が可能です。 +* 入力データの 変換: ネットワークから Python のデータや型へ。以下から読み取ります: + * JSON。 + * パスパラメータ。 + * クエリパラメータ。 + * Cookie。 + * ヘッダー。 + * フォーム。 + * ファイル。 +* 出力データの 変換: Python のデータや型からネットワークデータへ(JSON として)変換します: + * Python の型(`str`、`int`、`float`、`bool`、`list` など)の変換。 + * `datetime` オブジェクト。 + * `UUID` オブジェクト。 + * データベースモデル。 + * ...などなど。 +* 2 つの代替ユーザーインターフェースを含む自動対話型 API ドキュメント: + * Swagger UI。 + * ReDoc。 --- -コード例に戻りましょう、**FastAPI** は次のようになります: +前のコード例に戻ると、**FastAPI** は次のように動作します。 -- `GET`および`PUT`リクエストのパスに`item_id` があることを検証します。 -- `item_id`が`GET`および`PUT`リクエストに対して`int` 型であることを検証します。 - - そうでない場合は、クライアントは有用で明確なエラーが表示されます。 -- `GET` リクエストに対してオプションのクエリパラメータ `q` (`http://127.0.0.1:8000/items/foo?q=somequery` のように) が存在するかどうかを調べます。 - - パラメータ `q` は `= None` で宣言されているので、オプションです。 - - `None`がなければ必須になります(`PUT`の場合のボディと同様です)。 -- `PUT` リクエストを `/items/{item_id}` に送信する場合は、ボディを JSON として読み込みます: - - 必須の属性 `name` を確認してください。 それは `str` であるべきです。 - - 必須の属性 `price` を確認してください。それは `float` でなければならないです。 - - オプションの属性 `is_offer` を確認してください。値がある場合は、`bool` であるべきです。 - - これらはすべて、深くネストされた JSON オブジェクトに対しても動作します。 -- JSON から JSON に自動的に変換します。 -- OpenAPIですべてを文書化し、以下を使用することができます: - - 対話的なドキュメントシステム。 - - 多くの言語に対応した自動クライアントコード生成システム。 -- 2 つの対話的なドキュメントのWebインターフェイスを直接提供します。 +* `GET` および `PUT` リクエストのパスに `item_id` があることを検証します。 +* `GET` および `PUT` リクエストに対して `item_id` が `int` 型であることを検証します。 + * そうでない場合、クライアントは有用で明確なエラーを受け取ります。 +* `GET` リクエストに対して、`q` という名前のオプションのクエリパラメータ(`http://127.0.0.1:8000/items/foo?q=somequery` のような)が存在するかどうかを調べます。 + * `q` パラメータは `= None` で宣言されているため、オプションです。 + * `None` がなければ必須になります(`PUT` の場合のボディと同様です)。 +* `PUT` リクエストを `/items/{item_id}` に送信する場合、ボディを JSON として読み込みます: + * 必須の属性 `name` があり、`str` であるべきことを確認します。 + * 必須の属性 `price` があり、`float` でなければならないことを確認します。 + * オプションの属性 `is_offer` があり、存在する場合は `bool` であるべきことを確認します。 + * これらはすべて、深くネストされた JSON オブジェクトに対しても動作します。 +* JSON への/からの変換を自動的に行います。 +* OpenAPI ですべてを文書化し、以下で利用できます: + * 対話型ドキュメントシステム。 + * 多くの言語に対応した自動クライアントコード生成システム。 +* 2 つの対話型ドキュメント Web インターフェースを直接提供します。 --- -まだ表面的な部分に触れただけですが、もう全ての仕組みは分かっているはずです。 +まだ表面的な部分に触れただけですが、仕組みはすでにイメージできているはずです。 -以下の行を変更してみてください: +以下の行を変更してみてください。 ```Python return {"item_name": item.name, "item_id": item_id} ``` -...以下を: +...以下の: ```Python ... "item_name": item.name ... ``` -...以下のように: +...を: ```Python ... "item_price": item.price ... ``` -...そして、エディタが属性を自動補完し、そのタイプを知る方法を確認してください。: +...に変更し、エディタが属性を自動補完し、その型を知ることを確認してください。 ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -より多くの機能を含む、より完全な例については、チュートリアル - ユーザーガイドをご覧ください。 +より多くの機能を含む、より完全な例については、Tutorial - User Guide を参照してください。 -**ネタバレ注意**: チュートリアル - ユーザーガイドは以下の情報が含まれています: +**ネタバレ注意**: tutorial - user guide には以下が含まれます。 -- **ヘッダー**、**クッキー**、**フォームフィールド**、**ファイル**などの他の場所からの **パラメータ** 宣言。 -- `maximum_length`や`regex`のような**検証や制約**を設定する方法。 -- 非常に強力で使いやすい **依存性注入**システム。 -- **JWT トークン**を用いた **OAuth2** や **HTTP Basic 認証** のサポートを含む、セキュリティと認証。 -- **深くネストされた JSON モデル**を宣言するためのより高度な(しかし同様に簡単な)技術(Pydantic のおかげです)。 -- 以下のようなたくさんのおまけ機能(Starlette のおかげです): - - **WebSockets** - - **GraphQL** - - `httpx` や `pytest`をもとにした極限に簡単なテスト - - **CORS** - - **クッキーセッション** - - ...などなど。 +* **ヘッダー**、**Cookie**、**フォームフィールド**、**ファイル**など、他のさまざまな場所からの **パラメータ** 宣言。 +* `maximum_length` や `regex` のような **検証制約** を設定する方法。 +* 非常に強力で使いやすい **依存性注入** システム。 +* **JWT トークン**を用いた **OAuth2** や **HTTP Basic** 認証のサポートを含む、セキュリティと認証。 +* **深くネストされた JSON モデル**を宣言するための、より高度な(しかし同様に簡単な)手法(Pydantic のおかげです)。 +* Strawberry および他のライブラリによる **GraphQL** 統合。 +* 以下のようなたくさんのおまけ機能(Starlette のおかげです): + * **WebSockets** + * HTTPX と `pytest` に基づく極めて簡単なテスト + * **CORS** + * **Cookie Sessions** + * ...などなど。 -## パフォーマンス +### アプリをデプロイ(任意) { #deploy-your-app-optional } -独立した TechEmpower のベンチマークでは、Uvicorn で動作する**FastAPI**アプリケーションが、Python フレームワークの中で最も高速なものの 1 つであり、Starlette と Uvicorn(FastAPI で内部的に使用されています)にのみ下回っていると示されています。 +必要に応じて FastAPI アプリを FastAPI Cloud にデプロイできます。まだの場合はウェイティングリストに参加してください。 🚀 -詳細はベンチマークセクションをご覧ください。 +すでに **FastAPI Cloud** アカウント(ウェイティングリストから招待されました 😉)がある場合は、1 コマンドでアプリケーションをデプロイできます。 -## オプションの依存関係 +デプロイ前に、ログインしていることを確認してください。 + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +次に、アプリをデプロイします。 + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +これで完了です!その URL でアプリにアクセスできます。 ✨ + +#### FastAPI Cloud について { #about-fastapi-cloud } + +**FastAPI Cloud** は **FastAPI** の作者と同じチームによって作られています。 + +最小限の労力で API を **構築**、**デプロイ**、**アクセス** するためのプロセスを効率化します。 + +FastAPI でアプリを構築するのと同じ **開発者体験** を、クラウドへの **デプロイ** にももたらします。 🎉 + +FastAPI Cloud は *FastAPI and friends* オープンソースプロジェクトの主要スポンサーであり、資金提供元です。 ✨ + +#### 他のクラウドプロバイダにデプロイ { #deploy-to-other-cloud-providers } + +FastAPI はオープンソースであり、標準に基づいています。選択した任意のクラウドプロバイダに FastAPI アプリをデプロイできます。 + +各クラウドプロバイダのガイドに従って、FastAPI アプリをデプロイしてください。 🤓 + +## パフォーマンス { #performance } + +独立した TechEmpower のベンチマークでは、Uvicorn で動作する **FastAPI** アプリケーションが、利用可能な最も高速な Python フレームワークの一つであり、Starlette と Uvicorn(FastAPI で内部的に使用されています)にのみ下回っていると示されています。(*) + +詳細は Benchmarks セクションをご覧ください。 + +## 依存関係 { #dependencies } + +FastAPI は Pydantic と Starlette に依存しています。 + +### `standard` 依存関係 { #standard-dependencies } + +FastAPI を `pip install "fastapi[standard]"` でインストールすると、`standard` グループのオプション依存関係が含まれます。 Pydantic によって使用されるもの: -- email-validator - E メールの検証 +* email-validator - メール検証のため。 Starlette によって使用されるもの: -- httpx - `TestClient`を使用するために必要です。 -- jinja2 - デフォルトのテンプレート設定を使用する場合は必要です。 -- python-multipart - "parsing"`request.form()`からの変換をサポートしたい場合は必要です。 -- itsdangerous - `SessionMiddleware` サポートのためには必要です。 -- pyyaml - Starlette の `SchemaGenerator` サポートのために必要です。 (FastAPI では必要ないでしょう。) -- graphene - `GraphQLApp` サポートのためには必要です。 +* httpx - `TestClient` を使用したい場合に必要です。 +* jinja2 - デフォルトのテンプレート設定を使用したい場合に必要です。 +* python-multipart - `request.form()` とともに、フォームの 「parsing」 をサポートしたい場合に必要です。 -FastAPI / Starlette に使用されるもの: +FastAPI によって使用されるもの: -- uvicorn - アプリケーションをロードしてサーブするサーバーのため。 -- orjson - `ORJSONResponse`を使用したい場合は必要です。 -- ujson - `UJSONResponse`を使用する場合は必須です。 +* uvicorn - アプリケーションをロードして提供するサーバーのため。これには `uvicorn[standard]` も含まれ、高性能なサービングに必要な依存関係(例: `uvloop`)が含まれます。 +* `fastapi-cli[standard]` - `fastapi` コマンドを提供します。 + * これには `fastapi-cloud-cli` が含まれ、FastAPI アプリケーションを FastAPI Cloud にデプロイできます。 -これらは全て `pip install fastapi[all]`でインストールできます。 +### `standard` 依存関係なし { #without-standard-dependencies } -## ライセンス +`standard` のオプション依存関係を含めたくない場合は、`pip install "fastapi[standard]"` の代わりに `pip install fastapi` でインストールできます。 -このプロジェクトは MIT ライセンスです。 +### `fastapi-cloud-cli` なし { #without-fastapi-cloud-cli } + +標準の依存関係を含めつつ `fastapi-cloud-cli` を除外して FastAPI をインストールしたい場合は、`pip install "fastapi[standard-no-fastapi-cloud-cli]"` でインストールできます。 + +### 追加のオプション依存関係 { #additional-optional-dependencies } + +追加でインストールしたい依存関係があります。 + +追加のオプション Pydantic 依存関係: + +* pydantic-settings - 設定管理のため。 +* pydantic-extra-types - Pydantic で使用する追加の型のため。 + +追加のオプション FastAPI 依存関係: + +* orjson - `ORJSONResponse` を使用したい場合に必要です。 +* ujson - `UJSONResponse` を使用したい場合に必要です。 + +## ライセンス { #license } + +このプロジェクトは MIT ライセンスの条項の下でライセンスされています。 diff --git a/docs/ja/docs/learn/index.md b/docs/ja/docs/learn/index.md index 2f24c670a..bcdb1e37e 100644 --- a/docs/ja/docs/learn/index.md +++ b/docs/ja/docs/learn/index.md @@ -1,5 +1,5 @@ -# 学習 +# 学習 { #learn } ここでは、**FastAPI** を学習するための入門セクションとチュートリアルを紹介します。 -これは、FastAPIを学習するにあたっての**書籍**や**コース**であり、**公式**かつ推奨される方法とみなすことができます 😎 +これは、**書籍**や**コース**、FastAPIを学習するための**公式**かつ推奨される方法とみなすことができます。😎 diff --git a/docs/ja/docs/project-generation.md b/docs/ja/docs/project-generation.md index daef52efa..c930fb557 100644 --- a/docs/ja/docs/project-generation.md +++ b/docs/ja/docs/project-generation.md @@ -1,84 +1,28 @@ -# プロジェクト生成 - テンプレート +# Full Stack FastAPI テンプレート { #full-stack-fastapi-template } -プロジェクトジェネレーターは、初期設定、セキュリティ、データベース、初期APIエンドポイントなどの多くが含まれているため、プロジェクトの開始に利用できます。 +テンプレートは通常、特定のセットアップが含まれていますが、柔軟でカスタマイズできるように設計されています。これにより、プロジェクトの要件に合わせて変更・適応でき、優れた出発点になります。🏁 -プロジェクトジェネレーターは常に非常に意見が分かれる設定がされており、ニーズに合わせて更新および調整する必要があります。しかしきっと、プロジェクトの良い出発点となるでしょう。 +このテンプレートを使って開始できます。初期セットアップ、セキュリティ、データベース、いくつかのAPIエンドポイントがすでに用意されています。 -## フルスタック FastAPI PostgreSQL +GitHubリポジトリ: Full Stack FastAPI Template -GitHub: https://github.com/tiangolo/full-stack-fastapi-postgresql +## Full Stack FastAPI テンプレート - 技術スタックと機能 { #full-stack-fastapi-template-technology-stack-and-features } -### フルスタック FastAPI PostgreSQL - 機能 - -* 完全な**Docker**インテグレーション (Dockerベース)。 -* Docker Swarm モードデプロイ。 -* ローカル開発環境向けの**Docker Compose**インテグレーションと最適化。 -* UvicornとGunicornを使用した**リリース可能な** Python web サーバ。 -* Python **FastAPI** バックエンド: - * **高速**: **NodeJS** や **Go** 並みのとても高いパフォーマンス (Starlette と Pydantic のおかげ)。 - * **直感的**: 素晴らしいエディタのサポートや 補完。 デバッグ時間の短縮。 - * **簡単**: 簡単に利用、習得できるようなデザイン。ドキュメントを読む時間を削減。 - * **短い**: コードの重複を最小限に。パラメータ宣言による複数の機能。 - * **堅牢性**: 自動対話ドキュメントを使用した、本番環境で使用できるコード。 - * **標準規格準拠**: API のオープンスタンダードに基く、完全な互換性: OpenAPIJSON スキーマ。 - * 自動バリデーション、シリアライゼーション、対話的なドキュメント、OAuth2 JWTトークンを用いた認証などを含む、**その他多くの機能**。 -* **セキュアなパスワード** ハッシュ化 (デフォルトで)。 -* **JWTトークン** 認証。 -* **SQLAlchemy** モデル (Flask用の拡張と独立しているので、Celeryワーカーと直接的に併用できます)。 -* 基本的なユーザーモデル (任意の修正や削除が可能)。 -* **Alembic** マイグレーション。 -* **CORS** (Cross Origin Resource Sharing (オリジン間リソース共有))。 -* **Celery** ワーカー。バックエンドの残りの部分からモデルとコードを選択的にインポートし、使用可能。 -* Dockerと統合された**Pytest**ベースのRESTバックエンドテスト。データベースに依存せずに、全てのAPIをテスト可能。Docker上で動作するので、毎回ゼロから新たなデータストアを構築可能。(ElasticSearch、MongoDB、CouchDBなどを使用して、APIの動作をテスト可能) -* Atom HydrogenやVisual Studio Code Jupyterなどの拡張機能を使用した、リモートまたはDocker開発用の**Jupyterカーネル**との簡単なPython統合。 -* **Vue** フロントエンド: - * Vue CLIにより生成。 - * **JWT認証**の処理。 - * ログインビュー。 - * ログイン後の、メインダッシュボードビュー。 - * メインダッシュボードでのユーザー作成と編集。 - * セルフユーザー版 - * **Vuex**。 - * **Vue-router**。 - * 美しいマテリアルデザインコンポーネントのための**Vuetify**。 - * **TypeScript**。 - * **Nginx**ベースのDockerサーバ (Vue-routerとうまく協調する構成)。 - * Dockerマルチステージビルド。コンパイルされたコードの保存やコミットが不要。 - * ビルド時にフロントエンドテスト実行 (無効化も可能)。 - * 可能な限りモジュール化されているのでそのまま使用できますが、Vue CLIで再生成したり、必要に応じて作成したりして、必要なものを再利用可能。 -* PostgreSQLデータベースのための**PGAdmin**。(PHPMyAdminとMySQLを使用できるように簡単に変更可能) -* Celeryジョブ監視のための**Flower**。 -* **Traefik**を使用してフロントエンドとバックエンド間をロードバランシング。同一ドメインに配置しパスで区切る、ただし、異なるコンテナで処理。 -* Traefik統合。Let's Encrypt **HTTPS**証明書の自動生成を含む。 -* GitLab **CI** (継続的インテグレーション)。フロントエンドおよびバックエンドテストを含む。 - -## フルスタック FastAPI Couchbase - -GitHub: https://github.com/tiangolo/full-stack-fastapi-couchbase - -⚠️ **警告** ⚠️ - -ゼロから新規プロジェクトを始める場合は、ここで代替案を確認してください。 - -例えば、フルスタック FastAPI PostgreSQLのプロジェクトジェネレーターは、積極的にメンテナンスされ、利用されているのでより良い代替案かもしれません。また、すべての新機能と改善点が含まれています。 - -Couchbaseベースのジェネレーターは今も無償提供されています。恐らく正常に動作するでしょう。また、すでにそのジェネレーターで生成されたプロジェクトが存在する場合でも (ニーズに合わせてアップデートしているかもしれません)、同様に正常に動作するはずです。 - -詳細はレポジトリのドキュメントを参照して下さい。 - -## フルスタック FastAPI MongoDB - -...時間の都合等によっては、今後作成されるかもしれません。😅 🎉 - -## spaCyとFastAPIを使用した機械学習モデル - -GitHub: https://github.com/microsoft/cookiecutter-spacy-fastapi - -### spaCyとFastAPIを使用した機械学習モデル - 機能 - -* **spaCy** のNERモデルの統合。 -* **Azure Cognitive Search** のリクエストフォーマットを搭載。 -* **リリース可能な** UvicornとGunicornを使用したPythonウェブサーバ。 -* **Azure DevOps** のKubernetes (AKS) CI/CD デプロイを搭載。 -* **多言語** プロジェクトのために、セットアップ時に言語を容易に選択可能 (spaCyに組み込まれている言語の中から)。 -* **簡単に拡張可能**。spaCyだけでなく、他のモデルフレームワーク (Pytorch、Tensorflow) へ。 +- ⚡ PythonバックエンドAPI向けの [**FastAPI**](https://fastapi.tiangolo.com/ja)。 + - 🧰 PythonのSQLデータベース操作(ORM)向けの [SQLModel](https://sqlmodel.tiangolo.com)。 + - 🔍 FastAPIで使用される、データバリデーションと設定管理向けの [Pydantic](https://docs.pydantic.dev)。 + - 💾 SQLデータベースとしての [PostgreSQL](https://www.postgresql.org)。 +- 🚀 フロントエンド向けの [React](https://react.dev)。 + - 💃 TypeScript、hooks、Vite、その他のモダンなフロントエンドスタックの各要素を使用。 + - 🎨 フロントエンドコンポーネント向けの [Tailwind CSS](https://tailwindcss.com) と [shadcn/ui](https://ui.shadcn.com)。 + - 🤖 自動生成されたフロントエンドクライアント。 + - 🧪 End-to-Endテスト向けの [Playwright](https://playwright.dev)。 + - 🦇 ダークモードのサポート。 +- 🐋 開発および本番向けの [Docker Compose](https://www.docker.com)。 +- 🔒 デフォルトでの安全なパスワードハッシュ化。 +- 🔑 JWT(JSON Web Token)認証。 +- 📫 メールベースのパスワードリカバリ。 +- ✅ [Pytest](https://pytest.org) によるテスト。 +- 📞 リバースプロキシ / ロードバランサとしての [Traefik](https://traefik.io)。 +- 🚢 Docker Composeを使用したデプロイ手順(自動HTTPS証明書を処理するフロントエンドTraefikプロキシのセットアップ方法を含む)。 +- 🏭 GitHub Actionsに基づくCI(continuous integration)とCD(continuous deployment)。 diff --git a/docs/ja/docs/python-types.md b/docs/ja/docs/python-types.md index a847ce5d5..26a9e2193 100644 --- a/docs/ja/docs/python-types.md +++ b/docs/ja/docs/python-types.md @@ -1,16 +1,16 @@ -# Pythonの型の紹介 +# Pythonの型の紹介 { #python-types-intro } -**Python 3.6以降** では「型ヒント」オプションがサポートされています。 +Pythonではオプションの「型ヒント」(「型アノテーション」とも呼ばれます)がサポートされています。 -これらの **"型ヒント"** は変数のを宣言することができる新しい構文です。(Python 3.6以降) +これらの **「型ヒント」** またはアノテーションは、変数のを宣言できる特別な構文です。 -変数に型を宣言することでエディターやツールがより良いサポートを提供することができます。 +変数に型を宣言することで、エディターやツールがより良いサポートを提供できます。 -ここではPythonの型ヒントについての **クイックチュートリアル/リフレッシュ** で、**FastAPI**でそれらを使用するために必要な最低限のことだけをカバーしています。...実際には本当に少ないです。 +これはPythonの型ヒントについての **クイックチュートリアル/リフレッシュ** にすぎません。**FastAPI** で使用するために必要な最低限のことだけをカバーしています。...実際には本当に少ないです。 **FastAPI** はすべてこれらの型ヒントに基づいており、多くの強みと利点を与えてくれます。 -しかしたとえまったく **FastAPI** を使用しない場合でも、それらについて少し学ぶことで利点を得ることができるでしょう。 +しかし、たとえ **FastAPI** をまったく使用しない場合でも、それらについて少し学ぶことで利点を得られます。 /// note | 備考 @@ -18,14 +18,13 @@ /// -## 動機 +## 動機 { #motivation } 簡単な例から始めてみましょう: -{* ../../docs_src/python_types/tutorial001.py *} +{* ../../docs_src/python_types/tutorial001_py39.py *} - -このプログラムを実行すると以下が出力されます: +このプログラムを呼び出すと、以下が出力されます: ``` John Doe @@ -35,12 +34,11 @@ John Doe * `first_name`と`last_name`を取得します。 * `title()`を用いて、それぞれの最初の文字を大文字に変換します。 -* 真ん中にスペースを入れて連結します。 +* 真ん中にスペースを入れて連結します。 -{* ../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} - -### 編集 +### 編集 { #edit-it } これはとても簡単なプログラムです。 @@ -50,7 +48,7 @@ John Doe しかし、そうすると「最初の文字を大文字に変換するあのメソッド」を呼び出す必要があります。 -それは`upper`でしたか?`uppercase`でしたか?それとも`first_uppercase`?または`capitalize`? +それは`upper`でしたか?`uppercase`でしたか?`first_uppercase`?`capitalize`? そして、古くからプログラマーの友人であるエディタで自動補完を試してみます。 @@ -58,13 +56,13 @@ John Doe しかし、悲しいことに、これはなんの役にも立ちません: - + -### 型の追加 +### 型の追加 { #add-types } 先ほどのコードから一行変更してみましょう。 -以下の関数のパラメータ部分を: +関数のパラメータである次の断片を、以下から: ```Python first_name, last_name @@ -80,8 +78,7 @@ John Doe それが「型ヒント」です: -{* ../../docs_src/python_types/tutorial002.py hl[1] *} - +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} これは、以下のようにデフォルト値を宣言するのと同じではありません: @@ -95,41 +92,39 @@ John Doe そして、通常、型ヒントを追加しても、それらがない状態と起こることは何も変わりません。 -しかし今、あなたが再びその関数を作成している最中に、型ヒントを使っていると想像してみて下さい。 +しかし今、あなたが再びその関数を作成している最中に、型ヒントを使っていると想像してみてください。 同じタイミングで`Ctrl+Space`で自動補完を実行すると、以下のようになります: - + -これであれば、あなたは「ベルを鳴らす」一つを見つけるまで、オプションを見て、スクロールすることができます: +これであれば、あなたは「ベルを鳴らす」ものを見つけるまで、オプションを見てスクロールできます: - + -## より強い動機 +## より強い動機 { #more-motivation } この関数を見てください。すでに型ヒントを持っています: -{* ../../docs_src/python_types/tutorial003.py hl[1] *} +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} +エディタは変数の型を知っているので、補完だけでなく、エラーチェックをすることもできます: -エディタは変数の型を知っているので、補完だけでなく、エラーチェックをすることもできます。 - - + これで`age`を`str(age)`で文字列に変換して修正する必要があることがわかります: -{* ../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} +## 型の宣言 { #declaring-types } -## 型の宣言 - -関数のパラメータとして、型ヒントを宣言している主な場所を確認しました。 +型ヒントを宣言する主な場所を見てきました。関数のパラメータです。 これは **FastAPI** で使用する主な場所でもあります。 -### 単純な型 +### 単純な型 { #simple-types } -`str`だけでなく、Pythonの標準的な型すべてを宣言することができます。 +`str`だけでなく、Pythonの標準的な型すべてを宣言できます。 例えば、以下を使用可能です: @@ -138,40 +133,47 @@ John Doe * `bool` * `bytes` -{* ../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} +### 型パラメータを持つジェネリック型 { #generic-types-with-type-parameters } -### 型パラメータを持つジェネリック型 +データ構造の中には、`dict`、`list`、`set`、`tuple`のように他の値を含むことができるものがあります。また内部の値も独自の型を持つことができます。 -データ構造の中には、`dict`、`list`、`set`、そして`tuple`のように他の値を含むことができるものがあります。また内部の値も独自の型を持つことができます。 +内部の型を持つこれらの型は「**generic**」型と呼ばれます。そして、内部の型も含めて宣言することが可能です。 -これらの型や内部の型を宣言するには、Pythonの標準モジュール`typing`を使用します。 +これらの型や内部の型を宣言するには、Pythonの標準モジュール`typing`を使用できます。これらの型ヒントをサポートするために特別に存在しています。 -これらの型ヒントをサポートするために特別に存在しています。 +#### 新しいPythonバージョン { #newer-versions-of-python } -#### `List` +`typing`を使う構文は、Python 3.6から最新バージョンまで(Python 3.9、Python 3.10などを含む)すべてのバージョンと **互換性** があります。 + +Pythonが進化するにつれ、**新しいバージョン** ではこれらの型アノテーションへのサポートが改善され、多くの場合、型アノテーションを宣言するために`typing`モジュールをインポートして使う必要すらなくなります。 + +プロジェクトでより新しいPythonバージョンを選べるなら、その追加のシンプルさを活用できます。 + +ドキュメント全体で、Pythonの各バージョンと互換性のある例(差分がある場合)を示しています。 + +例えば「**Python 3.6+**」はPython 3.6以上(3.7、3.8、3.9、3.10などを含む)と互換性があることを意味します。また「**Python 3.9+**」はPython 3.9以上(3.10などを含む)と互換性があることを意味します。 + +**最新のPythonバージョン** を使えるなら、最新バージョン向けの例を使ってください。例えば「**Python 3.10+**」のように、それらは **最良かつ最もシンプルな構文** になります。 + +#### List { #list } 例えば、`str`の`list`の変数を定義してみましょう。 -`typing`から`List`をインポートします(大文字の`L`を含む): +同じコロン(`:`)の構文で変数を宣言します。 -{* ../../docs_src/python_types/tutorial006.py hl[1] *} +型として、`list`を指定します。 +リストはいくつかの内部の型を含む型なので、それらを角括弧で囲みます: -同じようにコロン(`:`)の構文で変数を宣言します。 +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} -型として、`List`を入力します。 - -リストはいくつかの内部の型を含む型なので、それらを角括弧で囲んでいます。 - -{* ../../docs_src/python_types/tutorial006.py hl[4] *} - - -/// tip | 豆知識 +/// info | 情報 角括弧内の内部の型は「型パラメータ」と呼ばれています。 -この場合、`str`は`List`に渡される型パラメータです。 +この場合、`str`は`list`に渡される型パラメータです。 /// @@ -179,86 +181,203 @@ John Doe そうすることで、エディタはリストの項目を処理している間にもサポートを提供できます。 - + -タイプがなければ、それはほぼ不可能です。 +型がなければ、それはほぼ不可能です。 変数`item`はリスト`items`の要素の一つであることに注意してください。 それでも、エディタはそれが`str`であることを知っていて、そのためのサポートを提供しています。 -#### `Tuple` と `Set` +#### Tuple と Set { #tuple-and-set } `tuple`と`set`の宣言も同様です: -{* ../../docs_src/python_types/tutorial007.py hl[1,4] *} - +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} つまり: -* 変数`items_t`は`int`、`int`、`str`の3つの項目を持つ`tuple`です +* 変数`items_t`は`int`、別の`int`、`str`の3つの項目を持つ`tuple`です。 +* 変数`items_s`は`set`であり、その各項目は`bytes`型です。 -* 変数`items_s`はそれぞれの項目が`bytes`型である`set`です。 +#### Dict { #dict } -#### `Dict` - -`dict`を宣言するためには、カンマ区切りで2つの型パラメータを渡します。 +`dict`を定義するには、カンマ区切りで2つの型パラメータを渡します。 最初の型パラメータは`dict`のキーです。 -2番目の型パラメータは`dict`の値です。 - -{* ../../docs_src/python_types/tutorial008.py hl[1,4] *} +2番目の型パラメータは`dict`の値です: +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} つまり: -* 変数`prices`は`dict`であり: - * この`dict`のキーは`str`型です。(つまり、各項目の名前) - * この`dict`の値は`float`型です。(つまり、各項目の価格) +* 変数`prices`は`dict`です: + * この`dict`のキーは`str`型です(例えば、各項目の名前)。 + * この`dict`の値は`float`型です(例えば、各項目の価格)。 -#### `Optional` +#### Union { #union } -また、`Optional`を使用して、変数が`str`のような型を持つことを宣言することもできますが、それは「オプション」であり、`None`にすることもできます。 +変数が**複数の型のいずれか**になり得ることを宣言できます。例えば、`int`または`str`です。 -```Python hl_lines="1 4" -{!../../docs_src/python_types/tutorial009.py!} +Python 3.6以上(Python 3.10を含む)では、`typing`の`Union`型を使い、角括弧の中に受け付ける可能性のある型を入れられます。 + +Python 3.10では、受け付ける可能性のある型を縦棒(`|`)で区切って書ける **新しい構文** もあります。 + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../docs_src/python_types/tutorial008b_py310.py!} ``` -ただの`str`の代わりに`Optional[str]`を使用することで、エディタは値が常に`str`であると仮定している場合に実際には`None`である可能性があるエラーを検出するのに役立ちます。 +//// -#### ジェネリック型 +//// tab | Python 3.9+ -以下のように角括弧で型パラメータを取る型を: +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial008b_py39.py!} +``` -* `List` -* `Tuple` -* `Set` -* `Dict` +//// + +どちらの場合も、`item`は`int`または`str`になり得ることを意味します。 + +#### `None`の可能性 { #possibly-none } + +値が`str`のような型を持つ可能性がある一方で、`None`にもなり得ることを宣言できます。 + +Python 3.6以上(Python 3.10を含む)では、`typing`モジュールから`Optional`をインポートして使うことで宣言できます。 + +```Python hl_lines="1 4" +{!../../docs_src/python_types/tutorial009_py39.py!} +``` + +ただの`str`の代わりに`Optional[str]`を使用することで、値が常に`str`であると仮定しているときに、実際には`None`である可能性もあるというエラーをエディタが検出するのに役立ちます。 + +`Optional[Something]`は実際には`Union[Something, None]`のショートカットで、両者は等価です。 + +これは、Python 3.10では`Something | None`も使えることを意味します: + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../docs_src/python_types/tutorial009_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009_py39.py!} +``` + +//// + +//// tab | Python 3.9+ alternative + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009b_py39.py!} +``` + +//// + +#### `Union`または`Optional`の使用 { #using-union-or-optional } + +Python 3.10未満のバージョンを使っている場合、これは私のとても **主観的** な観点からのヒントです: + +* 🚨 `Optional[SomeType]`は避けてください +* 代わりに ✨ **`Union[SomeType, None]`を使ってください** ✨ + +どちらも等価で、内部的には同じですが、`Optional`より`Union`をおすすめします。というのも「**optional**」という単語は値がオプションであることを示唆するように見えますが、実際には「`None`になり得る」という意味であり、オプションではなく必須である場合でもそうだからです。 + +`Union[SomeType, None]`のほうが意味がより明示的だと思います。 + +これは言葉や名前の話にすぎません。しかし、その言葉はあなたやチームメイトがコードをどう考えるかに影響し得ます。 + +例として、この関数を見てみましょう: + +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} + +パラメータ`name`は`Optional[str]`として定義されていますが、**オプションではありません**。そのパラメータなしで関数を呼び出せません: + +```Python +say_hi() # Oh, no, this throws an error! 😱 +``` + +`name`パラメータはデフォルト値がないため、**依然として必須**(*optional*ではない)です。それでも、`name`は値として`None`を受け付けます: + +```Python +say_hi(name=None) # This works, None is valid 🎉 +``` + +良い知らせとして、Python 3.10になればその心配は不要です。型のユニオンを定義するために`|`を単純に使えるからです: + +{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *} + +そして、`Optional`や`Union`のような名前について心配する必要もなくなります。😎 + +#### ジェネリック型 { #generic-types } + +角括弧で型パラメータを取るこれらの型は、例えば次のように **Generic types** または **Generics** と呼ばれます: + +//// tab | Python 3.10+ + +同じ組み込み型をジェネリクスとして(角括弧と内部の型で)使えます: + +* `list` +* `tuple` +* `set` +* `dict` + +また、これまでのPythonバージョンと同様に、`typing`モジュールから: + +* `Union` * `Optional` -* ...など +* ...and others. -**ジェネリック型** または **ジェネリクス** と呼びます。 +Python 3.10では、ジェネリクスの`Union`や`Optional`を使う代替として、型のユニオンを宣言するために縦棒(`|`)を使えます。これはずっと良く、よりシンプルです。 -### 型としてのクラス +//// + +//// tab | Python 3.9+ + +同じ組み込み型をジェネリクスとして(角括弧と内部の型で)使えます: + +* `list` +* `tuple` +* `set` +* `dict` + +そして`typing`モジュールのジェネリクス: + +* `Union` +* `Optional` +* ...and others. + +//// + +### 型としてのクラス { #classes-as-types } 変数の型としてクラスを宣言することもできます。 -例えば、`Person`クラスという名前のクラスがあるとしましょう: +名前を持つ`Person`クラスがあるとしましょう: -{* ../../docs_src/python_types/tutorial010.py hl[1,2,3] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} +変数を`Person`型として宣言できます: -変数の型を`Person`として宣言することができます: - -{* ../../docs_src/python_types/tutorial010.py hl[6] *} - +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} そして、再び、すべてのエディタのサポートを得ることができます: - + -## Pydanticのモデル +これは「`one_person`はクラス`Person`の**インスタンス**である」ことを意味します。 + +「`one_person`は`Person`という名前の**クラス**である」という意味ではありません。 + +## Pydanticのモデル { #pydantic-models } Pydantic はデータ検証を行うためのPythonライブラリです。 @@ -266,18 +385,17 @@ John Doe そして、それぞれの属性は型を持ちます。 -さらに、いくつかの値を持つクラスのインスタンスを作成すると、その値を検証し、適切な型に変換して(もしそうであれば)全てのデータを持つオブジェクトを提供してくれます。 +さらに、いくつかの値を持つクラスのインスタンスを作成すると、その値を検証し、適切な型に変換して(もしそうであれば)すべてのデータを持つオブジェクトを提供してくれます。 また、その結果のオブジェクトですべてのエディタのサポートを受けることができます。 -Pydanticの公式ドキュメントから引用: - -{* ../../docs_src/python_types/tutorial011.py *} +Pydanticの公式ドキュメントからの例: +{* ../../docs_src/python_types/tutorial011_py310.py *} /// info | 情報 -Pydanticについてより学びたい方はドキュメントを参照してください. +Pydanticの詳細はドキュメントを参照してください。 /// @@ -285,30 +403,62 @@ Pydanticについてより学びたい方はRequired Optional fieldsを参照してください。 + +/// + +## メタデータアノテーション付き型ヒント { #type-hints-with-metadata-annotations } + +Pythonには、`Annotated`を使って型ヒントに**追加のメタデータ**を付与できる機能もあります。 + +Python 3.9以降、`Annotated`は標準ライブラリの一部なので、`typing`からインポートできます。 + +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} + +Python自体は、この`Annotated`で何かをするわけではありません。また、エディタや他のツールにとっても、型は依然として`str`です。 + +しかし、`Annotated`内のこのスペースを使って、アプリケーションをどのように動作させたいかについての追加メタデータを **FastAPI** に提供できます。 + +覚えておくべき重要な点は、`Annotated`に渡す**最初の*型パラメータ***が**実際の型**であることです。残りは、他のツール向けのメタデータにすぎません。 + +今のところは、`Annotated`が存在し、それが標準のPythonであることを知っておけば十分です。😎 + +後で、これがどれほど**強力**になり得るかを見ることになります。 + +/// tip | 豆知識 + +これが **標準のPython** であるという事実は、エディタで、使用しているツール(コードの解析やリファクタリングなど)とともに、**可能な限り最高の開発体験**が得られることを意味します。 ✨ + +また、あなたのコードが他の多くのPythonツールやライブラリとも非常に互換性が高いことも意味します。 🚀 + +/// + +## **FastAPI**での型ヒント { #type-hints-in-fastapi } **FastAPI** はこれらの型ヒントを利用していくつかのことを行います。 -**FastAPI** では型ヒントを使って型パラメータを宣言すると以下のものが得られます: +**FastAPI** では型ヒントを使ってパラメータを宣言すると以下のものが得られます: -* **エディタサポート**. -* **型チェック**. +* **エディタサポート**。 +* **型チェック**。 -...そして **FastAPI** は同じように宣言をすると、以下のことを行います: +...そして **FastAPI** は同じ宣言を使って、以下のことを行います: -* **要件の定義**: リクエストパスパラメータ、クエリパラメータ、ヘッダー、ボディ、依存関係などから要件を定義します。 -* **データの変換**: リクエストのデータを必要な型に変換します。 -* **データの検証**: リクエストごとに: +* **要件の定義**: リクエストのパスパラメータ、クエリパラメータ、ヘッダー、ボディ、依存関係などから要件を定義します。 +* **データの変換**: リクエストから必要な型にデータを変換します。 +* **データの検証**: 各リクエストから来るデータについて: * データが無効な場合にクライアントに返される **自動エラー** を生成します。 -* **ドキュメント** OpenAPIを使用したAPI: - * 自動的に対話型ドキュメントのユーザーインターフェイスで使用されます。 +* OpenAPIを使用してAPIを**ドキュメント化**します: + * これは自動の対話型ドキュメントのユーザーインターフェイスで使われます。 すべてが抽象的に聞こえるかもしれません。心配しないでください。 この全ての動作は [チュートリアル - ユーザーガイド](tutorial/index.md){.internal-link target=_blank}で見ることができます。 -重要なのは、Pythonの標準的な型を使うことで、(クラスやデコレータなどを追加するのではなく)1つの場所で **FastAPI** が多くの作業を代わりにやってくれているということです。 +重要なのは、Pythonの標準的な型を使うことで、(クラスやデコレータなどを追加するのではなく)1つの場所で **FastAPI** が多くの作業を代わりにやってくれているということです。 /// info | 情報 -すでにすべてのチュートリアルを終えて、型についての詳細を見るためにこのページに戻ってきた場合は、`mypy`のチートシートを参照してください +すでにすべてのチュートリアルを終えて、型についての詳細を見るためにこのページに戻ってきた場合は、良いリソースとして`mypy`の「チートシート」があります。 /// diff --git a/docs/ja/docs/tutorial/background-tasks.md b/docs/ja/docs/tutorial/background-tasks.md index b289faf12..0ed41ce11 100644 --- a/docs/ja/docs/tutorial/background-tasks.md +++ b/docs/ja/docs/tutorial/background-tasks.md @@ -1,4 +1,4 @@ -# バックグラウンドタスク +# バックグラウンドタスク { #background-tasks } レスポンスを返した *後に* 実行されるバックグラウンドタスクを定義できます。 @@ -9,17 +9,17 @@ * 作業実行後のメール通知: * メールサーバーへの接続とメールの送信は「遅い」(数秒) 傾向があるため、すぐにレスポンスを返し、バックグラウンドでメール通知ができます。 * データ処理: - * たとえば、時間のかかる処理を必要とするファイル受信時には、「受信済み」(HTTP 202) のレスポンスを返し、バックグラウンドで処理できます。 + * たとえば、時間のかかる処理を必要とするファイル受信時には、「Accepted」(HTTP 202) のレスポンスを返し、バックグラウンドで処理できます。 -## `BackgroundTasks` の使用 +## `BackgroundTasks` の使用 { #using-backgroundtasks } -まず初めに、`BackgroundTasks` をインポートし、` BackgroundTasks` の型宣言と共に、*path operation 関数* のパラメーターを定義します: +まず初めに、`BackgroundTasks` をインポートし、`BackgroundTasks` の型宣言と共に、*path operation function* のパラメーターを定義します: -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} **FastAPI** は、`BackgroundTasks` 型のオブジェクトを作成し、そのパラメーターに渡します。 -## タスク関数の作成 +## タスク関数の作成 { #create-a-task-function } バックグラウンドタスクとして実行される関数を作成します。 @@ -31,13 +31,13 @@ また、書き込み操作では `async` と `await` を使用しないため、通常の `def` で関数を定義します。 -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} -## バックグラウンドタスクの追加 +## バックグラウンドタスクの追加 { #add-the-background-task } -*path operations 関数* 内で、`.add_task()` メソッドを使用してタスク関数を *background tasks* オブジェクトに渡します。 +*path operation function* 内で、`.add_task()` メソッドを使用してタスク関数を *background tasks* オブジェクトに渡します。 -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} `.add_task()` は以下の引数を受け取ります: @@ -45,40 +45,42 @@ * タスク関数に順番に渡す必要のある引数の列 (`email`)。 * タスク関数に渡す必要のあるキーワード引数 (`message="some notification"`)。 -## 依存性注入 +## 依存性注入 { #dependency-injection } -`BackgroundTasks` の使用は依存性注入システムでも機能し、様々な階層 (*path operations 関数*、依存性 (依存可能性)、サブ依存性など) で `BackgroundTasks` 型のパラメーターを宣言できます。 +`BackgroundTasks` の使用は依存性注入システムでも機能し、様々な階層 (*path operation function*、依存性 (dependable)、サブ依存性など) で `BackgroundTasks` 型のパラメーターを宣言できます。 -**FastAPI** は、それぞれの場合の処理​​方法と同じオブジェクトの再利用方法を知っているため、すべてのバックグラウンドタスクがマージされ、バックグラウンドで後で実行されます。 +**FastAPI** は、それぞれの場合の処理​​方法と同じオブジェクトの再利用方法を知っているため、すべてのバックグラウンドタスクがマージされ、バックグラウンドで後で実行されます: + + +{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} -{* ../../docs_src/background_tasks/tutorial002.py hl[13,15,22,25] *} この例では、レスポンスが送信された *後* にメッセージが `log.txt` ファイルに書き込まれます。 リクエストにクエリがあった場合、バックグラウンドタスクでログに書き込まれます。 -そして、*path operations 関数* で生成された別のバックグラウンドタスクは、`email` パスパラメータを使用してメッセージを書き込みます。 +そして、*path operation function* で生成された別のバックグラウンドタスクは、`email` パスパラメータを使用してメッセージを書き込みます。 -## 技術的な詳細 +## 技術的な詳細 { #technical-details } `BackgroundTasks` クラスは、`starlette.background`から直接取得されます。 これは、FastAPI に直接インポート/インクルードされるため、`fastapi` からインポートできる上に、`starlette.background`から別の `BackgroundTask` (末尾に `s` がない) を誤ってインポートすることを回避できます。 -`BackgroundTasks`のみを使用することで (`BackgroundTask` ではなく)、`Request` オブジェクトを直接使用する場合と同様に、それを *path operations 関数* パラメーターとして使用し、**FastAPI** に残りの処理を任せることができます。 +`BackgroundTasks`のみを使用することで (`BackgroundTask` ではなく)、`Request` オブジェクトを直接使用する場合と同様に、それを *path operation function* パラメーターとして使用し、**FastAPI** に残りの処理を任せることができます。 それでも、FastAPI で `BackgroundTask` を単独で使用することは可能ですが、コード内でオブジェクトを作成し、それを含むStarlette `Response` を返す必要があります。 -詳細については、バックグラウンドタスクに関する Starlette の公式ドキュメントを参照して下さい。 +詳細については、Starlette のバックグラウンドタスクに関する公式ドキュメントを参照して下さい。 -## 警告 +## 注意 { #caveat } -大量のバックグラウンド計算が必要であり、必ずしも同じプロセスで実行する必要がない場合 (たとえば、メモリや変数などを共有する必要がない場合)、Celery のようなより大きな他のツールを使用するとメリットがあるかもしれません。 +大量のバックグラウンド計算が必要であり、必ずしも同じプロセスで実行する必要がない場合 (たとえば、メモリや変数などを共有する必要がない場合)、Celery のようなより大きな他のツールを使用するとメリットがあるかもしれません。 これらは、より複雑な構成、RabbitMQ や Redis などのメッセージ/ジョブキューマネージャーを必要とする傾向がありますが、複数のプロセス、特に複数のサーバーでバックグラウンドタスクを実行できます。 ただし、同じ **FastAPI** アプリから変数とオブジェクトにアクセスする必要がある場合、または小さなバックグラウンドタスク (電子メール通知の送信など) を実行する必要がある場合は、単に `BackgroundTasks` を使用できます。 -## まとめ +## まとめ { #recap } -`BackgroundTasks` をインポートして、*path operations 関数* や依存関係のパラメータに `BackgroundTasks`を使用し、バックグラウンドタスクを追加して下さい。 +*path operation functions* と依存性のパラメータで `BackgroundTasks`をインポートして使用し、バックグラウンドタスクを追加して下さい。 diff --git a/docs/ja/docs/tutorial/body-fields.md b/docs/ja/docs/tutorial/body-fields.md index ce5630351..234c99d52 100644 --- a/docs/ja/docs/tutorial/body-fields.md +++ b/docs/ja/docs/tutorial/body-fields.md @@ -1,12 +1,13 @@ -# ボディ - フィールド +# ボディ - フィールド { #body-fields } `Query`や`Path`、`Body`を使って *path operation関数* のパラメータに追加のバリデーションやメタデータを宣言するのと同じように、Pydanticの`Field`を使ってPydanticモデルの内部でバリデーションやメタデータを宣言することができます。 -## `Field`のインポート +## `Field`のインポート { #import-field } まず、以下のようにインポートします: -{* ../../docs_src/body_fields/tutorial001.py hl[4] *} +{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[4] *} + /// warning | 注意 @@ -14,11 +15,11 @@ /// -## モデルの属性の宣言 +## モデルの属性の宣言 { #declare-model-attributes } 以下のように`Field`をモデルの属性として使用することができます: -{* ../../docs_src/body_fields/tutorial001.py hl[11,12,13,14] *} +{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[11:14] *} `Field`は`Query`や`Path`、`Body`と同じように動作し、全く同様のパラメータなどを持ちます。 @@ -40,13 +41,20 @@ /// -## 追加情報の追加 +## 追加情報の追加 { #add-extra-information } 追加情報は`Field`や`Query`、`Body`などで宣言することができます。そしてそれは生成されたJSONスキーマに含まれます。 後に例を用いて宣言を学ぶ際に、追加情報を追加する方法を学べます。 -## まとめ +/// warning | 注意 + +`Field`に渡された追加のキーは、結果として生成されるアプリケーションのOpenAPIスキーマにも含まれます。 +これらのキーは必ずしもOpenAPI仕様の一部であるとは限らないため、例えば[OpenAPI validator](https://validator.swagger.io/)などの一部のOpenAPIツールは、生成されたスキーマでは動作しない場合があります。 + +/// + +## まとめ { #recap } Pydanticの`Field`を使用して、モデルの属性に追加のバリデーションやメタデータを宣言することができます。 diff --git a/docs/ja/docs/tutorial/body-multiple-params.md b/docs/ja/docs/tutorial/body-multiple-params.md index cbfdda4b2..4ce77cc0d 100644 --- a/docs/ja/docs/tutorial/body-multiple-params.md +++ b/docs/ja/docs/tutorial/body-multiple-params.md @@ -1,24 +1,24 @@ -# ボディ - 複数のパラメータ +# ボディ - 複数のパラメータ { #body-multiple-parameters } -これまで`Path`と`Query`をどう使うかを見てきましたが、リクエストボディの宣言のより高度な使い方を見てみましょう。 +これまで`Path`と`Query`をどう使うかを見てきましたが、リクエストボディ宣言のより高度な使い方を見てみましょう。 -## `Path`、`Query`とボディパラメータを混ぜる +## `Path`、`Query`とボディパラメータを混ぜる { #mix-path-query-and-body-parameters } -まず、もちろん、`Path`と`Query`とリクエストボディのパラメータの宣言は自由に混ぜることができ、 **FastAPI** は何をするべきかを知っています。 +まず、もちろん、`Path`と`Query`とリクエストボディのパラメータ宣言は自由に混ぜることができ、 **FastAPI** は何をするべきかを知っています。 -また、デフォルトの`None`を設定することで、ボディパラメータをオプションとして宣言することもできます: +また、デフォルトを`None`に設定することで、ボディパラメータをオプションとして宣言することもできます: -{* ../../docs_src/body_multiple_params/tutorial001.py hl[19,20,21] *} +{* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} /// note | 備考 -この場合、ボディから取得する`item`はオプションであることに注意してください。デフォルト値は`None`です。 +この場合、ボディから取得する`item`はオプションであることに注意してください。デフォルト値が`None`になっているためです。 /// -## 複数のボディパラメータ +## 複数のボディパラメータ { #multiple-body-parameters } -上述の例では、*path operations*は`item`の属性を持つ以下のようなJSONボディを期待していました: +上述の例では、*path operations*は`Item`の属性を持つ以下のようなJSONボディを期待していました: ```JSON { @@ -31,11 +31,12 @@ しかし、`item`と`user`のように複数のボディパラメータを宣言することもできます: -{* ../../docs_src/body_multiple_params/tutorial002.py hl[22] *} +{* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *} -この場合、**FastAPI**は関数内に複数のボディパラメータ(Pydanticモデルである2つのパラメータ)があることに気付きます。 -そのため、パラメータ名をボディのキー(フィールド名)として使用し、以下のようなボディを期待しています: +この場合、**FastAPI**は関数内に複数のボディパラメータがあることに気付きます(Pydanticモデルである2つのパラメータがあります)。 + +そのため、パラメータ名をボディのキー(フィールド名)として使用し、以下のようなボディを期待します: ```JSON { @@ -62,7 +63,7 @@ 複合データの検証を行い、OpenAPIスキーマや自動ドキュメントのように文書化してくれます。 -## ボディ内の単数値 +## ボディ内の単数値 { #singular-values-in-body } クエリとパスパラメータの追加データを定義するための `Query` と `Path` があるのと同じように、 **FastAPI** は同等の `Body` を提供します。 @@ -72,12 +73,11 @@ しかし、`Body`を使用して、**FastAPI** に別のボディキーとして扱うように指示することができます: +{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} -{* ../../docs_src/body_multiple_params/tutorial003.py hl[23] *} この場合、**FastAPI** は以下のようなボディを期待します: - ```JSON { "item": { @@ -96,41 +96,48 @@ 繰り返しになりますが、データ型の変換、検証、文書化などを行います。 -## 複数のボディパラメータとクエリ +## 複数のボディパラメータとクエリ { #multiple-body-params-and-query } もちろん、ボディパラメータに加えて、必要に応じて追加のクエリパラメータを宣言することもできます。 -デフォルトでは、単数値はクエリパラメータとして解釈されるので、明示的に `Query` を追加する必要はありません。 +デフォルトでは、単数値はクエリパラメータとして解釈されるので、明示的に `Query` を追加する必要はなく、次のようにできます: ```Python -q: str = None +q: str | None = None ``` -以下において: +またはPython 3.10以上では: -{* ../../docs_src/body_multiple_params/tutorial004.py hl[27] *} +```Python +q: Union[str, None] = None +``` + +例えば: + +{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} /// info | 情報 -`Body`もまた、後述する `Query` や `Path` などと同様に、すべての検証パラメータとメタデータパラメータを持っています。 +`Body`もまた、後述する `Query` や `Path` などと同様に、すべての追加検証パラメータとメタデータパラメータを持っています。 /// -## 単一のボディパラメータの埋め込み +## 単一のボディパラメータの埋め込み { #embed-a-single-body-parameter } -Pydanticモデル`Item`のボディパラメータ`item`を1つだけ持っているとしましょう。 +Pydanticモデル`Item`の単一の`item`ボディパラメータしかないとしましょう。 デフォルトでは、**FastAPI**はそのボディを直接期待します。 -しかし、追加のボディパラメータを宣言したときのように、キー `item` を持つ JSON とその中のモデルの内容を期待したい場合は、特別な `Body` パラメータ `embed` を使うことができます: +しかし、追加のボディパラメータを宣言したときのように、キー `item` を持つ JSON と、その中のモデル内容を期待したい場合は、特別な `Body` パラメータ `embed` を使うことができます: ```Python -item: Item = Body(..., embed=True) +item: Item = Body(embed=True) ``` 以下において: -{* ../../docs_src/body_multiple_params/tutorial005.py hl[17] *} +{* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} + この場合、**FastAPI** は以下のようなボディを期待します: @@ -156,9 +163,9 @@ item: Item = Body(..., embed=True) } ``` -## まとめ +## まとめ { #recap } -リクエストが単一のボディしか持てない場合でも、*path operation関数*に複数のボディパラメータを追加することができます。 +リクエストが単一のボディしか持てない場合でも、*path operation function*に複数のボディパラメータを追加することができます。 しかし、**FastAPI** はそれを処理し、関数内の正しいデータを与え、*path operation*内の正しいスキーマを検証し、文書化します。 diff --git a/docs/ja/docs/tutorial/body-nested-models.md b/docs/ja/docs/tutorial/body-nested-models.md index a1680d10f..24eb30208 100644 --- a/docs/ja/docs/tutorial/body-nested-models.md +++ b/docs/ja/docs/tutorial/body-nested-models.md @@ -1,36 +1,26 @@ -# ボディ - ネストされたモデル +# ボディ - ネストされたモデル { #body-nested-models } **FastAPI** を使用すると、深くネストされた任意のモデルを定義、検証、文書化、使用することができます(Pydanticのおかげです)。 -## リストのフィールド +## リストのフィールド { #list-fields } -属性をサブタイプとして定義することができます。例えば、Pythonの`list`は以下のように定義できます: +属性をサブタイプとして定義することができます。例えば、Pythonの`list`: -{* ../../docs_src/body_nested_models/tutorial001.py hl[12] *} +{* ../../docs_src/body_nested_models/tutorial001_py310.py hl[12] *} -これにより、各項目の型は宣言されていませんが、`tags`はある項目のリストになります。 +これにより、各項目の型は宣言されていませんが、`tags`はリストになります。 -## タイプパラメータを持つリストのフィールド +## タイプパラメータを持つリストのフィールド { #list-fields-with-type-parameter } -しかし、Pythonには型や「タイプパラメータ」を使ってリストを宣言する方法があります: +しかし、Pythonには内部の型、または「タイプパラメータ」を使ってリストを宣言するための特定の方法があります: -### typingの`List`をインポート +### タイプパラメータを持つ`list`の宣言 { #declare-a-list-with-a-type-parameter } -まず、Pythonの標準の`typing`モジュールから`List`をインポートします: - -{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} - -### タイプパラメータを持つ`List`の宣言 - -`list`や`dict`、`tuple`のようなタイプパラメータ(内部の型)を持つ型を宣言するには: - -* `typing`モジュールからそれらをインストールします。 -* 角括弧(`[`と`]`)を使って「タイプパラメータ」として内部の型を渡します: +`list`、`dict`、`tuple`のようにタイプパラメータ(内部の型)を持つ型を宣言するには、 +角括弧(`[`と`]`)を使って内部の型を「タイプパラメータ」として渡します。 ```Python -from typing import List - -my_list: List[str] +my_list: list[str] ``` 型宣言の標準的なPythonの構文はこれだけです。 @@ -39,17 +29,17 @@ my_list: List[str] そのため、以下の例では`tags`を具体的な「文字列のリスト」にすることができます: -{* ../../docs_src/body_nested_models/tutorial002.py hl[14] *} +{* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} -## セット型 +## セット型 { #set-types } しかし、よく考えてみると、タグは繰り返すべきではなく、おそらくユニークな文字列になるのではないかと気付いたとします。 そして、Pythonにはユニークな項目のセットのための特別なデータ型`set`があります。 -そのため、以下のように、`Set`をインポートして`str`の`set`として`tags`を宣言することができます: +そして、`tags`を文字列のセットとして宣言できます: -{* ../../docs_src/body_nested_models/tutorial003.py hl[1,14] *} +{* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} これを使えば、データが重複しているリクエストを受けた場合でも、ユニークな項目のセットに変換されます。 @@ -57,27 +47,27 @@ my_list: List[str] また、それに応じて注釈をつけたり、文書化したりします。 -## ネストされたモデル +## ネストされたモデル { #nested-models } Pydanticモデルの各属性には型があります。 しかし、その型はそれ自体が別のPydanticモデルである可能性があります。 -そのため、特定の属性名、型、バリデーションを指定して、深くネストしたJSON`object`を宣言することができます。 +そのため、特定の属性名、型、バリデーションを指定して、深くネストしたJSON「オブジェクト」を宣言することができます。 すべては、任意のネストにされています。 -### サブモデルの定義 +### サブモデルの定義 { #define-a-submodel } 例えば、`Image`モデルを定義することができます: -{* ../../docs_src/body_nested_models/tutorial004.py hl[9,10,11] *} +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} -### サブモデルを型として使用 +### サブモデルを型として使用 { #use-the-submodel-as-a-type } そして、それを属性の型として使用することができます: -{* ../../docs_src/body_nested_models/tutorial004.py hl[20] *} +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[18] *} これは **FastAPI** が以下のようなボディを期待することを意味します: @@ -102,23 +92,23 @@ Pydanticモデルの各属性には型があります。 * データの検証 * 自動文書化 -## 特殊な型とバリデーション +## 特殊な型とバリデーション { #special-types-and-validation } -`str`や`int`、`float`のような通常の単数型の他にも、`str`を継承したより複雑な単数型を使うこともできます。 +`str`や`int`、`float`などの通常の単数型の他にも、`str`を継承したより複雑な単数型を使うこともできます。 -すべてのオプションをみるには、Pydanticのエキゾチック な型のドキュメントを確認してください。次の章でいくつかの例をみることができます。 +すべてのオプションをみるには、Pydanticの型の概要を確認してください。次の章でいくつかの例をみることができます。 -例えば、`Image`モデルのように`url`フィールドがある場合、`str`の代わりにPydanticの`HttpUrl`を指定することができます: +例えば、`Image`モデルのように`url`フィールドがある場合、`str`の代わりにPydanticの`HttpUrl`のインスタンスとして宣言することができます: -{* ../../docs_src/body_nested_models/tutorial005.py hl[4,10] *} +{* ../../docs_src/body_nested_models/tutorial005_py310.py hl[2,8] *} -文字列は有効なURLであることが確認され、そのようにJSONスキーマ・OpenAPIで文書化されます。 +文字列は有効なURLであることが確認され、そのようにJSON Schema / OpenAPIで文書化されます。 -## サブモデルのリストを持つ属性 +## サブモデルのリストを持つ属性 { #attributes-with-lists-of-submodels } Pydanticモデルを`list`や`set`などのサブタイプとして使用することもできます: -{* ../../docs_src/body_nested_models/tutorial006.py hl[20] *} +{* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *} これは、次のようなJSONボディを期待します(変換、検証、ドキュメントなど): @@ -152,59 +142,59 @@ Pydanticモデルを`list`や`set`などのサブタイプとして使用する /// -## 深くネストされたモデル +## 深くネストされたモデル { #deeply-nested-models } 深くネストされた任意のモデルを定義することができます: -{* ../../docs_src/body_nested_models/tutorial007.py hl[9,14,20,23,27] *} +{* ../../docs_src/body_nested_models/tutorial007_py310.py hl[7,12,18,21,25] *} /// info | 情報 -`Offer`は`Item`のリストであり、オプションの`Image`のリストを持っていることに注目してください。 +`Offer`は`Item`のリストであり、それらがさらにオプションの`Image`のリストを持っていることに注目してください。 /// -## 純粋なリストのボディ +## 純粋なリストのボディ { #bodies-of-pure-lists } 期待するJSONボディのトップレベルの値がJSON`array`(Pythonの`list`)であれば、Pydanticモデルと同じように、関数のパラメータで型を宣言することができます: ```Python -images: List[Image] +images: list[Image] ``` 以下のように: -{* ../../docs_src/body_nested_models/tutorial008.py hl[15] *} +{* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *} -## あらゆる場所でのエディタサポート +## あらゆる場所でのエディタサポート { #editor-support-everywhere } -エディタのサポートもどこでも受けることができます。 +そして、あらゆる場所でエディタサポートを得られます。 以下のようにリストの中の項目でも: - + Pydanticモデルではなく、`dict`を直接使用している場合はこのようなエディタのサポートは得られません。 -しかし、それらについて心配する必要はありません。入力された辞書は自動的に変換され、出力も自動的にJSONに変換されます。 +しかし、それらについて心配する必要はありません。入力されたdictは自動的に変換され、出力も自動的にJSONに変換されます。 -## 任意の`dict`のボディ +## 任意の`dict`のボディ { #bodies-of-arbitrary-dicts } また、ある型のキーと別の型の値を持つ`dict`としてボディを宣言することもできます。 -有効なフィールド・属性名を事前に知る必要がありません(Pydanticモデルの場合のように)。 +この方法で、有効なフィールド/属性名を事前に知る必要がありません(Pydanticモデルの場合のように)。 -これは、まだ知らないキーを受け取りたいときに便利だと思います。 +これは、まだ知らないキーを受け取りたいときに便利です。 --- -他にも、`int`のように他の型のキーを持ちたい場合などに便利です。 +もうひとつ便利なケースは、別の型(例: `int`)のキーを持ちたい場合です。 -それをここで見ていきましょう。 +それをここで見ていきます。 この場合、`int`のキーと`float`の値を持つものであれば、どんな`dict`でも受け入れることができます: -{* ../../docs_src/body_nested_models/tutorial009.py hl[15] *} +{* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *} /// tip | 豆知識 @@ -218,14 +208,14 @@ JSONはキーとして`str`しかサポートしていないことに注意し /// -## まとめ +## まとめ { #recap } **FastAPI** を使用すると、Pydanticモデルが提供する最大限の柔軟性を持ちながら、コードをシンプルに短く、エレガントに保つことができます。 -以下のような利点があります: +しかし、以下のような利点があります: * エディタのサポート(どこでも補完!) -* データ変換(別名:構文解析・シリアライズ) +* データ変換(別名:構文解析 / シリアライズ) * データの検証 * スキーマ文書 -* 自動文書化 +* 自動ドキュメント diff --git a/docs/ja/docs/tutorial/body-updates.md b/docs/ja/docs/tutorial/body-updates.md index ffbe52e1d..e888d5a0d 100644 --- a/docs/ja/docs/tutorial/body-updates.md +++ b/docs/ja/docs/tutorial/body-updates.md @@ -1,16 +1,16 @@ -# ボディ - 更新 +# ボディ - 更新 { #body-updates } -## `PUT`による置換での更新 +## `PUT`による置換での更新 { #update-replacing-with-put } 項目を更新するにはHTTPの`PUT`操作を使用することができます。 -`jsonable_encoder`を用いて、入力データをJSON形式で保存できるデータに変換することができます(例:NoSQLデータベース)。例えば、`datetime`を`str`に変換します。 +`jsonable_encoder`を用いて、入力データをJSONとして保存できるデータに変換することができます(例:NoSQLデータベース)。例えば、`datetime`を`str`に変換します。 -{* ../../docs_src/body_updates/tutorial001.py hl[30,31,32,33,34,35] *} +{* ../../docs_src/body_updates/tutorial001_py310.py hl[28:33] *} -既存のデータを置き換えるべきデータを受け取るために`PUT`は使用されます。 +`PUT`は、既存のデータを置き換えるべきデータを受け取るために使用されます。 -### 置換についての注意 +### 置換についての注意 { #warning-about-replacing } つまり、`PUT`を使用して以下のボディで項目`bar`を更新したい場合は: @@ -22,11 +22,11 @@ } ``` -すでに格納されている属性`"tax": 20.2`を含まないため、入力モデルのデフォルト値は`"tax": 10.5`です。 +すでに格納されている属性`"tax": 20.2`を含まないため、入力モデルは`"tax": 10.5`のデフォルト値を取ります。 そして、データはその「新しい」`10.5`の`tax`と共に保存されます。 -## `PATCH`による部分的な更新 +## `PATCH`による部分的な更新 { #partial-updates-with-patch } また、HTTPの`PATCH`操作でデータを*部分的に*更新することもできます。 @@ -44,27 +44,27 @@ /// -### Pydanticの`exclude_unset`パラメータの使用 +### Pydanticの`exclude_unset`パラメータの使用 { #using-pydantics-exclude-unset-parameter } -部分的な更新を受け取りたい場合は、Pydanticモデルの`.dict()`の`exclude_unset`パラメータを使用すると非常に便利です。 +部分的な更新を受け取りたい場合は、Pydanticモデルの`.model_dump()`の`exclude_unset`パラメータを使用すると非常に便利です。 -`item.dict(exclude_unset=True)`のように。 +`item.model_dump(exclude_unset=True)`のように。 これにより、`item`モデルの作成時に設定されたデータのみを持つ`dict`が生成され、デフォルト値は除外されます。 これを使うことで、デフォルト値を省略して、設定された(リクエストで送られた)データのみを含む`dict`を生成することができます: -{* ../../docs_src/body_updates/tutorial002.py hl[34] *} +{* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} -### Pydanticの`update`パラメータ +### Pydanticの`update`パラメータの使用 { #using-pydantics-update-parameter } -ここで、`.copy()`を用いて既存のモデルのコピーを作成し、`update`パラメータに更新するデータを含む`dict`を渡すことができます。 +ここで、`.model_copy()`を用いて既存のモデルのコピーを作成し、`update`パラメータに更新するデータを含む`dict`を渡すことができます。 -`stored_item_model.copy(update=update_data)`のように: +`stored_item_model.model_copy(update=update_data)`のように: -{* ../../docs_src/body_updates/tutorial002.py hl[35] *} +{* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} -### 部分的更新のまとめ +### 部分的更新のまとめ { #partial-updates-recap } まとめると、部分的な更新を適用するには、次のようにします: @@ -75,11 +75,11 @@ * この方法では、モデル内のデフォルト値ですでに保存されている値を上書きするのではなく、ユーザーが実際に設定した値のみを更新することができます。 * 保存されているモデルのコピーを作成し、受け取った部分的な更新で属性を更新します(`update`パラメータを使用します)。 * コピーしたモデルをDBに保存できるものに変換します(例えば、`jsonable_encoder`を使用します)。 - * これはモデルの`.dict()`メソッドを再度利用することに匹敵しますが、値をJSONに変換できるデータ型、例えば`datetime`を`str`に変換します。 + * これはモデルの`.model_dump()`メソッドを再度利用することに匹敵しますが、値をJSONに変換できるデータ型になるようにし(変換し)、例えば`datetime`を`str`に変換します。 * データをDBに保存します。 * 更新されたモデルを返します。 -{* ../../docs_src/body_updates/tutorial002.py hl[30,31,32,33,34,35,36,37] *} +{* ../../docs_src/body_updates/tutorial002_py310.py hl[28:35] *} /// tip | 豆知識 diff --git a/docs/ja/docs/tutorial/body.md b/docs/ja/docs/tutorial/body.md index 1298eec7e..a219faed0 100644 --- a/docs/ja/docs/tutorial/body.md +++ b/docs/ja/docs/tutorial/body.md @@ -1,40 +1,41 @@ -# リクエストボディ +# リクエストボディ { #request-body } -クライアント (ブラウザなど) からAPIにデータを送信する必要があるとき、データを **リクエストボディ (request body)** として送ります。 +クライアント(例えばブラウザ)からAPIにデータを送信する必要がある場合、**リクエストボディ**として送信します。 -**リクエスト** ボディはクライアントによってAPIへ送られます。**レスポンス** ボディはAPIがクライアントに送るデータです。 +**リクエスト**ボディは、クライアントからAPIへ送信されるデータです。**レスポンス**ボディは、APIがクライアントに送信するデータです。 -APIはほとんどの場合 **レスポンス** ボディを送らなければなりません。しかし、クライアントは必ずしも **リクエスト** ボディを送らなければいけないわけではありません。 +APIはほとんどの場合 **レスポンス** ボディを送信する必要があります。しかしクライアントは、常に **リクエストボディ** を送信する必要があるとは限りません。場合によっては、クエリパラメータ付きのパスだけをリクエストして、ボディを送信しないこともあります。 -**リクエスト** ボディを宣言するために Pydantic モデルを使用します。そして、その全てのパワーとメリットを利用します。 +**リクエスト**ボディを宣言するには、Pydantic モデルを使用し、その強力な機能とメリットをすべて利用します。 /// info | 情報 -データを送るには、`POST` (もっともよく使われる)、`PUT`、`DELETE` または `PATCH` を使うべきです。 +データを送信するには、`POST`(より一般的)、`PUT`、`DELETE`、`PATCH` のいずれかを使用すべきです。 -GET リクエストでボディを送信することは、仕様では未定義の動作ですが、FastAPI でサポートされており、非常に複雑な(極端な)ユースケースにのみ対応しています。 +`GET` リクエストでボディを送信することは仕様上は未定義の動作ですが、それでもFastAPIではサポートされています。ただし、非常に複雑/極端なユースケースのためだけです。 -非推奨なので、Swagger UIを使った対話型のドキュメントにはGETのボディ情報は表示されません。さらに、中継するプロキシが対応していない可能性があります。 +推奨されないため、Swagger UIによる対話的ドキュメントでは `GET` 使用時のボディのドキュメントは表示されず、途中のプロキシが対応していない可能性もあります。 /// -## Pydanticの `BaseModel` をインポート +## Pydanticの `BaseModel` をインポート { #import-pydantics-basemodel } -ます初めに、 `pydantic` から `BaseModel` をインポートする必要があります: +まず、`pydantic` から `BaseModel` をインポートする必要があります: -{* ../../docs_src/body/tutorial001.py hl[4] *} +{* ../../docs_src/body/tutorial001_py310.py hl[2] *} -## データモデルの作成 +## データモデルの作成 { #create-your-data-model } -そして、`BaseModel` を継承したクラスとしてデータモデルを宣言します。 +次に、`BaseModel` を継承するクラスとしてデータモデルを宣言します。 -すべての属性にpython標準の型を使用します: +すべての属性に標準のPython型を使用します: -{* ../../docs_src/body/tutorial001.py hl[7:11] *} +{* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} -クエリパラメータの宣言と同様に、モデル属性がデフォルト値をもつとき、必須な属性ではなくなります。それ以外は必須になります。オプショナルな属性にしたい場合は `None` を使用してください。 -例えば、上記のモデルは以下の様なJSON「`オブジェクト`」(もしくはPythonの `dict` ) を宣言しています: +クエリパラメータの宣言と同様に、モデル属性がデフォルト値を持つ場合は必須ではありません。そうでなければ必須です。単にオプションにするには `None` を使用してください。 + +例えば、上記のモデルは次のようなJSON「`object`」(またはPythonの `dict`)を宣言します: ```JSON { @@ -45,7 +46,7 @@ GET リクエストでボディを送信することは、仕様では未定義 } ``` -...`description` と `tax` はオプショナル (デフォルト値は `None`) なので、以下のJSON「`オブジェクト`」も有効です: +...`description` と `tax` はオプション(デフォルト値が `None`)なので、このJSON「`object`」も有効です: ```JSON { @@ -54,109 +55,112 @@ GET リクエストでボディを送信することは、仕様では未定義 } ``` -## パラメータとして宣言 +## パラメータとして宣言 { #declare-it-as-a-parameter } -*パスオペレーション* に加えるために、パスパラメータやクエリパラメータと同じ様に宣言します: +*path operation* に追加するには、パスパラメータやクエリパラメータを宣言したのと同じ方法で宣言します: -{* ../../docs_src/body/tutorial001.py hl[18] *} +{* ../../docs_src/body/tutorial001_py310.py hl[16] *} -...そして、作成したモデル `Item` で型を宣言します。 +...そして、作成したモデル `Item` を型として宣言します。 -## 結果 +## 結果 { #results } -そのPythonの型宣言だけで **FastAPI** は以下のことを行います: +そのPythonの型宣言だけで **FastAPI** は以下を行います: -* リクエストボディをJSONとして読み取ります。 -* 適当な型に変換します(必要な場合)。 +* リクエストのボディをJSONとして読み取ります。 +* 対応する型に変換します(必要な場合)。 * データを検証します。 - * データが無効な場合は、明確なエラーが返され、どこが不正なデータであったかを示します。 -* 受け取ったデータをパラメータ `item` に変換します。 - * 関数内で `Item` 型であると宣言したので、すべての属性とその型に対するエディタサポート(補完など)をすべて使用できます。 -* モデルのJSONスキーマ定義を生成し、好きな場所で使用することができます。 -* これらのスキーマは、生成されたOpenAPIスキーマの一部となり、自動ドキュメントのUIに使用されます。 + * データが無効な場合は、どこで何が不正なデータだったのかを正確に示す、分かりやすい明確なエラーを返します。 +* 受け取ったデータをパラメータ `item` に渡します。 + * 関数内で `Item` 型として宣言したため、すべての属性とその型について、エディタサポート(補完など)も利用できます。 +* モデル向けの JSON Schema 定義を生成します。プロジェクトにとって意味があるなら、他の場所でも好きなように利用できます。 +* それらのスキーマは生成されるOpenAPIスキーマの一部となり、自動ドキュメントの UIs で使用されます。 -## 自動ドキュメント生成 +## 自動ドキュメント { #automatic-docs } -モデルのJSONスキーマはOpenAPIで生成されたスキーマの一部になり、対話的なAPIドキュメントに表示されます: +モデルのJSON Schemaは、OpenAPIで生成されたスキーマの一部になり、対話的なAPIドキュメントに表示されます: -そして、それらが使われる *パスオペレーション* のそれぞれのAPIドキュメントにも表示されます: +また、それらが必要な各 *path operation* 内のAPIドキュメントでも使用されます: -## エディターサポート +## エディタサポート { #editor-support } -エディターによる型ヒントと補完が関数内で利用できます (Pydanticモデルではなく `dict` を受け取ると、同じサポートは受けられません): +エディタ上で、関数内のあらゆる場所で型ヒントと補完が得られます(Pydanticモデルの代わりに `dict` を受け取った場合は起きません): -型によるエラーチェックも可能です: +不正な型操作に対するエラーチェックも得られます: -これは偶然ではなく、このデザインに基づいてフレームワークが作られています。 +これは偶然ではなく、フレームワーク全体がその設計を中心に構築されています。 -全てのエディターで機能することを確認するために、実装前の設計時に徹底的にテストしました。 +そして、すべてのエディタで動作することを確実にするために、実装前の設計フェーズで徹底的にテストされました。 -これをサポートするためにPydantic自体にもいくつかの変更がありました。 +これをサポートするために、Pydantic自体にもいくつかの変更が加えられました。 -上記のスクリーンショットはVisual Studio Codeを撮ったものです。 +前述のスクリーンショットは Visual Studio Code で撮影されたものです。 -しかし、PyCharmやほとんどのPythonエディタでも同様なエディターサポートを受けられます: +ただし、PyCharm や、他のほとんどのPythonエディタでも同じエディタサポートを得られます: /// tip | 豆知識 -PyCharmエディタを使用している場合は、Pydantic PyCharm Pluginが使用可能です。 +エディタとして PyCharm を使用している場合、Pydantic PyCharm Plugin を使用できます。 -以下のエディターサポートが強化されます: +以下により、Pydanticモデルに対するエディタサポートが改善されます: -* 自動補完 -* 型チェック -* リファクタリング -* 検索 -* インスペクション +* auto-completion +* type checks +* refactoring +* searching +* inspections /// -## モデルの使用 +## モデルを使用する { #use-the-model } -関数内部で、モデルの全ての属性に直接アクセスできます: +関数内では、モデルオブジェクトのすべての属性に直接アクセスできます: -{* ../../docs_src/body/tutorial002.py hl[21] *} +{* ../../docs_src/body/tutorial002_py310.py *} -## リクエストボディ + パスパラメータ +## リクエストボディ + パスパラメータ { #request-body-path-parameters } パスパラメータとリクエストボディを同時に宣言できます。 -**FastAPI** はパスパラメータである関数パラメータは**パスから受け取り**、Pydanticモデルによって宣言された関数パラメータは**リクエストボディから受け取る**ということを認識します。 +**FastAPI** は、パスパラメータに一致する関数パラメータは **パスから取得** し、Pydanticモデルとして宣言された関数パラメータは **リクエストボディから取得** すべきだと認識します。 -{* ../../docs_src/body/tutorial003.py hl[17:18] *} +{* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} -## リクエストボディ + パスパラメータ + クエリパラメータ -また、**ボディ**と**パス**と**クエリ**のパラメータも同時に宣言できます。 +## リクエストボディ + パス + クエリパラメータ { #request-body-path-query-parameters } -**FastAPI** はそれぞれを認識し、適切な場所からデータを取得します。 +**body**、**path**、**query** パラメータもすべて同時に宣言できます。 -{* ../../docs_src/body/tutorial004.py hl[18] *} +**FastAPI** はそれぞれを認識し、正しい場所からデータを取得します。 -関数パラメータは以下の様に認識されます: +{* ../../docs_src/body/tutorial004_py310.py hl[16] *} -* パラメータが**パス**で宣言されている場合は、優先的にパスパラメータとして扱われます。 -* パラメータが**単数型** (`int`、`float`、`str`、`bool` など)の場合は**クエリ**パラメータとして解釈されます。 -* パラメータが **Pydantic モデル**型で宣言された場合、リクエスト**ボディ**として解釈されます。 +関数パラメータは以下のように認識されます: + +* パラメータが **path** でも宣言されている場合、パスパラメータとして使用されます。 +* パラメータが **単数型**(`int`、`float`、`str`、`bool` など)の場合、**query** パラメータとして解釈されます。 +* パラメータが **Pydanticモデル** の型として宣言されている場合、リクエスト **body** として解釈されます。 /// note | 備考 -FastAPIは、`= None`があるおかげで、`q`がオプショナルだとわかります。 +FastAPIは、デフォルト値 `= None` があるため、`q` の値が必須ではないことを認識します。 -`Optional[str]` の`Optional` はFastAPIでは使用されていません(FastAPIは`str`の部分のみ使用します)。しかし、`Optional[str]` はエディタがコードのエラーを見つけるのを助けてくれます。 +`str | None`(Python 3.10+)や `Union[str, None]`(Python 3.9+)の `Union` は、値が必須ではないことを判断するためにFastAPIでは使用されません。`= None` というデフォルト値があるため、必須ではないことを認識します。 + +しかし、型アノテーションを追加すると、エディタがより良いサポートを提供し、エラーを検出できるようになります。 /// -## Pydanticを使わない方法 +## Pydanticを使わない方法 { #without-pydantic } -もしPydanticモデルを使用したくない場合は、**Body**パラメータが利用できます。[Body - Multiple Parameters: Singular values in body](body-multiple-params.md#_2){.internal-link target=_blank}を確認してください。 +Pydanticモデルを使いたくない場合は、**Body** パラメータも使用できます。[Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank} のドキュメントを参照してください。 diff --git a/docs/ja/docs/tutorial/cookie-param-models.md b/docs/ja/docs/tutorial/cookie-param-models.md index 8285f44ef..10ffb2566 100644 --- a/docs/ja/docs/tutorial/cookie-param-models.md +++ b/docs/ja/docs/tutorial/cookie-param-models.md @@ -1,8 +1,8 @@ -# クッキーパラメータモデル +# クッキーパラメータモデル { #cookie-parameter-models } もし関連する**複数のクッキー**から成るグループがあるなら、それらを宣言するために、**Pydanticモデル**を作成できます。🍪 -こうすることで、**複数の場所**で**そのPydanticモデルを再利用**でき、バリデーションやメタデータを、すべてのクッキーパラメータに対して一度に宣言できます。😎 +こうすることで、**複数の場所**で**そのPydanticモデルを再利用**でき、バリデーションやメタデータを、すべてのパラメータに対して一度に宣言できます。😎 /// note | 備考 @@ -16,15 +16,15 @@ /// -## クッキーにPydanticモデルを使用する +## Pydanticモデルを使用したクッキー { #cookies-with-a-pydantic-model } -必要な複数の**クッキー**パラメータを**Pydanticモデル**で宣言し、さらに、それを `Cookie` として宣言しましょう: +必要な複数の**クッキー**パラメータを**Pydanticモデル**で宣言し、さらに、パラメータを `Cookie` として宣言しましょう: {* ../../docs_src/cookie_param_models/tutorial001_an_py310.py hl[9:12,16] *} -**FastAPI**は、リクエストの**クッキー**から**それぞれのフィールド**のデータを**抽出**し、定義された**Pydanticモデル**を提供します。 +**FastAPI**は、リクエストで受け取った**クッキー**から**それぞれのフィールド**のデータを**抽出**し、定義したPydanticモデルを提供します。 -## ドキュメントの確認 +## ドキュメントの確認 { #check-the-docs } 対話的APIドキュメントUI `/docs` で、定義されているクッキーを確認できます: @@ -32,18 +32,17 @@
-/// info | 備考 - +/// info | 情報 **ブラウザがクッキーを処理し**ていますが、特別な方法で内部的に処理を行っているために、**JavaScript**からは簡単に操作**できない**ことに留意してください。 -**対話的APIドキュメントUI** `/docs` にアクセスすれば、*パスオペレーション*に関するクッキーの**ドキュメンテーション**を確認できます。 +**APIドキュメントUI** `/docs` にアクセスすれば、*path operation*に関するクッキーの**ドキュメンテーション**を確認できます。 -しかし、たとえ**クッキーデータを入力して**「Execute」をクリックしても、対話的APIドキュメントUIは**JavaScript**で動作しているためクッキーは送信されず、まるで値を入力しなかったかのような**エラー**メッセージが表示されます。 +しかし、たとえ**データを入力して**「Execute」をクリックしても、ドキュメントUIは**JavaScript**で動作しているためクッキーは送信されず、まるで値を入力しなかったかのような**エラー**メッセージが表示されます。 /// -## 余分なクッキーを禁止する +## 余分なクッキーを禁止する { #forbid-extra-cookies } 特定の(あまり一般的ではないかもしれない)ケースで、受け付けるクッキーを**制限**する必要があるかもしれません。 @@ -51,7 +50,7 @@ Pydanticのモデルの Configuration を利用して、 `extra` フィールドを `forbid` とすることができます。 -{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} +{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *} もしクライアントが**余分なクッキー**を送ろうとすると、**エラー**レスポンスが返されます。 @@ -72,6 +71,6 @@ Pydanticのモデルの Configuration を利用して、 `extra` フィールド } ``` -## まとめ +## まとめ { #summary } **FastAPI**では、**クッキー**を宣言するために、**Pydanticモデル**を使用できます。😎 diff --git a/docs/ja/docs/tutorial/cookie-params.md b/docs/ja/docs/tutorial/cookie-params.md index 13af6d3c7..1e5a0d3cf 100644 --- a/docs/ja/docs/tutorial/cookie-params.md +++ b/docs/ja/docs/tutorial/cookie-params.md @@ -1,20 +1,20 @@ -# クッキーのパラメータ +# クッキーのパラメータ { #cookie-parameters } クッキーのパラメータは、`Query`や`Path`のパラメータを定義するのと同じ方法で定義できます。 -## `Cookie`をインポート +## `Cookie`をインポート { #import-cookie } まず、`Cookie`をインポートします: -{* ../../docs_src/cookie_params/tutorial001.py hl[3] *} +{* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[3] *} -## `Cookie`のパラメータを宣言 +## `Cookie`のパラメータを宣言 { #declare-cookie-parameters } 次に、`Path`や`Query`と同じ構造を使ってクッキーのパラメータを宣言します。 最初の値がデフォルト値で、追加の検証パラメータや注釈パラメータをすべて渡すことができます: -{* ../../docs_src/cookie_params/tutorial001.py hl[9] *} +{* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[9] *} /// note | 技術詳細 @@ -30,6 +30,16 @@ /// -## まとめ +/// info | 情報 -クッキーは`Cookie`を使って宣言し、`Query`や`Path`と同じパターンを使用する。 +**ブラウザがクッキーを**特殊な方法で裏側で扱うため、**JavaScript** から簡単には触れられないことを念頭に置いてください。 + +`/docs` の **API docs UI** に移動すると、*path operation* のクッキーに関する **documentation** を確認できます。 + +しかし、データを **入力** して「Execute」をクリックしても、docs UI は **JavaScript** で動作するためクッキーは送信されず、値を何も書かなかったかのような **error** メッセージが表示されます。 + +/// + +## まとめ { #recap } + +クッキーは`Cookie`を使って宣言し、`Query`や`Path`と同じ共通のパターンを使用する。 diff --git a/docs/ja/docs/tutorial/cors.md b/docs/ja/docs/tutorial/cors.md index f7bd59b70..a1dfe8e62 100644 --- a/docs/ja/docs/tutorial/cors.md +++ b/docs/ja/docs/tutorial/cors.md @@ -1,8 +1,8 @@ -# CORS (オリジン間リソース共有) +# CORS (Cross-Origin Resource Sharing) { #cors-cross-origin-resource-sharing } -CORSまたは「オリジン間リソース共有」 は、ブラウザで実行されているフロントエンドにバックエンドと通信するJavaScriptコードがあり、そのバックエンドがフロントエンドとは異なる「オリジン」にある状況を指します。 +CORSまたは「Cross-Origin Resource Sharing」 は、ブラウザで実行されているフロントエンドにバックエンドと通信するJavaScriptコードがあり、そのバックエンドがフロントエンドとは異なる「オリジン」にある状況を指します。 -## オリジン +## オリジン { #origin } オリジンはプロトコル (`http`、`https`) とドメイン (`myapp.com`、`localhost`、`localhost.tiangolo.com`) とポート (`80`、`443`、`8080`) の組み合わせです。 @@ -14,25 +14,25 @@ すべて `localhost` であっても、異なるプロトコルやポートを使用するので、異なる「オリジン」です。 -## ステップ +## ステップ { #steps } そして、ブラウザ上で実行されているフロントエンド (`http://localhost:8080`) があり、そのJavaScriptが `http://localhost` で実行されているバックエンドと通信するとします。(ポートを指定していないので、ブラウザはデフォルトの`80`ポートを使用します) -次に、ブラウザはHTTPの `OPTIONS` リクエストをバックエンドに送信します。そして、バックエンドがこの異なるオリジン (`http://localhost:8080`) からの通信を許可する適切なヘッダーを送信すると、ブラウザはフロントエンドのJavaScriptにバックエンドへのリクエストを送信させます。 +次に、ブラウザはHTTPの `OPTIONS` リクエストを `:80` のバックエンドに送信します。そして、バックエンドがこの異なるオリジン (`http://localhost:8080`) からの通信を許可する適切なヘッダーを送信すると、`:8080` のブラウザはフロントエンドのJavaScriptに `:80` のバックエンドへのリクエストを送信させます。 -これを実現するには、バックエンドに「許可されたオリジン」のリストがなければなりません。 +これを実現するには、`:80` のバックエンドに「許可されたオリジン」のリストがなければなりません。 -この場合、フロントエンドを正しく機能させるには、そのリストに `http://localhost:8080` を含める必要があります。 +この場合、`:8080` のフロントエンドを正しく機能させるには、そのリストに `http://localhost:8080` を含める必要があります。 -## ワイルドカード +## ワイルドカード { #wildcards } -リストを `"*"` (ワイルドカード) と宣言して、すべてを許可することもできます。 +リストを `"*"` (「ワイルドカード」) と宣言して、すべてを許可することもできます。 -ただし、Bearer Tokenで使用されるような認証ヘッダーやCookieなどのクレデンシャル情報に関するものを除いて、特定の種類の通信のみが許可されます。 +ただし、クレデンシャル情報に関するもの、つまりCookie、Bearer Tokenで使用されるようなAuthorizationヘッダーなどを含むものは除外され、特定の種類の通信のみが許可されます。 したがって、すべてを正しく機能させるために、許可されたオリジンの明示的な指定をお勧めします。 -## `CORSMiddleware` の使用 +## `CORSMiddleware` の使用 { #use-corsmiddleware } **FastAPI** アプリケーションでは `CORSMiddleware` を使用して、CORSに関する設定ができます。 @@ -42,39 +42,43 @@ 以下も、バックエンドに許可させるかどうか指定できます: -* クレデンシャル情報 (認証ヘッダー、Cookieなど) 。 +* クレデンシャル情報 (Authorizationヘッダー、Cookieなど) 。 * 特定のHTTPメソッド (`POST`、`PUT`) またはワイルドカード `"*"` を使用してすべて許可。 * 特定のHTTPヘッダー、またはワイルドカード `"*"`を使用してすべて許可。 -{* ../../docs_src/cors/tutorial001.py hl[2,6:11,13:19] *} +{* ../../docs_src/cors/tutorial001_py39.py hl[2,6:11,13:19] *} -`CORSMiddleware` 実装のデフォルトのパラメータはCORSに関して制限を与えるものになっているので、ブラウザにドメインを跨いで特定のオリジン、メソッド、またはヘッダーを使用可能にするためには、それらを明示的に有効にする必要があります + +`CORSMiddleware` 実装で使用されるデフォルトのパラメータはデフォルトで制限が厳しいため、ブラウザがクロスドメインのコンテキストでそれらを使用できるようにするには、特定のオリジン、メソッド、またはヘッダーを明示的に有効にする必要があります。 以下の引数がサポートされています: * `allow_origins` - オリジン間リクエストを許可するオリジンのリスト。例えば、`['https://example.org', 'https://www.example.org']`。`['*']`を使用して任意のオリジンを許可できます。 * `allow_origin_regex` - オリジン間リクエストを許可するオリジンの正規表現文字列。例えば、`'https://.*\.example\.org'`。 * `allow_methods` - オリジン間リクエストで許可するHTTPメソッドのリスト。デフォルトは `['GET']` です。`['*']`を使用してすべての標準メソッドを許可できます。 -* `allow_headers` - オリジン間リクエストでサポートするHTTPリクエストヘッダーのリスト。デフォルトは `[]` です。`['*']`を使用して、すべてのヘッダーを許可できます。CORSリクエストでは、 `Accept` 、 `Accept-Language` 、 `Content-Language` 、 `Content-Type` ヘッダーが常に許可されます。 +* `allow_headers` - オリジン間リクエストでサポートするHTTPリクエストヘッダーのリスト。デフォルトは `[]` です。`['*']`を使用して、すべてのヘッダーを許可できます。シンプルなCORSリクエストでは、 `Accept` 、 `Accept-Language` 、 `Content-Language` 、 `Content-Type` ヘッダーが常に許可されます。 * `allow_credentials` - オリジン間リクエストでCookieをサポートする必要があることを示します。デフォルトは `False` です。 + + `allow_credentials` が `True` に設定されている場合、`allow_origins`、`allow_methods`、`allow_headers` のいずれも `['*']` に設定できません。これらはすべて明示的に指定する必要があります。 + * `expose_headers` - ブラウザからアクセスできるようにするレスポンスヘッダーを示します。デフォルトは `[]` です。 * `max_age` - ブラウザがCORSレスポンスをキャッシュする最大時間を秒単位で設定します。デフォルトは `600` です。 このミドルウェアは2種類のHTTPリクエストに応答します... -### CORSプリフライトリクエスト +### CORSプリフライトリクエスト { #cors-preflight-requests } これらは、 `Origin` ヘッダーと `Access-Control-Request-Method` ヘッダーを持つ `OPTIONS` リクエストです。 この場合、ミドルウェアはリクエストを横取りし、適切なCORSヘッダーと共に情報提供のために `200` または `400` のレスポンスを返します。 -### シンプルなリクエスト +### シンプルなリクエスト { #simple-requests } `Origin` ヘッダーのあるリクエスト。この場合、ミドルウェアは通常どおりリクエストに何もしないですが、レスポンスに適切なCORSヘッダーを加えます。 -## より詳しい情報 +## より詳しい情報 { #more-info } -CORSについてより詳しい情報は、Mozilla CORS documentation を参照して下さい。 +CORSについてより詳しい情報は、Mozilla CORS documentation を参照して下さい。 /// note | 技術詳細 diff --git a/docs/ja/docs/tutorial/debugging.md b/docs/ja/docs/tutorial/debugging.md index 6c29679ef..8fe5b2d5d 100644 --- a/docs/ja/docs/tutorial/debugging.md +++ b/docs/ja/docs/tutorial/debugging.md @@ -1,14 +1,14 @@ -# デバッグ +# デバッグ { #debugging } Visual Studio CodeやPyCharmなどを使用して、エディター上でデバッガーと連携できます。 -## `uvicorn` の実行 +## `uvicorn` を呼び出す { #call-uvicorn } FastAPIアプリケーション上で、`uvicorn` を直接インポートして実行します: -{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} -### `__name__ == "__main__"` について +### `__name__ == "__main__"` について { #about-name-main } `__name__ == "__main__"` の主な目的は、ファイルが次のコマンドで呼び出されたときに実行されるコードを用意することです: @@ -26,7 +26,7 @@ $ python myapp.py from myapp import app ``` -#### より詳しい説明 +#### より詳しい説明 { #more-details } ファイルの名前が `myapp.py` だとします。 @@ -62,7 +62,7 @@ from myapp import app # Some more code ``` -`myapp.py` 内の自動変数には、値が `"__main __"` の変数 `__name__` はありません。 +その場合、`myapp.py` 内の自動的に作成された変数 `__name__` は、値として `"__main__"` を持ちません。 したがって、以下の行: @@ -78,7 +78,7 @@ from myapp import app /// -## デバッガーでコードを実行 +## デバッガーでコードを実行 { #run-your-code-with-your-debugger } コードから直接Uvicornサーバーを実行しているため、デバッガーから直接Pythonプログラム (FastAPIアプリケーション) を呼び出せます。 diff --git a/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md index 80153529e..3cb1fe73d 100644 --- a/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md @@ -1,12 +1,12 @@ -# 依存関係としてのクラス +# 依存関係としてのクラス { #classes-as-dependencies } **依存性注入** システムを深く掘り下げる前に、先ほどの例をアップグレードしてみましょう。 -## 前の例の`dict` +## 前の例の`dict` { #a-dict-from-the-previous-example } 前の例では、依存関係("dependable")から`dict`を返していました: -{* ../../docs_src/dependencies/tutorial001.py hl[9] *} +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[9] *} しかし、*path operation関数*のパラメータ`commons`に`dict`が含まれています。 @@ -14,7 +14,7 @@ もっとうまくやれるはずです...。 -## 依存関係を作るもの +## 依存関係を作るもの { #what-makes-a-dependency } これまでは、依存関係が関数として宣言されているのを見てきました。 @@ -38,7 +38,7 @@ something(some_argument, some_keyword_argument="foo") これを「呼び出し可能」なものと呼びます。 -## 依存関係としてのクラス +## 依存関係としてのクラス { #classes-as-dependencies_1 } Pythonのクラスのインスタンスを作成する際に、同じ構文を使用していることに気づくかもしれません。 @@ -67,48 +67,66 @@ FastAPIが実際にチェックしているのは、それが「呼び出し可 それは、パラメータが全くない呼び出し可能なものにも適用されます。パラメータのない*path operation関数*と同じように。 -そこで、上で紹介した依存関係の`common_parameters`を`CommonQueryParams`クラスに変更します: +そこで、上で紹介した依存関係の"dependable" `common_parameters`を`CommonQueryParams`クラスに変更します: -{* ../../docs_src/dependencies/tutorial002.py hl[11,12,13,14,15] *} +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[11:15] *} クラスのインスタンスを作成するために使用される`__init__`メソッドに注目してください: -{* ../../docs_src/dependencies/tutorial002.py hl[12] *} +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[12] *} ...以前の`common_parameters`と同じパラメータを持っています: -{* ../../docs_src/dependencies/tutorial001.py hl[8] *} +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8] *} これらのパラメータは **FastAPI** が依存関係を「解決」するために使用するものです。 どちらの場合も以下を持っています: -* オプショナルの`q`クエリパラメータ。 -* `skip`クエリパラメータ、デフォルトは`0`。 -* `limit`クエリパラメータ、デフォルトは`100`。 +* `str`であるオプショナルの`q`クエリパラメータ。 +* デフォルトが`0`である`int`の`skip`クエリパラメータ。 +* デフォルトが`100`である`int`の`limit`クエリパラメータ。 どちらの場合も、データは変換され、検証され、OpenAPIスキーマなどで文書化されます。 -## 使用 +## 使用 { #use-it } これで、このクラスを使用して依存関係を宣言することができます。 -{* ../../docs_src/dependencies/tutorial002.py hl[19] *} +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[19] *} **FastAPI** は`CommonQueryParams`クラスを呼び出します。これにより、そのクラスの「インスタンス」が作成され、インスタンスはパラメータ`commons`として関数に渡されます。 -## 型注釈と`Depends` +## 型注釈と`Depends` { #type-annotation-vs-depends } 上のコードでは`CommonQueryParams`を2回書いていることに注目してください: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ 注釈なし + +/// tip | 豆知識 + +可能であれば`Annotated`バージョンを使用することを推奨します。 + +/// + ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` +//// + 以下にある最後の`CommonQueryParams`: ```Python -... = Depends(CommonQueryParams) +... Depends(CommonQueryParams) ``` ...は、**FastAPI** が依存関係を知るために実際に使用するものです。 @@ -119,55 +137,145 @@ commons: CommonQueryParams = Depends(CommonQueryParams) この場合、以下にある最初の`CommonQueryParams`: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, ... +``` + +//// + +//// tab | Python 3.9+ 注釈なし + +/// tip | 豆知識 + +可能であれば`Annotated`バージョンを使用することを推奨します。 + +/// + ```Python commons: CommonQueryParams ... ``` -...は **FastAPI** に対して特別な意味をもちません。FastAPIはデータ変換や検証などには使用しません(それらのためには`= Depends(CommonQueryParams)`を使用しています)。 +//// + +...は **FastAPI** に対して特別な意味をもちません。FastAPIはデータ変換や検証などには使用しません(それらのためには`Depends(CommonQueryParams)`を使用しています)。 実際には以下のように書けばいいだけです: +//// tab | Python 3.9+ + +```Python +commons: Annotated[Any, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ 注釈なし + +/// tip | 豆知識 + +可能であれば`Annotated`バージョンを使用することを推奨します。 + +/// + ```Python commons = Depends(CommonQueryParams) ``` +//// + 以下にあるように: -{* ../../docs_src/dependencies/tutorial003.py hl[19] *} +{* ../../docs_src/dependencies/tutorial003_an_py310.py hl[19] *} しかし、型を宣言することは推奨されています。そうすれば、エディタは`commons`のパラメータとして何が渡されるかを知ることができ、コードの補完や型チェックなどを行うのに役立ちます: - + -## ショートカット +## ショートカット { #shortcut } しかし、ここでは`CommonQueryParams`を2回書くというコードの繰り返しが発生していることがわかります: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ 注釈なし + +/// tip | 豆知識 + +可能であれば`Annotated`バージョンを使用することを推奨します。 + +/// + ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` +//// + 依存関係が、クラス自体のインスタンスを作成するために**FastAPI**が「呼び出す」*特定の*クラスである場合、**FastAPI** はこれらのケースのショートカットを提供しています。 それらの具体的なケースについては以下のようにします: 以下のように書く代わりに: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ 注釈なし + +/// tip | 豆知識 + +可能であれば`Annotated`バージョンを使用することを推奨します。 + +/// + ```Python commons: CommonQueryParams = Depends(CommonQueryParams) ``` +//// + ...以下のように書きます: +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends()] +``` + +//// + +//// tab | Python 3.9+ 注釈なし + +/// tip | 豆知識 + +可能であれば`Annotated`バージョンを使用することを推奨します。 + +/// + ```Python commons: CommonQueryParams = Depends() ``` +//// + パラメータの型として依存関係を宣言し、`Depends()`の中でパラメータを指定せず、`Depends()`をその関数のパラメータの「デフォルト」値(`=`のあとの値)として使用することで、`Depends(CommonQueryParams)`の中でクラス全体を*もう一度*書かなくてもよくなります。 同じ例では以下のようになります: -{* ../../docs_src/dependencies/tutorial004.py hl[19] *} +{* ../../docs_src/dependencies/tutorial004_an_py310.py hl[19] *} ...そして **FastAPI** は何をすべきか知っています。 diff --git a/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index 0fb15ae02..2051afc05 100644 --- a/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -1,57 +1,69 @@ -# path operationデコレータの依存関係 +# path operation デコレータの依存関係 { #dependencies-in-path-operation-decorators } -場合によっては*path operation関数*の中で依存関係の戻り値を本当に必要としないこともあります。 +場合によっては、*path operation 関数*の中で依存関係の戻り値を実際には必要としないことがあります。 -もしくは、依存関係が値を返さない場合もあります。 +または、依存関係が値を返さない場合もあります。 -しかし、それでも実行・解決する必要があります。 +しかし、それでも実行・解決される必要があります。 -このような場合、*path operation関数*のパラメータを`Depends`で宣言する代わりに、*path operation decorator*に`dependencies`の`list`を追加することができます。 +そのような場合、`Depends` で *path operation 関数* のパラメータを宣言する代わりに、*path operation デコレータ*に `dependencies` の `list` を追加できます。 -## *path operationデコレータ*への`dependencies`の追加 +## *path operation デコレータ*に`dependencies`を追加 { #add-dependencies-to-the-path-operation-decorator } -*path operationデコレータ*はオプショナルの引数`dependencies`を受け取ります。 +*path operation デコレータ*はオプション引数`dependencies`を受け取ります。 それは`Depends()`の`list`であるべきです: -{* ../../docs_src/dependencies/tutorial006.py hl[17] *} +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[19] *} -これらの依存関係は、通常の依存関係と同様に実行・解決されます。しかし、それらの値(何かを返す場合)は*path operation関数*には渡されません。 +これらの依存関係は、通常の依存関係と同様に実行・解決されます。しかし、それらの値(何かを返す場合)は*path operation 関数*には渡されません。 /// tip | 豆知識 -エディタによっては、未使用の関数パラメータをチェックしてエラーとして表示するものもあります。 +一部のエディタは、未使用の関数パラメータをチェックしてエラーとして表示します。 -`dependencies`を`path operationデコレータ`で使用することで、エディタやツールのエラーを回避しながら確実に実行することができます。 +これらの`dependencies`を*path operation デコレータ*で使用することで、エディタ/ツールのエラーを回避しつつ、確実に実行されるようにできます。 -また、コードの未使用のパラメータがあるのを見て、それが不要だと思ってしまうような新しい開発者の混乱を避けるのにも役立つかもしれません。 +また、コード内の未使用のパラメータを見た新しい開発者が、それを不要だと思って混乱するのを避ける助けにもなるかもしれません。 /// -## 依存関係のエラーと戻り値 +/// info | 情報 -通常使用している依存関係の*関数*と同じものを使用することができます。 +この例では、架空のカスタムヘッダー `X-Key` と `X-Token` を使用しています。 -### 依存関係の要件 +しかし実際のケースでセキュリティを実装する際は、統合された[Security utilities(次の章)](../security/index.md){.internal-link target=_blank}を使うことで、より多くの利点を得られます。 -これらはリクエストの要件(ヘッダのようなもの)やその他のサブ依存関係を宣言することができます: +/// -{* ../../docs_src/dependencies/tutorial006.py hl[6,11] *} +## 依存関係のエラーと戻り値 { #dependencies-errors-and-return-values } -### 例外の発生 +通常使用している依存関係の*関数*と同じものを使用できます。 -これらの依存関係は通常の依存関係と同じように、例外を`raise`発生させることができます: +### 依存関係の要件 { #dependency-requirements } -{* ../../docs_src/dependencies/tutorial006.py hl[8,13] *} +これらはリクエストの要件(ヘッダーのようなもの)やその他のサブ依存関係を宣言できます: -### 戻り値 +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[8,13] *} + +### 例外の発生 { #raise-exceptions } + +これらの依存関係は、通常の依存関係と同じように例外を`raise`できます: + +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[10,15] *} + +### 戻り値 { #return-values } そして、値を返すことも返さないこともできますが、値は使われません。 -つまり、すでにどこかで使っている通常の依存関係(値を返すもの)を再利用することができ、値は使われなくても依存関係は実行されます: +つまり、すでにどこかで使っている通常の依存関係(値を返すもの)を再利用でき、値は使われなくても依存関係は実行されます: -{* ../../docs_src/dependencies/tutorial006.py hl[9,14] *} +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[11,16] *} -## *path operations*のグループに対する依存関係 +## *path operation*のグループに対する依存関係 { #dependencies-for-a-group-of-path-operations } -後で、より大きなアプリケーションの構造([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank})について読む時に、おそらく複数のファイルを使用して、*path operations*のグループに対して単一の`dependencies`パラメータを宣言する方法を学ぶでしょう。 +後で、より大きなアプリケーションを(おそらく複数ファイルで)構造化する方法([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank})について読むときに、*path operation*のグループに対して単一の`dependencies`パラメータを宣言する方法を学びます。 + +## グローバル依存関係 { #global-dependencies } + +次に、`FastAPI`アプリケーション全体に依存関係を追加して、各*path operation*に適用する方法を見ていきます。 diff --git a/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md index 35a69de0d..8095114c3 100644 --- a/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md @@ -1,24 +1,12 @@ -# yieldを持つ依存関係 +# `yield`を持つ依存関係 { #dependencies-with-yield } -FastAPIは、いくつかの終了後の追加のステップを行う依存関係をサポートしています。 +FastAPIは、いくつかの終了後の追加のステップを行う依存関係をサポートしています。 -これを行うには、`return`の代わりに`yield`を使い、その後に追加のステップを書きます。 +これを行うには、`return`の代わりに`yield`を使い、その後に追加のステップ(コード)を書きます。 /// tip | 豆知識 -`yield`は必ず一度だけ使用するようにしてください。 - -/// - -/// info | 情報 - -これを動作させるには、**Python 3.7** 以上を使用するか、**Python 3.6** では"backports"をインストールする必要があります: - -``` -pip install async-exit-stack async-generator -``` - -これによりasync-exit-stackasync-generatorがインストールされます。 +`yield`は必ず依存関係ごとに1回だけ使用するようにしてください。 /// @@ -35,21 +23,21 @@ pip install async-exit-stack async-generator /// -## `yield`を持つデータベースの依存関係 +## `yield`を持つデータベースの依存関係 { #a-database-dependency-with-yield } 例えば、これを使ってデータベースセッションを作成し、終了後にそれを閉じることができます。 -レスポンスを送信する前に`yield`文を含む前のコードのみが実行されます。 +レスポンスを作成する前に、`yield`文より前のコード(および`yield`文を含む)が実行されます: -{* ../../docs_src/dependencies/tutorial007.py hl[2,3,4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[2:4] *} 生成された値は、*path operations*や他の依存関係に注入されるものです: -{* ../../docs_src/dependencies/tutorial007.py hl[4] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[4] *} -`yield`文に続くコードは、レスポンスが送信された後に実行されます: +`yield`文に続くコードは、レスポンスの後に実行されます: -{* ../../docs_src/dependencies/tutorial007.py hl[5,6] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[5:6] *} /// tip | 豆知識 @@ -59,27 +47,27 @@ pip install async-exit-stack async-generator /// -## `yield`と`try`を持つ依存関係 +## `yield`と`try`を持つ依存関係 { #a-dependency-with-yield-and-try } -`yield`を持つ依存関係で`try`ブロックを使用した場合、その依存関係を使用した際に発生した例外を受け取ることになります。 +`yield`を持つ依存関係で`try`ブロックを使用した場合、その依存関係を使用した際にスローされたあらゆる例外を受け取ることになります。 -例えば、途中のどこかの時点で、別の依存関係や*path operation*の中で、データベーストランザクションを「ロールバック」したり、その他のエラーを作成したりするコードがあった場合、依存関係の中で例外を受け取ることになります。 +例えば、途中のどこかの時点で、別の依存関係や*path operation*の中で、データベーストランザクションを「ロールバック」したり、その他の例外を作成したりするコードがあった場合、依存関係の中で例外を受け取ることになります。 そのため、依存関係の中にある特定の例外を`except SomeException`で探すことができます。 同様に、`finally`を用いて例外があったかどうかにかかわらず、終了ステップを確実に実行することができます。 -{* ../../docs_src/dependencies/tutorial007.py hl[3,5] *} +{* ../../docs_src/dependencies/tutorial007_py39.py hl[3,5] *} -## `yield`を持つサブ依存関係 +## `yield`を持つサブ依存関係 { #sub-dependencies-with-yield } 任意の大きさや形のサブ依存関係やサブ依存関係の「ツリー」を持つことができ、その中で`yield`を使用することができます。 **FastAPI** は、`yield`を持つ各依存関係の「終了コード」が正しい順番で実行されていることを確認します。 -例えば、`dependency_c`は`dependency_b`と`dependency_b`に依存する`dependency_a`に、依存することができます: +例えば、`dependency_c`は`dependency_b`に、そして`dependency_b`は`dependency_a`に依存することができます: -{* ../../docs_src/dependencies/tutorial008.py hl[4,12,20] *} +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[6,14,22] *} そして、それらはすべて`yield`を使用することができます。 @@ -87,11 +75,11 @@ pip install async-exit-stack async-generator そして、`dependency_b`は`dependency_a`(ここでは`dep_a`という名前)の値を終了コードで利用できるようにする必要があります。 -{* ../../docs_src/dependencies/tutorial008.py hl[16,17,24,25] *} +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[18:19,26:27] *} -同様に、`yield`と`return`が混在した依存関係を持つこともできます。 +同様に、`yield`を持つ依存関係と`return`を持つ他の依存関係をいくつか持ち、それらの一部が他の一部に依存するようにもできます。 -また、単一の依存関係を持っていて、`yield`などの他の依存関係をいくつか必要とすることもできます。 +また、単一の依存関係を持っていて、`yield`を持つ他の依存関係をいくつか必要とすることもできます。 依存関係の組み合わせは自由です。 @@ -105,32 +93,46 @@ pip install async-exit-stack async-generator /// -## `yield`と`HTTPException`を持つ依存関係 +## `yield`と`HTTPException`を持つ依存関係 { #dependencies-with-yield-and-httpexception } -`yield`と例外をキャッチする`try`ブロックを持つことができる依存関係を使用することができることがわかりました。 +`yield`を持つ依存関係を使い、何らかのコードを実行し、その後に`finally`の後で終了コードを実行しようとする`try`ブロックを持てることが分かりました。 -`yield`の後の終了コードで`HTTPException`などを発生させたくなるかもしれません。しかし**それはうまくいきません** +また、`except`を使って発生した例外をキャッチし、それに対して何かをすることもできます。 -`yield`を持つ依存関係の終了コードは[例外ハンドラ](../handling-errors.md#_4){.internal-link target=_blank}の*後に*実行されます。依存関係によって投げられた例外を終了コード(`yield`の後)でキャッチするものはなにもありません。 - -つまり、`yield`の後に`HTTPException`を発生させた場合、`HTTTPException`をキャッチしてHTTP 400のレスポンスを返すデフォルトの(あるいは任意のカスタムの)例外ハンドラは、その例外をキャッチすることができなくなります。 - -これは、依存関係に設定されているもの(例えば、DBセッション)を、例えば、バックグラウンドタスクで使用できるようにするものです。 - -バックグラウンドタスクはレスポンスが送信された*後*に実行されます。そのため、*すでに送信されている*レスポンスを変更する方法すらないので、`HTTPException`を発生させる方法はありません。 - -しかし、バックグラウンドタスクがDBエラーを発生させた場合、少なくとも`yield`で依存関係のセッションをロールバックしたり、きれいに閉じたりすることができ、エラーをログに記録したり、リモートのトラッキングシステムに報告したりすることができます。 - -例外が発生する可能性があるコードがある場合は、最も普通の「Python流」なことをして、コードのその部分に`try`ブロックを追加してください。 - -レスポンスを返したり、レスポンスを変更したり、`HTTPException`を発生させたりする*前に*処理したいカスタム例外がある場合は、[カスタム例外ハンドラ](../handling-errors.md#_4){.internal-link target=_blank}を作成してください。 +例えば、`HTTPException`のように別の例外を発生させることができます。 /// tip | 豆知識 -`HTTPException`を含む例外は、`yield`の*前*でも発生させることができます。ただし、後ではできません。 +これはやや高度なテクニックで、ほとんどの場合は本当に必要にはなりません。例えば、*path operation 関数*など、アプリケーションコードの他の場所から(`HTTPException`を含む)例外を発生させられるためです。 + +ただし必要であれば使えます。 🤓 /// +{* ../../docs_src/dependencies/tutorial008b_an_py39.py hl[18:22,31] *} + +例外をキャッチして、それに基づいてカスタムレスポンスを作成したい場合は、[カスタム例外ハンドラ](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}を作成してください。 + +## `yield`と`except`を持つ依存関係 { #dependencies-with-yield-and-except } + +`yield`を持つ依存関係で`except`を使って例外をキャッチし、それを再度raiseしない(または新しい例外をraiseしない)場合、通常のPythonと同じように、FastAPIは例外があったことに気づけません: + +{* ../../docs_src/dependencies/tutorial008c_an_py39.py hl[15:16] *} + +この場合、(`HTTPException`やそれに類するものをraiseしていないため)クライアントには適切に*HTTP 500 Internal Server Error*レスポンスが返りますが、サーバーには**ログが一切残らず**、何がエラーだったのかを示す他の手がかりもありません。 😱 + +### `yield`と`except`を持つ依存関係では常に`raise`する { #always-raise-in-dependencies-with-yield-and-except } + +`yield`を持つ依存関係で例外をキャッチした場合、別の`HTTPException`などをraiseするのでない限り、**元の例外を再raiseすべきです**。 + +`raise`を使うと同じ例外を再raiseできます: + +{* ../../docs_src/dependencies/tutorial008d_an_py39.py hl[17] *} + +これでクライアントは同じ*HTTP 500 Internal Server Error*レスポンスを受け取りますが、サーバーのログにはカスタムの`InternalError`が残ります。 😎 + +## `yield`を持つ依存関係の実行 { #execution-of-dependencies-with-yield } + 実行の順序は多かれ少なかれ以下の図のようになります。時間は上から下へと流れていきます。そして、各列はコードを相互作用させたり、実行したりしている部分の一つです。 ```mermaid @@ -142,32 +144,29 @@ participant dep as Dep with yield participant operation as Path Operation participant tasks as Background tasks - Note over client,tasks: Can raise exception for dependency, handled after response is sent - Note over client,operation: Can raise HTTPException and can change the response + Note over client,operation: Can raise exceptions, including HTTPException client ->> dep: Start request Note over dep: Run code up to yield - opt raise - dep -->> handler: Raise HTTPException + opt raise Exception + dep -->> handler: Raise Exception handler -->> client: HTTP error response - dep -->> dep: Raise other exception end dep ->> operation: Run dependency, e.g. DB session opt raise - operation -->> handler: Raise HTTPException + 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 - operation -->> dep: Raise other exception 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 -->> dep: Raise other exception - end - Note over dep: After yield - opt Handle other exception - dep -->> dep: Handle exception, can't change response. E.g. close DB session. + tasks -->> tasks: Handle exceptions in the background task code end ``` @@ -181,15 +180,63 @@ participant tasks as Background tasks /// tip | 豆知識 -この図は`HTTPException`を示していますが、[カスタム例外ハンドラ](../handling-errors.md#_4){.internal-link target=_blank}を作成することで、他の例外を発生させることもできます。そして、その例外は依存関係の終了コードではなく、そのカスタム例外ハンドラによって処理されます。 - -しかし例外ハンドラで処理されない例外を発生させた場合は、依存関係の終了コードで処理されます。 +*path operation 関数*のコードで例外をraiseした場合、`HTTPException`を含め、それはyieldを持つ依存関係に渡されます。ほとんどの場合、その例外が正しく処理されるように、`yield`を持つ依存関係から同じ例外、または新しい例外を再raiseしたくなるでしょう。 /// -## コンテキストマネージャ +## 早期終了と`scope` { #early-exit-and-scope } -### 「コンテキストマネージャ」とは +通常、`yield`を持つ依存関係の終了コードは、クライアントに**レスポンスが送信された後**に実行されます。 + +しかし、*path operation 関数*からreturnした後に依存関係を使う必要がないと分かっている場合は、`Depends(scope="function")`を使って、**レスポンスが送信される前**に、*path operation 関数*のreturn後に依存関係を閉じるべきだとFastAPIに伝えられます。 + +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} + +`Depends()`は、以下のいずれかを取る`scope`パラメータを受け取ります: + +* `"function"`: リクエストを処理する*path operation 関数*の前に依存関係を開始し、*path operation 関数*の終了後に依存関係を終了しますが、クライアントにレスポンスが返される**前**に終了します。つまり、依存関係関数は*path operation 関数*の**周囲**で実行されます。 +* `"request"`: リクエストを処理する*path operation 関数*の前に依存関係を開始し(`"function"`を使用する場合と同様)、クライアントにレスポンスが返された**後**に終了します。つまり、依存関係関数は**リクエスト**とレスポンスのサイクルの**周囲**で実行されます。 + +指定されておらず、依存関係に`yield`がある場合、デフォルトで`scope`は`"request"`になります。 + +### サブ依存関係の`scope` { #scope-for-sub-dependencies } + +`scope="request"`(デフォルト)を持つ依存関係を宣言する場合、どのサブ依存関係も`"request"`の`scope`を持つ必要があります。 + +しかし、`"function"`の`scope`を持つ依存関係は、`"function"`と`"request"`の`scope`を持つ依存関係を持てます。 + +これは、いずれの依存関係も、サブ依存関係より前に終了コードを実行できる必要があるためです(終了コードの実行中にサブ依存関係をまだ使う必要がある可能性があるためです)。 + +```mermaid +sequenceDiagram + +participant client as Client +participant dep_req as Dep scope="request" +participant dep_func as Dep scope="function" +participant operation as Path Operation + + client ->> dep_req: Start request + Note over dep_req: Run code up to yield + dep_req ->> dep_func: Pass dependency + Note over dep_func: Run code up to yield + dep_func ->> operation: Run path operation with dependency + operation ->> dep_func: Return from path operation + Note over dep_func: Run code after yield + Note over dep_func: ✅ Dependency closed + dep_func ->> client: Send response to client + Note over client: Response sent + Note over dep_req: Run code after yield + Note over dep_req: ✅ Dependency closed +``` + +## `yield`、`HTTPException`、`except`、バックグラウンドタスクを持つ依存関係 { #dependencies-with-yield-httpexception-except-and-background-tasks } + +`yield`を持つ依存関係は、さまざまなユースケースをカバーし、いくつかの問題を修正するために、時間とともに進化してきました。 + +FastAPIの異なるバージョンで何が変わったのかを知りたい場合は、上級ガイドの[上級の依存関係 - `yield`、`HTTPException`、`except`、バックグラウンドタスクを持つ依存関係](../../advanced/advanced-dependencies.md#dependencies-with-yield-httpexception-except-and-background-tasks){.internal-link target=_blank}で詳しく読めます。 +## コンテキストマネージャ { #context-managers } + +### 「コンテキストマネージャ」とは { #what-are-context-managers } 「コンテキストマネージャ」とは、`with`文の中で使用できるPythonオブジェクトのことです。 @@ -205,9 +252,9 @@ with open("./somefile.txt") as f: `with`ブロックが終了すると、例外があったとしてもファイルを確かに閉じます。 -`yield`を依存関係を作成すると、**FastAPI** は内部的にそれをコンテキストマネージャに変換し、他の関連ツールと組み合わせます。 +`yield`を持つ依存関係を作成すると、**FastAPI** は内部的にそれをコンテキストマネージャに変換し、他の関連ツールと組み合わせます。 -### `yield`を持つ依存関係でのコンテキストマネージャの使用 +### `yield`を持つ依存関係でのコンテキストマネージャの使用 { #using-context-managers-in-dependencies-with-yield } /// warning | 注意 @@ -221,7 +268,7 @@ Pythonでは、依存性注入** システムを持っています。 +**FastAPI** は非常に強力でありながら直感的な **Dependency Injection** システムを持っています。 それは非常にシンプルに使用できるように設計されており、開発者が他のコンポーネント **FastAPI** と統合するのが非常に簡単になるように設計されています。 -## 「依存性注入」とは +## 「Dependency Injection」とは { #what-is-dependency-injection } -**「依存性注入」** とは、プログラミングにおいて、コード(この場合は、*path operation関数*)が動作したり使用したりするために必要なもの(「依存関係」)を宣言する方法があることを意味します: +**「Dependency Injection」** とは、プログラミングにおいて、コード(この場合は、*path operation 関数*)が動作したり使用したりするために必要なもの(「依存関係」)を宣言する方法があることを意味します: そして、そのシステム(この場合は、**FastAPI**)は、必要な依存関係をコードに提供するために必要なことは何でも行います(依存関係を「注入」します)。 @@ -19,27 +19,27 @@ これらすべてを、コードの繰り返しを最小限に抑えながら行います。 -## 最初のステップ +## 最初のステップ { #first-steps } 非常にシンプルな例を見てみましょう。あまりにもシンプルなので、今のところはあまり参考にならないでしょう。 -しかし、この方法では **依存性注入** システムがどのように機能するかに焦点を当てることができます。 +しかし、この方法では **Dependency Injection** システムがどのように機能するかに焦点を当てることができます。 -### 依存関係の作成 +### 依存関係(「dependable」)の作成 { #create-a-dependency-or-dependable } まずは依存関係に注目してみましょう。 -以下のように、*path operation関数*と同じパラメータを全て取ることができる関数にすぎません: +以下のように、*path operation 関数*と同じパラメータを全て取ることができる関数にすぎません: -{* ../../docs_src/dependencies/tutorial001.py hl[8,9] *} +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8:9] *} これだけです。 **2行**。 -そして、それはすべての*path operation関数*が持っているのと同じ形と構造を持っています。 +そして、それはすべての*path operation 関数*が持っているのと同じ形と構造を持っています。 -「デコレータ」を含まない(`@app.get("/some-path")`を含まない)*path operation関数*と考えることもできます。 +「デコレータ」を含まない(`@app.get("/some-path")`を含まない)*path operation 関数*と考えることもできます。 そして何でも返すことができます。 @@ -51,15 +51,25 @@ そして、これらの値を含む`dict`を返します。 -### `Depends`のインポート +/// info | 情報 -{* ../../docs_src/dependencies/tutorial001.py hl[3] *} +FastAPI はバージョン 0.95.0 で `Annotated` のサポートを追加し(そして推奨し始めました)。 -### "dependant"での依存関係の宣言 +古いバージョンを使用している場合、`Annotated` を使おうとするとエラーになります。 -*path operation関数*のパラメータに`Body`や`Query`などを使用するのと同じように、新しいパラメータに`Depends`を使用することができます: +`Annotated` を使用する前に、少なくとも 0.95.1 まで [FastAPI のバージョンをアップグレード](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} してください。 -{* ../../docs_src/dependencies/tutorial001.py hl[13,18] *} +/// + +### `Depends`のインポート { #import-depends } + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[3] *} + +### 「dependant」での依存関係の宣言 { #declare-the-dependency-in-the-dependant } + +*path operation 関数*のパラメータに`Body`や`Query`などを使用するのと同じように、新しいパラメータに`Depends`を使用することができます: + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[13,18] *} 関数のパラメータに`Depends`を使用するのは`Body`や`Query`などと同じですが、`Depends`の動作は少し異なります。 @@ -67,7 +77,9 @@ このパラメータは関数のようなものである必要があります。 -そして、その関数は、*path operation関数*が行うのと同じ方法でパラメータを取ります。 +直接**呼び出しません**(末尾に括弧を付けません)。`Depends()` のパラメータとして渡すだけです。 + +そして、その関数は、*path operation 関数*が行うのと同じ方法でパラメータを取ります。 /// tip | 豆知識 @@ -79,7 +91,7 @@ * 依存関係("dependable")関数を正しいパラメータで呼び出します。 * 関数の結果を取得します。 -* *path operation関数*のパラメータにその結果を代入してください。 +* *path operation 関数*のパラメータにその結果を代入してください。 ```mermaid graph TB @@ -92,7 +104,7 @@ common_parameters --> read_items common_parameters --> read_users ``` -この方法では、共有されるコードを一度書き、**FastAPI** が*path operations*のための呼び出しを行います。 +この方法では、共有されるコードを一度書き、**FastAPI** が*path operation*のための呼び出しを行います。 /// check | 確認 @@ -102,59 +114,85 @@ common_parameters --> read_users /// -## `async`にするかどうか +## `Annotated` 依存関係の共有 { #share-annotated-dependencies } -依存関係は **FastAPI**(*path operation関数*と同じ)からも呼び出されるため、関数を定義する際にも同じルールが適用されます。 +上の例では、ほんの少し **コードの重複** があることがわかります。 + +`common_parameters()` 依存関係を使う必要があるときは、型アノテーションと `Depends()` を含むパラメータ全体を書く必要があります: + +```Python +commons: Annotated[dict, Depends(common_parameters)] +``` + +しかし、`Annotated` を使用しているので、その `Annotated` 値を変数に格納して複数箇所で使えます: + +{* ../../docs_src/dependencies/tutorial001_02_an_py310.py hl[12,16,21] *} + +/// tip | 豆知識 + +これはただの標準 Python で、「type alias」と呼ばれ、**FastAPI** 固有のものではありません。 + +しかし **FastAPI** は `Annotated` を含む Python 標準に基づいているため、このテクニックをコードで使えます。 😎 + +/// + +依存関係は期待どおりに動作し続け、**一番良い点** は **型情報が保持される** ことです。つまり、エディタは **自動補完**、**インラインエラー** などを提供し続けられます。`mypy` のような他のツールでも同様です。 + +これは **大規模なコードベース** で、**同じ依存関係** を **多くの *path operation*** で何度も使う場合に特に役立ちます。 + +## `async`にするかどうか { #to-async-or-not-to-async } + +依存関係は **FastAPI**(*path operation 関数*と同じ)からも呼び出されるため、関数を定義する際にも同じルールが適用されます。 `async def`や通常の`def`を使用することができます。 -また、通常の`def`*path operation関数*の中に`async def`を入れて依存関係を宣言したり、`async def`*path operation関数*の中に`def`を入れて依存関係を宣言したりすることなどができます。 +また、通常の`def`*path operation 関数*の中に`async def`を入れて依存関係を宣言したり、`async def`*path operation 関数*の中に`def`を入れて依存関係を宣言したりすることなどができます。 それは重要ではありません。**FastAPI** は何をすべきかを知っています。 /// note | 備考 -わからない場合は、ドキュメントの[Async: *"In a hurry?"*](../../async.md){.internal-link target=_blank}の中の`async`と`await`についてのセクションを確認してください。 +わからない場合は、ドキュメントの[Async: *"In a hurry?"*](../../async.md#in-a-hurry){.internal-link target=_blank}の中の`async`と`await`についてのセクションを確認してください。 /// -## OpenAPIとの統合 +## OpenAPIとの統合 { #integrated-with-openapi } 依存関係(およびサブ依存関係)のすべてのリクエスト宣言、検証、および要件は、同じOpenAPIスキーマに統合されます。 つまり、対話型ドキュメントにはこれらの依存関係から得られる全ての情報も含まれているということです: - + -## 簡単な使い方 +## 簡単な使い方 { #simple-usage } -見てみると、*path*と*operation*が一致した時に*path operation関数*が宣言されていて、**FastAPI** が正しいパラメータで関数を呼び出してリクエストからデータを抽出する処理をしています。 +見てみると、*path*と*operation*が一致した時に*path operation 関数*が宣言されていて、**FastAPI** が正しいパラメータで関数を呼び出してリクエストからデータを抽出する処理をしています。 実は、すべての(あるいはほとんどの)Webフレームワークは、このように動作します。 これらの関数を直接呼び出すことはありません。これらの関数はフレームワーク(この場合は、**FastAPI**)によって呼び出されます。 -依存性注入システムでは、**FastAPI** に*path operation*もまた、*path operation関数*の前に実行されるべき他の何かに「依存」していることを伝えることができ、**FastAPI** がそれを実行し、結果を「注入」することを引き受けます。 +Dependency Injection システムでは、**FastAPI** に*path operation 関数*もまた、*path operation 関数*の前に実行されるべき他の何かに「依存」していることを伝えることができ、**FastAPI** がそれを実行し、結果を「注入」することを引き受けます。 -他にも、「依存性注入」と同じような考えの一般的な用語があります: +他にも、「dependency injection」と同じような考えの一般的な用語があります: -* リソース -* プロバイダ -* サービス -* インジェクタブル -* コンポーネント +* resources +* providers +* services +* injectables +* components -## **FastAPI** プラグイン +## **FastAPI** プラグイン { #fastapi-plug-ins } -統合や「プラグイン」は **依存性注入** システムを使って構築することができます。しかし、実際には、**「プラグイン」を作成する必要はありません**。依存関係を使用することで、無限の数の統合やインタラクションを宣言することができ、それが**path operation関数*で利用可能になるからです。 +統合や「プラグイン」は **Dependency Injection** システムを使って構築することができます。しかし、実際には、**「プラグイン」を作成する必要はありません**。依存関係を使用することで、無限の数の統合やインタラクションを宣言することができ、それが*path operation 関数*で利用可能になるからです。 依存関係は非常にシンプルで直感的な方法で作成することができ、必要なPythonパッケージをインポートするだけで、*文字通り*数行のコードでAPI関数と統合することができます。 次の章では、リレーショナルデータベースやNoSQLデータベース、セキュリティなどについて、その例を見ていきます。 -## **FastAPI** 互換性 +## **FastAPI** 互換性 { #fastapi-compatibility } -依存性注入システムがシンプルなので、**FastAPI** は以下のようなものと互換性があります: +dependency injection システムがシンプルなので、**FastAPI** は以下のようなものと互換性があります: * すべてのリレーショナルデータベース * NoSQLデータベース @@ -165,15 +203,15 @@ common_parameters --> read_users * レスポンスデータ注入システム * など。 -## シンプルでパワフル +## シンプルでパワフル { #simple-and-powerful } -階層依存性注入システムは、定義や使用方法が非常にシンプルであるにもかかわらず、非常に強力なものとなっています。 +階層的な dependency injection システムは、定義や使用方法が非常にシンプルであるにもかかわらず、非常に強力なものとなっています。 -依存関係事態を定義する依存関係を定義することができます。 +依存関係が、さらに依存関係を定義することもできます。 -最終的には、依存関係の階層ツリーが構築され、**依存性注入**システムが、これらの依存関係(およびそのサブ依存関係)をすべて解決し、各ステップで結果を提供(注入)します。 +最終的には、依存関係の階層ツリーが構築され、**Dependency Injection**システムが、これらの依存関係(およびそのサブ依存関係)をすべて解決し、各ステップで結果を提供(注入)します。 -例えば、4つのAPIエンドポイント(*path operations*)があるとします: +例えば、4つのAPIエンドポイント(*path operation*)があるとします: * `/items/public/` * `/items/private/` @@ -205,8 +243,8 @@ admin_user --> activate_user paying_user --> pro_items ``` -## **OpenAPI** との統合 +## **OpenAPI** との統合 { #integrated-with-openapi_1 } -これら全ての依存関係は、要件を宣言すると同時に、*path operations*にパラメータやバリデーションを追加します。 +これら全ての依存関係は、要件を宣言すると同時に、*path operation*にパラメータやバリデーションを追加します。 **FastAPI** はそれをすべてOpenAPIスキーマに追加して、対話型のドキュメントシステムに表示されるようにします。 diff --git a/docs/ja/docs/tutorial/dependencies/sub-dependencies.md b/docs/ja/docs/tutorial/dependencies/sub-dependencies.md index 211a86a0a..007c320f3 100644 --- a/docs/ja/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/ja/docs/tutorial/dependencies/sub-dependencies.md @@ -1,4 +1,4 @@ -# サブ依存関係 +# サブ依存関係 { #sub-dependencies } **サブ依存関係** を持つ依存関係を作成することができます。 @@ -6,21 +6,21 @@ **FastAPI** はそれらを解決してくれます。 -### 最初の依存関係「依存可能なもの」 +## 最初の依存関係「依存可能なもの」 { #first-dependency-dependable } 以下のような最初の依存関係(「依存可能なもの」)を作成することができます: -{* ../../docs_src/dependencies/tutorial005.py hl[8,9] *} +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[8:9] *} これはオプショナルのクエリパラメータ`q`を`str`として宣言し、それを返すだけです。 これは非常にシンプルです(あまり便利ではありません)が、サブ依存関係がどのように機能するかに焦点を当てるのに役立ちます。 -### 第二の依存関係 「依存可能なもの」と「依存」 +## 第二の依存関係 「依存可能なもの」と「依存」 { #second-dependency-dependable-and-dependant } そして、別の依存関数(「依存可能なもの」)を作成して、同時にそれ自身の依存関係を宣言することができます(つまりそれ自身も「依存」です): -{* ../../docs_src/dependencies/tutorial005.py hl[13] *} +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[13] *} 宣言されたパラメータに注目してみましょう: @@ -29,15 +29,15 @@ * また、オプショナルの`last_query`クッキーを`str`として宣言します。 * ユーザーがクエリ`q`を提供しなかった場合、クッキーに保存していた最後に使用したクエリを使用します。 -### 依存関係の使用 +## 依存関係の使用 { #use-the-dependency } 以下のように依存関係を使用することができます: -{* ../../docs_src/dependencies/tutorial005.py hl[21] *} +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[23] *} /// info | 情報 -*path operation関数*の中で宣言している依存関係は`query_or_cookie_extractor`の1つだけであることに注意してください。 +*path operation 関数*の中で宣言している依存関係は`query_or_cookie_extractor`の1つだけであることに注意してください。 しかし、**FastAPI** は`query_extractor`を最初に解決し、その結果を`query_or_cookie_extractor`を呼び出す時に渡す必要があることを知っています。 @@ -54,24 +54,43 @@ read_query["/items/"] query_extractor --> query_or_cookie_extractor --> read_query ``` -## 同じ依存関係の複数回の使用 +## 同じ依存関係の複数回の使用 { #using-the-same-dependency-multiple-times } -依存関係の1つが同じ*path operation*に対して複数回宣言されている場合、例えば、複数の依存関係が共通のサブ依存関係を持っている場合、**FastAPI** はリクエストごとに1回だけそのサブ依存関係を呼び出します。 +依存関係の1つが同じ*path operation*に対して複数回宣言されている場合、例えば、複数の依存関係が共通のサブ依存関係を持っている場合、**FastAPI** はリクエストごとに1回だけそのサブ依存関係を呼び出します。 -そして、返された値を「キャッシュ」に保存し、同じリクエストに対して依存関係を何度も呼び出す代わりに、特定のリクエストでそれを必要とする全ての「依存関係」に渡すことになります。 +そして、返された値を「キャッシュ」に保存し、同じリクエストに対して依存関係を何度も呼び出す代わりに、その特定のリクエストでそれを必要とする全ての「依存」に渡すことになります。 -高度なシナリオでは、「キャッシュされた」値を使うのではなく、同じリクエストの各ステップ(おそらく複数回)で依存関係を呼び出す必要があることがわかっている場合、`Depens`を使用する際に、`use_cache=False`というパラメータを設定することができます。 +高度なシナリオでは、「キャッシュされた」値を使うのではなく、同じリクエストの各ステップ(おそらく複数回)で依存関係を呼び出す必要があることがわかっている場合、`Depends`を使用する際に、`use_cache=False`というパラメータを設定することができます: + +//// tab | Python 3.9+ + +```Python hl_lines="1" +async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]): + return {"fresh_value": fresh_value} +``` + +//// + +//// tab | Python 3.9+ 非Annotated + +/// tip | 豆知識 + +可能であれば`Annotated`版を使うことを推奨します。 + +/// ```Python hl_lines="1" async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): return {"fresh_value": fresh_value} ``` -## まとめ +//// -ここで使われている派手な言葉は別にして、**依存性注入** システムは非常にシンプルです。 +## まとめ { #recap } -*path operation関数*と同じように見えるただの関数です。 +ここで使われている派手な言葉は別にして、**Dependency Injection** システムは非常にシンプルです。 + +*path operation 関数*と同じように見えるただの関数です。 しかし、それでも非常に強力で、任意の深くネストされた依存関係「グラフ」(ツリー)を宣言することができます。 diff --git a/docs/ja/docs/tutorial/encoder.md b/docs/ja/docs/tutorial/encoder.md index 309cf8857..33cc6ae48 100644 --- a/docs/ja/docs/tutorial/encoder.md +++ b/docs/ja/docs/tutorial/encoder.md @@ -1,16 +1,16 @@ -# JSON互換エンコーダ +# JSON互換エンコーダ { #json-compatible-encoder } -データ型(Pydanticモデルのような)をJSONと互換性のあるもの(`dict`や`list`など)に変更する必要がある場合があります。 +データ型(Pydanticモデルのような)をJSONと互換性のあるもの(`dict`や`list`など)に変換する必要があるケースがあります。 例えば、データベースに保存する必要がある場合です。 そのために、**FastAPI** は`jsonable_encoder()`関数を提供しています。 -## `jsonable_encoder`の使用 +## `jsonable_encoder`の使用 { #using-the-jsonable-encoder } JSON互換のデータのみを受信するデータベース`fake_db`があるとしましょう。 -例えば、`datetime`オブジェクトはJSONと互換性がないので、このデーターベースには受け取られません。 +例えば、`datetime`オブジェクトはJSONと互換性がないので、受け取られません。 そのため、`datetime`オブジェクトはISO形式のデータを含む`str`に変換されなければなりません。 @@ -20,7 +20,7 @@ JSON互換のデータのみを受信するデータベース`fake_db`がある Pydanticモデルのようなオブジェクトを受け取り、JSON互換版を返します: -{* ../../docs_src/encoder/tutorial001.py hl[5,22] *} +{* ../../docs_src/encoder/tutorial001_py310.py hl[4,21] *} この例では、Pydanticモデルを`dict`に、`datetime`を`str`に変換します。 diff --git a/docs/ja/docs/tutorial/extra-data-types.md b/docs/ja/docs/tutorial/extra-data-types.md index 1be1c3f92..4ed84e86f 100644 --- a/docs/ja/docs/tutorial/extra-data-types.md +++ b/docs/ja/docs/tutorial/extra-data-types.md @@ -1,6 +1,6 @@ -# 追加データ型 +# 追加データ型 { #extra-data-types } -今までは、以下のような一般的なデータ型を使用してきました: +今まで、以下のような一般的なデータ型を使用してきました: * `int` * `float` @@ -11,13 +11,13 @@ そして、今まで見てきたのと同じ機能を持つことになります: -* 素晴らしいエディタのサポート -* 受信したリクエストからのデータ変換 -* レスポンスデータのデータ変換 -* データの検証 -* 自動注釈と文書化 +* 素晴らしいエディタのサポート。 +* 受信したリクエストからのデータ変換。 +* レスポンスデータのデータ変換。 +* データの検証。 +* 自動注釈と文書化。 -## 他のデータ型 +## 他のデータ型 { #other-data-types } ここでは、使用できる追加のデータ型のいくつかを紹介します: @@ -26,17 +26,17 @@ * リクエストとレスポンスでは`str`として表現されます。 * `datetime.datetime`: * Pythonの`datetime.datetime`です。 - * リクエストとレスポンスはISO 8601形式の`str`で表現されます: `2008-09-15T15:53:00+05:00` + * リクエストとレスポンスはISO 8601形式の`str`で表現されます(例: `2008-09-15T15:53:00+05:00`)。 * `datetime.date`: - * Pythonの`datetime.date`です。 - * リクエストとレスポンスはISO 8601形式の`str`で表現されます: `2008-09-15` + * Python `datetime.date`。 + * リクエストとレスポンスはISO 8601形式の`str`で表現されます(例: `2008-09-15`)。 * `datetime.time`: - * Pythonの`datetime.time`. - * リクエストとレスポンスはISO 8601形式の`str`で表現されます: `14:23:55.003` + * Pythonの`datetime.time`。 + * リクエストとレスポンスはISO 8601形式の`str`で表現されます(例: `14:23:55.003`)。 * `datetime.timedelta`: * Pythonの`datetime.timedelta`です。 * リクエストとレスポンスでは合計秒数の`float`で表現されます。 - * Pydanticでは「ISO 8601 time diff encoding」として表現することも可能です。詳細はドキュメントを参照してください。 + * Pydanticでは「ISO 8601 time diff encoding」として表現することも可能です。詳細はドキュメントを参照してください。 * `frozenset`: * リクエストとレスポンスでは`set`と同じように扱われます: * リクエストでは、リストが読み込まれ、重複を排除して`set`に変換されます。 @@ -45,18 +45,18 @@ * `bytes`: * Pythonの標準的な`bytes`です。 * リクエストとレスポンスでは`str`として扱われます。 - * 生成されたスキーマは`str`で`binary`の「フォーマット」持つことを指定します。 + * 生成されたスキーマは`str`で`binary`の「フォーマット」を持つことを指定します。 * `Decimal`: * Pythonの標準的な`Decimal`です。 - * リクエストやレスポンスでは`float`と同じように扱います。 + * リクエストとレスポンスでは`float`と同じように扱われます。 +* Pydanticの全ての有効な型はこちらで確認できます: Pydantic data types。 -* Pydanticの全ての有効な型はこちらで確認できます: Pydantic data types。 -## 例 +## 例 { #example } ここでは、上記の型のいくつかを使用したパラメータを持つ*path operation*の例を示します。 -{* ../../docs_src/extra_data_types/tutorial001.py hl[1,2,12:16] *} +{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[1,3,12:16] *} -関数内のパラメータは自然なデータ型を持っていることに注意してください。そして、以下のように通常の日付操作を行うことができます: +関数内のパラメータは自然なデータ型を持っていることに注意してください。そして、例えば、以下のように通常の日付操作を行うことができます: -{* ../../docs_src/extra_data_types/tutorial001.py hl[18,19] *} +{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[18:19] *} diff --git a/docs/ja/docs/tutorial/extra-models.md b/docs/ja/docs/tutorial/extra-models.md index b7e215409..05e267818 100644 --- a/docs/ja/docs/tutorial/extra-models.md +++ b/docs/ja/docs/tutorial/extra-models.md @@ -1,6 +1,6 @@ -# モデル - より詳しく +# Extra Models { #extra-models } -先ほどの例に続き、複数の関連モデルを持つことが一般的です。 +先ほどの例に続き、複数の関連モデルを持つことは一般的です。 これはユーザーモデルの場合は特にそうです。なぜなら: @@ -8,27 +8,27 @@ * **出力モデル**はパスワードをもつべきではありません。 * **データベースモデル**はおそらくハッシュ化されたパスワードが必要になるでしょう。 -/// danger | 危険 +/// danger -ユーザーの平文のパスワードは絶対に保存しないでください。常に認証に利用可能な「安全なハッシュ」を保存してください。 +ユーザーの平文のパスワードは絶対に保存しないでください。常に検証できる「安全なハッシュ」を保存してください。 知らない方は、[セキュリティの章](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}で「パスワードハッシュ」とは何かを学ぶことができます。 /// -## 複数のモデル +## Multiple models { #multiple-models } ここでは、パスワードフィールドをもつモデルがどのように見えるのか、また、どこで使われるのか、大まかなイメージを紹介します: -{* ../../docs_src/extra_models/tutorial001.py hl[9,11,16,22,24,29:30,33:35,40:41] *} +{* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} -### `**user_in.dict()`について +### About `**user_in.model_dump()` { #about-user-in-model-dump } -#### Pydanticの`.dict()` +#### Pydanticの`.model_dump()` { #pydantics-model-dump } `user_in`は`UserIn`クラスのPydanticモデルです。 -Pydanticモデルには、モデルのデータを含む`dict`を返す`.dict()`メソッドがあります。 +Pydanticモデルには、モデルのデータを含む`dict`を返す`.model_dump()`メソッドがあります。 そこで、以下のようなPydanticオブジェクト`user_in`を作成すると: @@ -39,7 +39,7 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com そして呼び出すと: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() ``` これで変数`user_dict`のデータを持つ`dict`ができました。(これはPydanticモデルのオブジェクトの代わりに`dict`です)。 @@ -61,7 +61,7 @@ print(user_dict) } ``` -#### `dict`の展開 +#### `dict`の展開 { #unpacking-a-dict } `user_dict`のような`dict`を受け取り、それを`**user_dict`を持つ関数(またはクラス)に渡すと、Pythonはそれを「展開」します。これは`user_dict`のキーと値を直接キー・バリューの引数として渡します。 @@ -93,31 +93,31 @@ UserInDB( ) ``` -#### 別のモデルからつくるPydanticモデル +#### 別のモデルの内容からつくるPydanticモデル { #a-pydantic-model-from-the-contents-of-another } -上述の例では`user_in.dict()`から`user_dict`をこのコードのように取得していますが: +上述の例では`user_in.model_dump()`から`user_dict`をこのコードのように取得していますが: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() UserInDB(**user_dict) ``` これは以下と同等です: ```Python -UserInDB(**user_in.dict()) +UserInDB(**user_in.model_dump()) ``` -...なぜなら`user_in.dict()`は`dict`であり、`**`を付与して`UserInDB`を渡してPythonに「展開」させているからです。 +...なぜなら`user_in.model_dump()`は`dict`であり、`**`を付与して`UserInDB`を渡してPythonに「展開」させているからです。 そこで、別のPydanticモデルのデータからPydanticモデルを取得します。 -#### `dict`の展開と追加引数 +#### `dict`の展開と追加キーワード { #unpacking-a-dict-and-extra-keywords } そして、追加のキーワード引数`hashed_password=hashed_password`を以下のように追加すると: ```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) ``` ...以下のようになります: @@ -132,13 +132,13 @@ UserInDB( ) ``` -/// warning | 注意 +/// warning -サポートしている追加機能は、データの可能な流れをデモするだけであり、もちろん本当のセキュリティを提供しているわけではありません。 +追加のサポート関数`fake_password_hasher`と`fake_save_user`は、データの可能な流れをデモするだけであり、もちろん本当のセキュリティを提供しているわけではありません。 /// -## 重複の削減 +## Reduce duplication { #reduce-duplication } コードの重複を減らすことは、**FastAPI**の中核的なアイデアの1つです。 @@ -152,40 +152,60 @@ UserInDB( データの変換、検証、文書化などはすべて通常通りに動作します。 -このようにして、モデル間の違いだけを宣言することができます: +このようにして、モデル間の違いだけを宣言することができます(平文の`password`、`hashed_password`、パスワードなし): -{* ../../docs_src/extra_models/tutorial002.py hl[9,15,16,19,20,23,24] *} +{* ../../docs_src/extra_models/tutorial002_py310.py hl[7,13:14,17:18,21:22] *} -## `Union`または`anyOf` +## `Union` or `anyOf` { #union-or-anyof } -レスポンスを2つの型の`Union`として宣言することができます。 +レスポンスを2つ以上の型の`Union`として宣言できます。つまり、そのレスポンスはそれらのいずれかになります。 OpenAPIでは`anyOf`で定義されます。 そのためには、標準的なPythonの型ヒント`typing.Union`を使用します: -{* ../../docs_src/extra_models/tutorial003.py hl[1,14,15,18,19,20,33] *} +/// note | 備考 -## モデルのリスト +`Union`を定義する場合は、最も具体的な型を先に、その後により具体性の低い型を含めてください。以下の例では、より具体的な`PlaneItem`が`Union[PlaneItem, CarItem]`内で`CarItem`より前に来ています。 -同じように、オブジェクトのリストのレスポンスを宣言することができます。 +/// -そのためには、標準のPythonの`typing.List`を使用する: +{* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *} -{* ../../docs_src/extra_models/tutorial004.py hl[1,20] *} +### Python 3.10の`Union` { #union-in-python-3-10 } -## 任意の`dict`を持つレスポンス +この例では、引数`response_model`の値として`Union[PlaneItem, CarItem]`を渡しています。 + +**型アノテーション**に書くのではなく、**引数の値**として渡しているため、Python 3.10でも`Union`を使う必要があります。 + +型アノテーションであれば、次のように縦棒を使用できました: + +```Python +some_variable: PlaneItem | CarItem +``` + +しかし、これを代入で`response_model=PlaneItem | CarItem`のように書くと、Pythonはそれを型アノテーションとして解釈するのではなく、`PlaneItem`と`CarItem`の間で**無効な操作**を行おうとしてしまうため、エラーになります。 + +## List of models { #list-of-models } + +同じように、オブジェクトのリストのレスポンスを宣言できます。 + +そのためには、標準のPythonの`typing.List`(またはPython 3.9以降では単に`list`)を使用します: + +{* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *} + +## Response with arbitrary `dict` { #response-with-arbitrary-dict } また、Pydanticモデルを使用せずに、キーと値の型だけを定義した任意の`dict`を使ってレスポンスを宣言することもできます。 これは、有効なフィールド・属性名(Pydanticモデルに必要なもの)を事前に知らない場合に便利です。 -この場合、`typing.Dict`を使用することができます: +この場合、`typing.Dict`(またはPython 3.9以降では単に`dict`)を使用できます: -{* ../../docs_src/extra_models/tutorial005.py hl[1,8] *} +{* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *} -## まとめ +## Recap { #recap } 複数のPydanticモデルを使用し、ケースごとに自由に継承します。 -エンティティが異なる「状態」を持たなければならない場合は、エンティティごとに単一のデータモデルを持つ必要はありません。`password` や `password_hash` やパスワードなしなどのいくつかの「状態」をもつユーザー「エンティティ」の場合の様にすれば良いです。 +エンティティが異なる「状態」を持たなければならない場合は、エンティティごとに単一のデータモデルを持つ必要はありません。`password`、`password_hash`、パスワードなしを含む状態を持つユーザー「エンティティ」の場合と同様です。 diff --git a/docs/ja/docs/tutorial/first-steps.md b/docs/ja/docs/tutorial/first-steps.md index 77f9cba43..ecad2f6ff 100644 --- a/docs/ja/docs/tutorial/first-steps.md +++ b/docs/ja/docs/tutorial/first-steps.md @@ -1,8 +1,8 @@ -# 最初のステップ +# 最初のステップ { #first-steps } 最もシンプルなFastAPIファイルは以下のようになります: -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} これを`main.py`にコピーします。 @@ -11,27 +11,43 @@
```console -$ uvicorn main:app --reload +$ fastapi dev main.py -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: Waiting for application startup. -INFO: Application startup complete. + 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. ```
-/// note | 備考 - -`uvicorn main:app`は以下を示します: - -* `main`: `main.py`ファイル (Python "module")。 -* `app`: `main.py`内部で作られるobject(`app = FastAPI()`のように記述される)。 -* `--reload`: コードの変更時にサーバーを再起動させる。開発用。 - -/// - 出力には次のような行があります: ```hl_lines="4" @@ -40,7 +56,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) この行はローカルマシンでアプリが提供されているURLを示しています。 -### チェック +### チェック { #check-it } ブラウザでhttp://127.0.0.1:8000を開きます。 @@ -50,7 +66,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) {"message": "Hello World"} ``` -### 対話的APIドキュメント +### 対話的APIドキュメント { #interactive-api-docs } 次に、http://127.0.0.1:8000/docsにアクセスします。 @@ -58,7 +74,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### 他のAPIドキュメント +### 代替APIドキュメント { #alternative-api-docs } 次に、http://127.0.0.1:8000/redocにアクセスします。 @@ -66,31 +82,31 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -### OpenAPI +### OpenAPI { #openapi } **FastAPI**は、APIを定義するための**OpenAPI**標準規格を使用して、すべてのAPIの「スキーマ」を生成します。 -#### 「スキーマ」 +#### 「スキーマ」 { #schema } 「スキーマ」は定義または説明です。実装コードではなく、単なる抽象的な説明です。 -#### API「スキーマ」 +#### API「スキーマ」 { #api-schema } ここでは、OpenAPIはAPIのスキーマ定義の方法を規定する仕様です。 このスキーマ定義はAPIパス、受け取り可能なパラメータなどが含まれます。 -#### データ「スキーマ」 +#### データ「スキーマ」 { #data-schema } 「スキーマ」という用語は、JSONコンテンツなどの一部のデータの形状を指す場合もあります。 そのような場合、スキーマはJSON属性とそれらが持つデータ型などを意味します。 -#### OpenAPIおよびJSONスキーマ +#### OpenAPIおよびJSONスキーマ { #openapi-and-json-schema } OpenAPIはAPIのためのAPIスキーマを定義します。そして、そのスキーマは**JSONデータスキーマ**の標準規格に準拠したJSONスキーマを利用するAPIによって送受されるデータの定義(または「スキーマ」)を含んでいます。 -#### `openapi.json`を確認 +#### `openapi.json`を確認 { #check-the-openapi-json } 素のOpenAPIスキーマがどのようなものか興味がある場合、FastAPIはすべてのAPIの説明を含むJSON(スキーマ)を自動的に生成します。 @@ -100,7 +116,7 @@ OpenAPIはAPIのためのAPIスキーマを定義します。そして、その ```JSON { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" @@ -119,7 +135,7 @@ OpenAPIはAPIのためのAPIスキーマを定義します。そして、その ... ``` -#### OpenAPIの目的 +#### OpenAPIの目的 { #what-is-openapi-for } OpenAPIスキーマは、FastAPIに含まれている2つのインタラクティブなドキュメントシステムの動力源です。 @@ -127,11 +143,47 @@ OpenAPIスキーマは、FastAPIに含まれている2つのインタラクテ また、APIと通信するクライアント用のコードを自動的に生成するために使用することもできます。たとえば、フロントエンド、モバイル、またはIoTアプリケーションです。 -## ステップ毎の要約 +### アプリをデプロイ(任意) { #deploy-your-app-optional } -### Step 1: `FastAPI`をインポート +任意でFastAPIアプリをFastAPI Cloudにデプロイできます。まだなら、待機リストに登録してください。 🚀 -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +すでに**FastAPI Cloud**アカウントがある場合(待機リストから招待済みの場合😉)、1コマンドでアプリケーションをデプロイできます。 + +デプロイする前に、ログインしていることを確認してください: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +その後、アプリをデプロイします: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +以上です!これで、そのURLでアプリにアクセスできます。 ✨ + +## ステップ毎の要約 { #recap-step-by-step } + +### Step 1: `FastAPI`をインポート { #step-1-import-fastapi } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} `FastAPI`は、APIのすべての機能を提供するPythonクラスです。 @@ -143,44 +195,16 @@ OpenAPIスキーマは、FastAPIに含まれている2つのインタラクテ /// -### Step 2: `FastAPI`の「インスタンス」を生成 +### Step 2: `FastAPI`の「インスタンス」を生成 { #step-2-create-a-fastapi-instance } -{* ../../docs_src/first_steps/tutorial001.py hl[3] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[3] *} ここで、`app`変数が`FastAPI`クラスの「インスタンス」になります。 これが、すべてのAPIを作成するための主要なポイントになります。 -この`app`はコマンドで`uvicorn`が参照するものと同じです: +### Step 3: *path operation*を作成 { #step-3-create-a-path-operation } -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -以下のようなアプリを作成したとき: - -{* ../../docs_src/first_steps/tutorial002.py hl[3] *} - -そして、それを`main.py`ファイルに置き、次のように`uvicorn`を呼び出します: - -
- -```console -$ uvicorn main:my_awesome_api --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -### Step 3: *path operation*を作成 - -#### パス +#### パス { #path } ここでの「パス」とは、最初の`/`から始まるURLの最後の部分を指します。 @@ -204,7 +228,7 @@ https://example.com/items/foo APIを構築する際、「パス」は「関心事」と「リソース」を分離するための主要な方法です。 -#### Operation +#### Operation { #operation } ここでの「オペレーション」とは、HTTPの「メソッド」の1つを指します。 @@ -239,15 +263,16 @@ APIを構築するときは、通常、これらの特定のHTTPメソッドを 「**オペレーションズ**」とも呼ぶことにします。 -#### *パスオペレーションデコレータ*を定義 +#### *path operation デコレータ*を定義 { #define-a-path-operation-decorator } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *} -{* ../../docs_src/first_steps/tutorial001.py hl[6] *} `@app.get("/")`は直下の関数が下記のリクエストの処理を担当することを**FastAPI**に伝えます: * パス `/` * get オペレーション -/// info | `@decorator` について +/// info | `@decorator` Info Pythonにおける`@something`シンタックスはデコレータと呼ばれます。 @@ -255,9 +280,9 @@ Pythonにおける`@something`シンタックスはデコレータと呼ばれ 「デコレータ」は直下の関数を受け取り、それを使って何かを行います。 -私たちの場合、このデコレーターは直下の関数が**オペレーション** `get`を使用した**パス**` / `に対応することを**FastAPI** に通知します。 +私たちの場合、このデコレーターは直下の関数が**オペレーション** `get`を使用した**パス** `/`に対応することを**FastAPI** に通知します。 -これが「*パスオペレーションデコレータ*」です。 +これが「*path operation デコレータ*」です。 /// @@ -286,15 +311,15 @@ Pythonにおける`@something`シンタックスはデコレータと呼ばれ /// -### Step 4: **パスオペレーション**を定義 +### Step 4: **path operation 関数**を定義 { #step-4-define-the-path-operation-function } -以下は「**パスオペレーション関数**」です: +以下は「**path operation 関数**」です: * **パス**: は`/`です。 * **オペレーション**: は`get`です。 * **関数**: 「デコレータ」の直下にある関数 (`@app.get("/")`の直下) です。 -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} これは、Pythonの関数です。 @@ -306,28 +331,49 @@ Pythonにおける`@something`シンタックスはデコレータと呼ばれ `async def`の代わりに通常の関数として定義することもできます: -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note | 備考 -違いが分からない場合は、[Async: *"急いでいますか?"*](../async.md#_1){.internal-link target=_blank}を確認してください。 +違いが分からない場合は、[Async: *"急いでいますか?"*](../async.md#in-a-hurry){.internal-link target=_blank}を確認してください。 /// -### Step 5: コンテンツの返信 +### Step 5: コンテンツの返信 { #step-5-return-the-content } -{* ../../docs_src/first_steps/tutorial001.py hl[8] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *} -`dict`、`list`、`str`、`int`などを返すことができます。 +`dict`、`list`、`str`、`int`などの単一の値を返すことができます。 Pydanticモデルを返すこともできます(後で詳しく説明します)。 JSONに自動的に変換されるオブジェクトやモデルは他にもたくさんあります(ORMなど)。 お気に入りのものを使ってみてください。すでにサポートされている可能性が高いです。 -## まとめ +### Step 6: デプロイする { #step-6-deploy-it } -* `FastAPI`をインポート -* `app`インスタンスを生成 -* **パスオペレーションデコレータ**を記述 (`@app.get("/")`) -* **パスオペレーション関数**を定義 (上記の`def root(): ...`のように) -* 開発サーバーを起動 (`uvicorn main:app --reload`) +**FastAPI Cloud**に1コマンドでアプリをデプロイします: `fastapi deploy`. 🎉 + +#### FastAPI Cloudについて { #about-fastapi-cloud } + +**FastAPI Cloud**は、**FastAPI**の作者とそのチームによって開発されています。 + +最小限の労力でAPIの**構築**、**デプロイ**、**アクセス**を行うプロセスを合理化します。 + +FastAPIでアプリを構築するのと同じ**開発体験**を、クラウドへの**デプロイ**にもたらします。 🎉 + +FastAPI Cloudは、*FastAPI and friends*のオープンソースプロジェクトに対する主要スポンサーであり、資金提供元です。 ✨ + +#### 他のクラウドプロバイダにデプロイする { #deploy-to-other-cloud-providers } + +FastAPIはオープンソースで、標準に基づいています。選択した任意のクラウドプロバイダにFastAPIアプリをデプロイできます。 + +クラウドプロバイダのガイドに従って、FastAPIアプリをデプロイしてください。 🤓 + +## まとめ { #recap } + +* `FastAPI`をインポートします。 +* `app`インスタンスを生成します。 +* `@app.get("/")`のようなデコレータを使用して、**path operation デコレータ**を記述します。 +* **path operation 関数**を定義します。例: `def root(): ...`。 +* `fastapi dev`コマンドで開発サーバーを起動します。 +* 任意で`fastapi deploy`を使ってアプリをデプロイします。 diff --git a/docs/ja/docs/tutorial/handling-errors.md b/docs/ja/docs/tutorial/handling-errors.md index 8578ca335..945fe0777 100644 --- a/docs/ja/docs/tutorial/handling-errors.md +++ b/docs/ja/docs/tutorial/handling-errors.md @@ -1,4 +1,4 @@ -# エラーハンドリング +# エラーハンドリング { #handling-errors } APIを使用しているクライアントにエラーを通知する必要がある状況はたくさんあります。 @@ -19,15 +19,15 @@ APIを使用しているクライアントにエラーを通知する必要が **"404 Not Found"** のエラー(およびジョーク)を覚えていますか? -## `HTTPException`の使用 +## `HTTPException`の使用 { #use-httpexception } HTTPレスポンスをエラーでクライアントに返すには、`HTTPException`を使用します。 -### `HTTPException`のインポート +### `HTTPException`のインポート { #import-httpexception } -{* ../../docs_src/handling_errors/tutorial001.py hl[1] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[1] *} -### コード内での`HTTPException`の発生 +### コード内での`HTTPException`の発生 { #raise-an-httpexception-in-your-code } `HTTPException`は通常のPythonの例外であり、APIに関連するデータを追加したものです。 @@ -39,9 +39,9 @@ Pythonの例外なので、`return`ではなく、`raise`です。 この例では、クライアントが存在しないIDでアイテムを要求した場合、`404`のステータスコードを持つ例外を発生させます: -{* ../../docs_src/handling_errors/tutorial001.py hl[11] *} +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[11] *} -### レスポンス結果 +### レスポンス結果 { #the-resulting-response } クライアントが`http://example.com/items/foo`(`item_id` `"foo"`)をリクエストすると、HTTPステータスコードが200で、以下のJSONレスポンスが返されます: @@ -69,7 +69,7 @@ Pythonの例外なので、`return`ではなく、`raise`です。 /// -## カスタムヘッダーの追加 +## カスタムヘッダーの追加 { #add-custom-headers } 例えば、いくつかのタイプのセキュリティのために、HTTPエラーにカスタムヘッダを追加できると便利な状況がいくつかあります。 @@ -77,9 +77,9 @@ Pythonの例外なので、`return`ではなく、`raise`です。 しかし、高度なシナリオのために必要な場合には、カスタムヘッダーを追加することができます: -{* ../../docs_src/handling_errors/tutorial002.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *} -## カスタム例外ハンドラのインストール +## カスタム例外ハンドラのインストール { #install-custom-exception-handlers } カスタム例外ハンドラはStarletteと同じ例外ユーティリティを使用して追加することができます。 @@ -89,7 +89,7 @@ Pythonの例外なので、`return`ではなく、`raise`です。 カスタム例外ハンドラを`@app.exception_handler()`で追加することができます: -{* ../../docs_src/handling_errors/tutorial003.py hl[5,6,7,13,14,15,16,17,18,24] *} +{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *} ここで、`/unicorns/yolo`をリクエストすると、*path operation*は`UnicornException`を`raise`します。 @@ -109,7 +109,7 @@ Pythonの例外なので、`return`ではなく、`raise`です。 /// -## デフォルトの例外ハンドラのオーバーライド +## デフォルトの例外ハンドラのオーバーライド { #override-the-default-exception-handlers } **FastAPI** にはいくつかのデフォルトの例外ハンドラがあります。 @@ -117,7 +117,7 @@ Pythonの例外なので、`return`ではなく、`raise`です。 これらの例外ハンドラを独自のものでオーバーライドすることができます。 -### リクエスト検証の例外のオーバーライド +### リクエスト検証の例外のオーバーライド { #override-request-validation-exceptions } リクエストに無効なデータが含まれている場合、**FastAPI** は内部的に`RequestValidationError`を発生させます。 @@ -125,11 +125,11 @@ Pythonの例外なので、`return`ではなく、`raise`です。 これをオーバーライドするには`RequestValidationError`をインポートして`@app.exception_handler(RequestValidationError)`と一緒に使用して例外ハンドラをデコレートします。 -この例外ハンドラは`Requset`と例外を受け取ります。 +この例外ハンドラは`Request`と例外を受け取ります。 -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14,15,16] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *} -これで、`/items/foo`にアクセスすると、デフォルトのJSONエラーの代わりに以下が返されます: +これで、`/items/foo`にアクセスすると、以下のデフォルトのJSONエラーの代わりに: ```JSON { @@ -146,39 +146,20 @@ Pythonの例外なので、`return`ではなく、`raise`です。 } ``` -以下のようなテキスト版を取得します: +以下のテキスト版を取得します: ``` -1 validation error -path -> item_id - value is not a valid integer (type=type_error.integer) +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer ``` -#### `RequestValidationError`と`ValidationError` - -/// warning | 注意 - -これらは今のあなたにとって重要でない場合は省略しても良い技術的な詳細です。 - -/// - -`RequestValidationError`はPydanticの`ValidationError`のサブクラスです。 - -**FastAPI** は`response_model`でPydanticモデルを使用していて、データにエラーがあった場合、ログにエラーが表示されるようにこれを使用しています。 - -しかし、クライアントやユーザーはそれを見ることはありません。その代わりに、クライアントはHTTPステータスコード`500`の「Internal Server Error」を受け取ります。 - -*レスポンス*やコードのどこか(クライアントの*リクエスト*ではなく)にPydanticの`ValidationError`がある場合、それは実際にはコードのバグなのでこのようにすべきです。 - -また、あなたがそれを修正している間は、セキュリティの脆弱性が露呈する場合があるため、クライアントやユーザーがエラーに関する内部情報にアクセスできないようにしてください。 - -### エラーハンドラ`HTTPException`のオーバーライド +### `HTTPException`エラーハンドラのオーバーライド { #override-the-httpexception-error-handler } 同様に、`HTTPException`ハンドラをオーバーライドすることもできます。 例えば、これらのエラーに対しては、JSONではなくプレーンテキストを返すようにすることができます: -{* ../../docs_src/handling_errors/tutorial004.py hl[3,4,9,10,11,22] *} +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *} /// note | 技術詳細 @@ -188,13 +169,21 @@ path -> item_id /// -### `RequestValidationError`のボディの使用 +/// warning | 注意 + +`RequestValidationError`には、検証エラーが発生したファイル名と行番号の情報が含まれているため、必要であれば関連情報と一緒にログに表示できます。 + +しかし、そのまま文字列に変換して直接その情報を返すと、システムに関する情報が多少漏えいする可能性があります。そのため、ここではコードが各エラーを個別に抽出して表示します。 + +/// + +### `RequestValidationError`のボディの使用 { #use-the-requestvalidationerror-body } `RequestValidationError`には無効なデータを含む`body`が含まれています。 -アプリ開発中に本体のログを取ってデバッグしたり、ユーザーに返したりなどに使用することができます。 +アプリ開発中にボディのログを取ってデバッグしたり、ユーザーに返したりなどに使用することができます。 -{* ../../docs_src/handling_errors/tutorial005.py hl[14] *} +{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *} ここで、以下のような無効な項目を送信してみてください: @@ -207,7 +196,7 @@ path -> item_id 受信したボディを含むデータが無効であることを示すレスポンスが表示されます: -```JSON hl_lines="12 13 14 15" +```JSON hl_lines="12-15" { "detail": [ { @@ -226,36 +215,30 @@ path -> item_id } ``` -#### FastAPIの`HTTPException`とStarletteの`HTTPException` +#### FastAPIの`HTTPException`とStarletteの`HTTPException` { #fastapis-httpexception-vs-starlettes-httpexception } **FastAPI**は独自の`HTTPException`を持っています。 -また、 **FastAPI**のエラークラス`HTTPException`はStarletteのエラークラス`HTTPException`を継承しています。 +また、 **FastAPI**の`HTTPException`エラークラスはStarletteの`HTTPException`エラークラスを継承しています。 -唯一の違いは、**FastAPI** の`HTTPException`はレスポンスに含まれるヘッダを追加できることです。 - -これはOAuth 2.0といくつかのセキュリティユーティリティのために内部的に必要とされ、使用されています。 +唯一の違いは、**FastAPI** の`HTTPException`は`detail`フィールドにJSONに変換可能な任意のデータを受け付けるのに対し、Starletteの`HTTPException`は文字列のみを受け付けることです。 そのため、コード内では通常通り **FastAPI** の`HTTPException`を発生させ続けることができます。 -しかし、例外ハンドラを登録する際には、Starletteの`HTTPException`を登録しておく必要があります。 +しかし、例外ハンドラを登録する際には、Starletteの`HTTPException`に対して登録しておく必要があります。 -これにより、Starletteの内部コードやStarletteの拡張機能やプラグインの一部が`HTTPException`を発生させた場合、ハンドラがそれをキャッチして処理することができるようになります。 +これにより、Starletteの内部コードやStarletteの拡張機能やプラグインの一部がStarletteの`HTTPException`を発生させた場合、ハンドラがそれをキャッチして処理できるようになります。 -以下の例では、同じコード内で両方の`HTTPException`を使用できるようにするために、Starletteの例外の名前を`StarletteHTTPException`に変更しています: +この例では、同じコード内で両方の`HTTPException`を使用できるようにするために、Starletteの例外を`StarletteHTTPException`にリネームしています: ```Python from starlette.exceptions import HTTPException as StarletteHTTPException ``` -### **FastAPI** の例外ハンドラの再利用 +### **FastAPI** の例外ハンドラの再利用 { #reuse-fastapis-exception-handlers } -また、何らかの方法で例外を使用することもできますが、**FastAPI** から同じデフォルトの例外ハンドラを使用することもできます。 +**FastAPI** から同じデフォルトの例外ハンドラと一緒に例外を使用したい場合は、`fastapi.exception_handlers`からデフォルトの例外ハンドラをインポートして再利用できます: -デフォルトの例外ハンドラを`fastapi.exception_handlers`からインポートして再利用することができます: +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} -{* ../../docs_src/handling_errors/tutorial006.py hl[2,3,4,5,15,21] *} - -この例では、非常に表現力のあるメッセージでエラーを`print`しています。 - -しかし、例外を使用して、デフォルトの例外ハンドラを再利用することができるということが理解できます。 +この例では、非常に表現力のあるメッセージでエラーを`print`しているだけですが、要点は理解できるはずです。例外を使用し、その後デフォルトの例外ハンドラを再利用できます。 diff --git a/docs/ja/docs/tutorial/header-params.md b/docs/ja/docs/tutorial/header-params.md index ac89afbdb..1916baf61 100644 --- a/docs/ja/docs/tutorial/header-params.md +++ b/docs/ja/docs/tutorial/header-params.md @@ -1,20 +1,20 @@ -# ヘッダーのパラメータ +# ヘッダーのパラメータ { #header-parameters } ヘッダーのパラメータは、`Query`や`Path`、`Cookie`のパラメータを定義するのと同じように定義できます。 -## `Header`をインポート +## `Header`をインポート { #import-header } まず、`Header`をインポートします: -{* ../../docs_src/header_params/tutorial001.py hl[3] *} +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *} -## `Header`のパラメータの宣言 +## `Header`のパラメータの宣言 { #declare-header-parameters } 次に、`Path`や`Query`、`Cookie`と同じ構造を用いてヘッダーのパラメータを宣言します。 -最初の値がデフォルト値で、追加の検証パラメータや注釈パラメータをすべて渡すことができます。 +デフォルト値に加えて、追加の検証パラメータや注釈パラメータをすべて定義できます: -{* ../../docs_src/header_params/tutorial001.py hl[9] *} +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[9] *} /// note | 技術詳細 @@ -30,23 +30,23 @@ /// -## 自動変換 +## 自動変換 { #automatic-conversion } `Header`は`Path`や`Query`、`Cookie`が提供する機能に加え、少しだけ追加の機能を持っています。 -ほとんどの標準ヘッダーは、「マイナス記号」(`-`)としても知られる「ハイフン」で区切られています。 +ほとんどの標準ヘッダーは、「マイナス記号」(`-`)としても知られる「ハイフン」文字で区切られています。 しかし、`user-agent`のような変数はPythonでは無効です。 -そのため、デフォルトでは、`Header`はパラメータの文字をアンダースコア(`_`)からハイフン(`-`)に変換して、ヘッダーを抽出して文書化します。 +そのため、デフォルトでは、`Header`はパラメータ名の文字をアンダースコア(`_`)からハイフン(`-`)に変換して、ヘッダーを抽出して文書化します。 また、HTTPヘッダは大文字小文字を区別しないので、Pythonの標準スタイル(別名「スネークケース」)で宣言することができます。 そのため、`User_Agent`などのように最初の文字を大文字にする必要はなく、通常のPythonコードと同じように`user_agent`を使用することができます。 -もしなんらかの理由でアンダースコアからハイフンへの自動変換を無効にする必要がある場合は、`Header`の`convert_underscores`に`False`を設定してください: +もしなんらかの理由でアンダースコアからハイフンへの自動変換を無効にする必要がある場合は、`Header`のパラメータ`convert_underscores`を`False`に設定してください: -{* ../../docs_src/header_params/tutorial002.py hl[9] *} +{* ../../docs_src/header_params/tutorial002_an_py310.py hl[10] *} /// warning | 注意 @@ -54,26 +54,26 @@ /// -## ヘッダーの重複 +## ヘッダーの重複 { #duplicate-headers } 受信したヘッダーが重複することがあります。つまり、同じヘッダーで複数の値を持つということです。 -これらの場合、リストの型宣言を使用して定義することができます。 +これらの場合、型宣言でリストを使用して定義することができます。 重複したヘッダーのすべての値をPythonの`list`として受け取ることができます。 例えば、複数回出現する可能性のある`X-Token`のヘッダを定義するには、以下のように書くことができます: -{* ../../docs_src/header_params/tutorial003.py hl[9] *} +{* ../../docs_src/header_params/tutorial003_an_py310.py hl[9] *} -もし、その*path operation*で通信する場合は、次のように2つのHTTPヘッダーを送信します: +その*path operation*と通信する際に、次のように2つのHTTPヘッダーを送信する場合: ``` X-Token: foo X-Token: bar ``` -このレスポンスは以下のようになります: +レスポンスは以下のようになります: ```JSON { @@ -84,8 +84,8 @@ X-Token: bar } ``` -## まとめ +## まとめ { #recap } -ヘッダーは`Header`で宣言し、`Query`や`Path`、`Cookie`と同じパターンを使用する。 +ヘッダーは`Header`で宣言し、`Query`や`Path`、`Cookie`と同じ共通パターンを使用します。 また、変数のアンダースコアを気にする必要はありません。**FastAPI** がそれらの変換をすべて取り持ってくれます。 diff --git a/docs/ja/docs/tutorial/index.md b/docs/ja/docs/tutorial/index.md index 87d3751fd..d298abc62 100644 --- a/docs/ja/docs/tutorial/index.md +++ b/docs/ja/docs/tutorial/index.md @@ -1,83 +1,95 @@ -# チュートリアル - ユーザーガイド +# チュートリアル - ユーザーガイド { #tutorial-user-guide } -このチュートリアルは**FastAPI**のほぼすべての機能の使い方を段階的に紹介します。 +このチュートリアルでは、**FastAPI**のほとんどの機能を使う方法を段階的に紹介します。 -各セクションは前のセクションを踏まえた内容になっています。しかし、トピックごとに分割されているので、特定のAPIの要求を満たすようなトピックに直接たどり着けるようになっています。 +各セクションは前のセクションを踏まえた内容になっています。しかし、トピックごとに分割されているので、特定のAPIのニーズを満たすために、任意の特定のトピックに直接進めるようになっています。 -また、将来的にリファレンスとして機能するように構築されています。 +また、将来的にリファレンスとして機能するように構築されているので、後で戻ってきて必要なものを正確に確認できます。 -従って、後でこのチュートリアルに戻ってきて必要なものを確認できます。 - -## コードを実行する +## コードを実行する { #run-the-code } すべてのコードブロックをコピーして直接使用できます(実際にテストされたPythonファイルです)。 -いずれかの例を実行するには、コードを `main.py`ファイルにコピーし、` uvicorn`を次のように起動します: +いずれかの例を実行するには、コードを `main.py`ファイルにコピーし、次のように `fastapi dev` を起動します:
```console -$ uvicorn main:app --reload +$ fastapi dev main.py -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: Waiting for application startup. -INFO: Application startup complete. + 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. ```
-コードを記述またはコピーし、編集してローカルで実行することを**強くお勧めします**。 +コードを記述またはコピーし、編集してローカルで実行することを**強く推奨します**。 -また、エディターで使用することで、書く必要のあるコードの少なさ、すべての型チェック、自動補完などのFastAPIの利点を実感できます。 +エディターで使用することで、書く必要のあるコードの少なさ、すべての型チェック、自動補完など、FastAPIの利点を本当に実感できます。 --- -## FastAPIをインストールする +## FastAPIをインストールする { #install-fastapi } 最初のステップは、FastAPIのインストールです。 -チュートリアルのために、すべてのオプションの依存関係と機能をインストールしたいとき: +[仮想環境](../virtual-environments.md){.internal-link target=_blank} を作成して有効化し、それから **FastAPIをインストール** してください:
```console -$ pip install "fastapi[all]" +$ pip install "fastapi[standard]" ---> 100% ```
-...これには、コードを実行するサーバーとして使用できる `uvicorn`も含まれます。 - /// note | 備考 -パーツ毎にインストールすることも可能です。 +`pip install "fastapi[standard]"` でインストールすると、`fastapi-cloud-cli` を含むいくつかのデフォルトのオプション標準依存関係が付属します。これにより、FastAPI Cloud にデプロイできます。 -以下は、アプリケーションを本番環境にデプロイする際に行うであろうものです: +これらのオプション依存関係が不要な場合は、代わりに `pip install fastapi` をインストールできます。 -``` -pip install fastapi -``` - -また、サーバーとして動作するように`uvicorn` をインストールします: - -``` -pip install "uvicorn[standard]" -``` - -そして、使用したい依存関係をそれぞれ同様にインストールします。 +標準依存関係はインストールしたいが `fastapi-cloud-cli` は不要な場合は、`pip install "fastapi[standard-no-fastapi-cloud-cli]"` でインストールできます。 /// -## 高度なユーザーガイド +## 高度なユーザーガイド { #advanced-user-guide } -**高度なユーザーガイド**もあり、**チュートリアル - ユーザーガイド**の後で読むことができます。 +この **チュートリアル - ユーザーガイド** の後で、後から読める **高度なユーザーガイド** もあります。 -**高度なユーザーガイド**は**チュートリアル - ユーザーガイド**に基づいており、同じ概念を使用し、いくつかの追加機能を紹介しています。 +**高度なユーザーガイド** は本チュートリアルをベースにしており、同じ概念を使用し、いくつかの追加機能を教えます。 -ただし、最初に**チュートリアル - ユーザーガイド**(現在読んでいる内容)をお読みください。 +ただし、最初に **チュートリアル - ユーザーガイド**(今読んでいる内容)をお読みください。 -**チュートリアル-ユーザーガイド**だけで完全なアプリケーションを構築できるように設計されています。加えて、**高度なユーザーガイド**の中からニーズに応じたアイデアを使用して、様々な拡張が可能です。 +**チュートリアル - ユーザーガイド** だけで完全なアプリケーションを構築できるように設計されており、その後ニーズに応じて、**高度なユーザーガイド** の追加のアイデアのいくつかを使って、さまざまな方法で拡張できます。 diff --git a/docs/ja/docs/tutorial/metadata.md b/docs/ja/docs/tutorial/metadata.md index b93dedcb9..0ffb8f350 100644 --- a/docs/ja/docs/tutorial/metadata.md +++ b/docs/ja/docs/tutorial/metadata.md @@ -1,47 +1,66 @@ -# メタデータとドキュメントのURL +# メタデータとドキュメントのURL { #metadata-and-docs-urls } -**FastAPI** アプリケーションのいくつかのメタデータの設定をカスタマイズできます。 +**FastAPI** アプリケーションのいくつかのメタデータ設定をカスタマイズできます。 -## タイトル、説明文、バージョン +## APIのメタデータ { #metadata-for-api } -以下を設定できます: +OpenAPI仕様および自動APIドキュメントUIで使用される次のフィールドを設定できます: -* **タイトル**: OpenAPIおよび自動APIドキュメントUIでAPIのタイトル/名前として使用される。 -* **説明文**: OpenAPIおよび自動APIドキュメントUIでのAPIの説明文。 -* **バージョン**: APIのバージョン。例: `v2` または `2.5.0`。 - *たとえば、以前のバージョンのアプリケーションがあり、OpenAPIも使用している場合に便利です。 +| パラメータ | 型 | 説明 | +|------------|------|-------------| +| `title` | `str` | APIのタイトルです。 | +| `summary` | `str` | APIの短い要約です。 OpenAPI 3.1.0、FastAPI 0.99.0 以降で利用できます。 | +| `description` | `str` | APIの短い説明です。Markdownを使用できます。 | +| `version` | `string` | APIのバージョンです。これはOpenAPIのバージョンではなく、あなた自身のアプリケーションのバージョンです。たとえば `2.5.0` です。 | +| `terms_of_service` | `str` | APIの利用規約へのURLです。指定する場合、URLである必要があります。 | +| `contact` | `dict` | 公開されるAPIの連絡先情報です。複数のフィールドを含められます。
contact fields
ParameterTypeDescription
namestr連絡先の個人/組織を識別する名前です。
urlstr連絡先情報を指すURLです。URL形式である必要があります。
emailstr連絡先の個人/組織のメールアドレスです。メールアドレス形式である必要があります。
| +| `license_info` | `dict` | 公開されるAPIのライセンス情報です。複数のフィールドを含められます。
license_info fields
ParameterTypeDescription
namestr必須license_info が設定されている場合)。APIに使用されるライセンス名です。
identifierstrAPIの SPDX ライセンス式です。identifier フィールドは url フィールドと同時に指定できません。 OpenAPI 3.1.0、FastAPI 0.99.0 以降で利用できます。
urlstrAPIに使用されるライセンスへのURLです。URL形式である必要があります。
| -これらを設定するには、パラメータ `title`、`description`、`version` を使用します: +以下のように設定できます: -{* ../../docs_src/metadata/tutorial001.py hl[4:6] *} +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} -この設定では、自動APIドキュメントは以下の様になります: +/// tip | 豆知識 + +`description` フィールドにはMarkdownを書けて、出力ではレンダリングされます。 + +/// + +この設定では、自動APIドキュメントは以下のようになります: -## タグのためのメタデータ +## ライセンス識別子 { #license-identifier } -さらに、パラメータ `openapi_tags` を使うと、path operations をグループ分けするための複数のタグに関するメタデータを追加できます。 +OpenAPI 3.1.0 および FastAPI 0.99.0 以降では、`license_info` を `url` の代わりに `identifier` で設定することもできます。 -それぞれのタグ毎にひとつの辞書を含むリストをとります。 +例: -それぞれの辞書は以下をもつことができます: +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} -* `name` (**必須**): *path operations* および `APIRouter` の `tags` パラメーターで使用するのと同じタグ名である `str`。 -* `description`: タグの簡単な説明文である `str`。 Markdownで記述でき、ドキュメントUIに表示されます。 -* `externalDocs`: 外部ドキュメントを説明するための `dict`: - * `description`: 外部ドキュメントの簡単な説明文である `str`。 - * `url` (**必須**): 外部ドキュメントのURLである `str`。 +## タグのメタデータ { #metadata-for-tags } -### タグのためのメタデータの作成 +パラメータ `openapi_tags` を使うと、path operation をグループ分けするために使用する各タグに追加のメタデータを追加できます。 -`users` と `items` のタグを使った例でメタデータの追加を試してみましょう。 +それぞれのタグごとに1つの辞書を含むリストを取ります。 -タグのためのメタデータを作成し、それを `openapi_tags` パラメータに渡します。 +それぞれの辞書は以下を含められます: -{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *} +* `name` (**必須**): *path operation* および `APIRouter` の `tags` パラメータで使用するのと同じタグ名の `str`。 +* `description`: タグの短い説明の `str`。Markdownを含められ、ドキュメントUIに表示されます。 +* `externalDocs`: 外部ドキュメントを説明する `dict`。以下を含みます: + * `description`: 外部ドキュメントの短い説明の `str`。 + * `url` (**必須**): 外部ドキュメントのURLの `str`。 -説明文 (description) の中で Markdown を使用できることに注意してください。たとえば、「login」は太字 (**login**) で表示され、「fancy」は斜体 (_fancy_) で表示されます。 +### タグのメタデータの作成 { #create-metadata-for-tags } + +`users` と `items` のタグを使った例で試してみましょう。 + +タグのメタデータを作成し、それを `openapi_tags` パラメータに渡します: + +{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *} + +説明の中でMarkdownを使用できることに注意してください。たとえば「login」は太字 (**login**) で表示され、「fancy」は斜体 (_fancy_) で表示されます。 /// tip | 豆知識 @@ -49,31 +68,31 @@ /// -### 自作タグの使用 +### タグの使用 { #use-your-tags } -`tags` パラメーターを使用して、それぞれの *path operations* (および `APIRouter`) を異なるタグに割り当てます: +*path operation*(および `APIRouter`)の `tags` パラメータを使用して、それらを異なるタグに割り当てます: -{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} /// info | 情報 -タグのより詳しい説明を知りたい場合は [Path Operation Configuration](path-operation-configuration.md#tags){.internal-link target=_blank} を参照して下さい。 +タグの詳細は [Path Operation Configuration](path-operation-configuration.md#tags){.internal-link target=_blank} を参照してください。 /// -### ドキュメントの確認 +### ドキュメントの確認 { #check-the-docs } -ここで、ドキュメントを確認すると、追加したメタデータがすべて表示されます: +ここでドキュメントを確認すると、追加したメタデータがすべて表示されます: -### タグの順番 +### タグの順番 { #order-of-tags } タグのメタデータ辞書の順序は、ドキュメントUIに表示される順序の定義にもなります。 -たとえば、`users` はアルファベット順では `items` の後に続きます。しかし、リストの最初に `users` のメタデータ辞書を追加したため、ドキュメントUIでは `users` が先に表示されます。 +たとえば、`users` はアルファベット順では `items` の後に続きますが、リストの最初の辞書としてメタデータを追加したため、それより前に表示されます。 -## OpenAPI URL +## OpenAPI URL { #openapi-url } デフォルトでは、OpenAPIスキーマは `/openapi.json` で提供されます。 @@ -81,21 +100,21 @@ たとえば、`/api/v1/openapi.json` で提供されるように設定するには: -{* ../../docs_src/metadata/tutorial002.py hl[3] *} +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} OpenAPIスキーマを完全に無効にする場合は、`openapi_url=None` を設定できます。これにより、それを使用するドキュメントUIも無効になります。 -## ドキュメントのURL +## ドキュメントのURL { #docs-urls } -以下の2つのドキュメントUIを構築できます: +含まれている2つのドキュメントUIを設定できます: * **Swagger UI**: `/docs` で提供されます。 - * URL はパラメータ `docs_url` で設定できます。 - * `docs_url=None` を設定することで無効にできます。 -* ReDoc: `/redoc` で提供されます。 - * URL はパラメータ `redoc_url` で設定できます。 - * `redoc_url=None` を設定することで無効にできます。 + * URL はパラメータ `docs_url` で設定できます。 + * `docs_url=None` を設定することで無効にできます。 +* **ReDoc**: `/redoc` で提供されます。 + * URL はパラメータ `redoc_url` で設定できます。 + * `redoc_url=None` を設定することで無効にできます。 たとえば、`/documentation` でSwagger UIが提供されるように設定し、ReDocを無効にするには: -{* ../../docs_src/metadata/tutorial003.py hl[3] *} +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/ja/docs/tutorial/middleware.md b/docs/ja/docs/tutorial/middleware.md index 539ec5b8c..12fb57a64 100644 --- a/docs/ja/docs/tutorial/middleware.md +++ b/docs/ja/docs/tutorial/middleware.md @@ -1,4 +1,4 @@ -# ミドルウェア +# ミドルウェア { #middleware } **FastAPI** アプリケーションにミドルウェアを追加できます。 @@ -15,11 +15,11 @@ `yield` を使った依存関係をもつ場合は、終了コードはミドルウェアの *後に* 実行されます。 -バックグラウンドタスク (後述) がある場合は、それらは全てのミドルウェアの *後に* 実行されます。 +バックグラウンドタスク ([バックグラウンドタスク](background-tasks.md){.internal-link target=_blank} セクションで説明します。後で確認できます) がある場合は、それらは全てのミドルウェアの *後に* 実行されます。 /// -## ミドルウェアの作成 +## ミドルウェアの作成 { #create-a-middleware } ミドルウェアを作成するには、関数の上部でデコレータ `@app.middleware("http")` を使用します。 @@ -31,13 +31,13 @@ * 次に、対応する*path operation*によって生成された `response` を返します。 * その後、`response` を返す前にさらに `response` を変更することもできます。 -{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[8:9,11,14] *} /// tip | 豆知識 -'X-'プレフィックスを使用してカスタムの独自ヘッダーを追加できます。 +カスタムの独自ヘッダーは `X-` プレフィックスを使用して追加できる点に注意してください。 -ただし、ブラウザのクライアントに表示させたいカスタムヘッダーがある場合は、StarletteのCORSドキュメントに記載されているパラメータ `expose_headers` を使用して、それらをCORS設定に追加する必要があります ([CORS (オリジン間リソース共有)](cors.md){.internal-link target=_blank}) +ただし、ブラウザのクライアントに表示させたいカスタムヘッダーがある場合は、StarletteのCORSドキュメントに記載されているパラメータ `expose_headers` を使用して、それらをCORS設定に追加する必要があります ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})。 /// @@ -49,7 +49,7 @@ /// -### `response` の前後 +### `response` の前後 { #before-and-after-the-response } *path operation* が `request` を受け取る前に、 `request` とともに実行されるコードを追加できます。 @@ -57,9 +57,38 @@ 例えば、リクエストの処理とレスポンスの生成にかかった秒数を含むカスタムヘッダー `X-Process-Time` を追加できます: -{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} -## その他のミドルウェア +/// tip | 豆知識 + +ここでは、これらのユースケースに対してより正確になり得るため、`time.time()` の代わりに `time.perf_counter()` を使用しています。 🤓 + +/// + +## 複数ミドルウェアの実行順序 { #multiple-middleware-execution-order } + +`@app.middleware()` デコレータまたは `app.add_middleware()` メソッドのいずれかを使って複数のミドルウェアを追加すると、新しく追加された各ミドルウェアがアプリケーションをラップし、スタックを形成します。最後に追加されたミドルウェアが *最も外側*、最初に追加されたミドルウェアが *最も内側* になります。 + +リクエスト経路では、*最も外側* のミドルウェアが最初に実行されます。 + +レスポンス経路では、最後に実行されます。 + +例: + +```Python +app.add_middleware(MiddlewareA) +app.add_middleware(MiddlewareB) +``` + +これにより、実行順序は次のようになります: + +* **リクエスト**: MiddlewareB → MiddlewareA → route + +* **レスポンス**: route → MiddlewareA → MiddlewareB + +このスタック動作により、ミドルウェアが予測可能で制御しやすい順序で実行されることが保証されます。 + +## その他のミドルウェア { #other-middlewares } 他のミドルウェアの詳細については、[高度なユーザーガイド: 高度なミドルウェア](../advanced/middleware.md){.internal-link target=_blank}を参照してください。 diff --git a/docs/ja/docs/tutorial/path-operation-configuration.md b/docs/ja/docs/tutorial/path-operation-configuration.md index 0cc38cb25..eb6b6b11a 100644 --- a/docs/ja/docs/tutorial/path-operation-configuration.md +++ b/docs/ja/docs/tutorial/path-operation-configuration.md @@ -1,14 +1,14 @@ -# Path Operationの設定 +# Path Operationの設定 { #path-operation-configuration } *path operationデコレータ*を設定するためのパラメータがいくつかあります。 /// warning | 注意 -これらのパラメータは*path operation関数*ではなく、*path operationデコレータ*に直接渡されることに注意してください。 +これらのパラメータは*path operationデコレータ*に直接渡され、*path operation関数*に渡されないことに注意してください。 /// -## レスポンスステータスコード +## レスポンスステータスコード { #response-status-code } *path operation*のレスポンスで使用する(HTTP)`status_code`を定義することができます。 @@ -16,55 +16,65 @@ しかし、それぞれの番号コードが何のためのものか覚えていない場合は、`status`のショートカット定数を使用することができます: -{* ../../docs_src/path_operation_configuration/tutorial001.py hl[3,17] *} +{* ../../docs_src/path_operation_configuration/tutorial001_py310.py hl[1,15] *} そのステータスコードはレスポンスで使用され、OpenAPIスキーマに追加されます。 /// note | 技術詳細 -また、`from starlette import status`を使用することもできます。 +`from starlette import status`を使用することもできます。 **FastAPI** は開発者の利便性を考慮して、`fastapi.status`と同じ`starlette.status`を提供しています。しかし、これはStarletteから直接提供されています。 /// -## タグ +## タグ { #tags } `tags`パラメータを`str`の`list`(通常は1つの`str`)と一緒に渡すと、*path operation*にタグを追加できます: -{* ../../docs_src/path_operation_configuration/tutorial002.py hl[17,22,27] *} +{* ../../docs_src/path_operation_configuration/tutorial002_py310.py hl[15,20,25] *} これらはOpenAPIスキーマに追加され、自動ドキュメントのインターフェースで使用されます: - + -## 概要と説明 +### Enumを使ったタグ { #tags-with-enums } + +大きなアプリケーションの場合、**複数のタグ**が蓄積されていき、関連する*path operations*に対して常に**同じタグ**を使っていることを確認したくなるかもしれません。 + +このような場合、タグを`Enum`に格納すると理にかなっています。 + +**FastAPI** は、プレーンな文字列の場合と同じ方法でそれをサポートしています: + +{* ../../docs_src/path_operation_configuration/tutorial002b_py39.py hl[1,8:10,13,18] *} + +## 概要と説明 { #summary-and-description } `summary`と`description`を追加できます: -{* ../../docs_src/path_operation_configuration/tutorial003.py hl[20:21] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} -## docstringを用いた説明 +## docstringを用いた説明 { #description-from-docstring } -説明文は長くて複数行におよぶ傾向があるので、関数docstring内に*path operation*の説明文を宣言できます。すると、**FastAPI** は説明文を読み込んでくれます。 +説明文は長くて複数行におよぶ傾向があるので、関数docstring内に*path operation*の説明文を宣言できます。すると、**FastAPI** は説明文を読み込んでくれます。 docstringにMarkdownを記述すれば、正しく解釈されて表示されます。(docstringのインデントを考慮して) -{* ../../docs_src/path_operation_configuration/tutorial004.py hl[19:27] *} +{* ../../docs_src/path_operation_configuration/tutorial004_py310.py hl[17:25] *} これは対話的ドキュメントで使用されます: - + -## レスポンスの説明 +## レスポンスの説明 { #response-description } `response_description`パラメータでレスポンスの説明をすることができます。 -{* ../../docs_src/path_operation_configuration/tutorial005.py hl[21] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | 情報 -`respnse_description`は具体的にレスポンスを参照し、`description`は*path operation*全般を参照していることに注意してください。 +`response_description`は具体的にレスポンスを参照し、`description`は*path operation*全般を参照していることに注意してください。 /// @@ -76,22 +86,22 @@ OpenAPIは*path operation*ごとにレスポンスの説明を必要としてい /// - + -## 非推奨の*path operation* +## *path operation*を非推奨にする { #deprecate-a-path-operation } -*path operation*をdeprecatedとしてマークする必要があるが、それを削除しない場合は、`deprecated`パラメータを渡します: +*path operation*をdeprecatedとしてマークする必要があるが、それを削除しない場合は、`deprecated`パラメータを渡します: -{* ../../docs_src/path_operation_configuration/tutorial006.py hl[16] *} +{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} 対話的ドキュメントでは非推奨と明記されます: - + *path operations*が非推奨である場合とそうでない場合でどのように見えるかを確認してください: - + -## まとめ +## まとめ { #recap } *path operationデコレータ*にパラメータを渡すことで、*path operations*のメタデータを簡単に設定・追加することができます。 diff --git a/docs/ja/docs/tutorial/path-params-numeric-validations.md b/docs/ja/docs/tutorial/path-params-numeric-validations.md index a1810ae37..6a9ecc4e7 100644 --- a/docs/ja/docs/tutorial/path-params-numeric-validations.md +++ b/docs/ja/docs/tutorial/path-params-numeric-validations.md @@ -1,40 +1,52 @@ -# パスパラメータと数値の検証 +# パスパラメータと数値の検証 { #path-parameters-and-numeric-validations } クエリパラメータに対して`Query`でより多くのバリデーションとメタデータを宣言できるのと同じように、パスパラメータに対しても`Path`で同じ種類のバリデーションとメタデータを宣言することができます。 -## Pathのインポート +## `Path`のインポート { #import-path } -まず初めに、`fastapi`から`Path`をインポートします: +まず初めに、`fastapi`から`Path`をインポートし、`Annotated`もインポートします: -{* ../../docs_src/path_params_numeric_validations/tutorial001.py hl[1] *} +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *} -## メタデータの宣言 +/// info | 情報 + +FastAPI はバージョン 0.95.0 で`Annotated`のサポートを追加し(そして推奨し始めました)。 + +古いバージョンの場合、`Annotated`を使おうとするとエラーになります。 + +`Annotated`を使用する前に、FastAPI のバージョンを少なくとも 0.95.1 まで[アップグレードしてください](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}。 + +/// + +## メタデータの宣言 { #declare-metadata } パラメータは`Query`と同じものを宣言することができます。 例えば、パスパラメータ`item_id`に対して`title`のメタデータを宣言するには以下のようにします: -{* ../../docs_src/path_params_numeric_validations/tutorial001.py hl[8] *} +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} /// note | 備考 -パスの一部でなければならないので、パスパラメータは常に必須です。 - -そのため、`...`を使用して必須と示す必要があります。 - -それでも、`None`で宣言しても、デフォルト値を設定しても、何の影響もなく、常に必要とされていることに変わりはありません。 +パスパラメータはパスの一部でなければならないので、常に必須です。`None`で宣言したりデフォルト値を設定したりしても何も影響せず、常に必須のままです。 /// -## 必要に応じてパラメータを並び替える +## 必要に応じてパラメータを並び替える { #order-the-parameters-as-you-need } + +/// tip | 豆知識 + +`Annotated`を使う場合、これはおそらくそれほど重要でも必要でもありません。 + +/// クエリパラメータ`q`を必須の`str`として宣言したいとしましょう。 また、このパラメータには何も宣言する必要がないので、`Query`を使う必要はありません。 -しかし、パスパラメータ`item_id`のために`Path`を使用する必要があります。 +しかし、パスパラメータ`item_id`のために`Path`を使用する必要があります。そして何らかの理由で`Annotated`を使いたくないとします。 -Pythonは「デフォルト」を持たない値の前に「デフォルト」を持つ値を置くことができません。 +Pythonは「デフォルト」を持つ値を「デフォルト」を持たない値の前に置くとエラーになります。 しかし、それらを並び替えることができ、デフォルト値を持たない値(クエリパラメータ`q`)を最初に持つことができます。 @@ -42,63 +54,88 @@ Pythonは「デフォルト」を持たない値の前に「デフォルト」 そのため、以下のように関数を宣言することができます: -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[8] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} -## 必要に応じてパラメータを並び替えるトリック +ただし、`Annotated`を使う場合はこの問題は起きないことを覚えておいてください。`Query()`や`Path()`に関数パラメータのデフォルト値を使わないためです。 -クエリパラメータ`q`を`Query`やデフォルト値なしで宣言し、パスパラメータ`item_id`を`Path`を用いて宣言し、それらを別の順番に並びたい場合、Pythonには少し特殊な構文が用意されています。 +{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} + +## 必要に応じてパラメータを並び替えるトリック { #order-the-parameters-as-you-need-tricks } + +/// tip | 豆知識 + +`Annotated`を使う場合、これはおそらくそれほど重要でも必要でもありません。 + +/// + +これは**小さなトリック**で、便利な場合がありますが、頻繁に必要になることはありません。 + +次のことをしたい場合: + +* `q`クエリパラメータを`Query`もデフォルト値もなしで宣言する +* パスパラメータ`item_id`を`Path`を使って宣言する +* それらを別の順番にする +* `Annotated`を使わない + +...Pythonにはそのための少し特殊な構文があります。 関数の最初のパラメータとして`*`を渡します。 Pythonはその`*`で何かをすることはありませんが、それ以降のすべてのパラメータがキーワード引数(キーと値のペア)として呼ばれるべきものであると知っているでしょう。それはkwargsとしても知られています。たとえデフォルト値がなくても。 -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[8] *} +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} -## 数値の検証: 以上 +### `Annotated`のほうがよい { #better-with-annotated } -`Query`と`Path`(、そして後述する他のもの)を用いて、文字列の制約を宣言することができますが、数値の制約も同様に宣言できます。 +`Annotated`を使う場合は、関数パラメータのデフォルト値を使わないため、この問題は起きず、おそらく`*`を使う必要もありません。 -ここで、`ge=1`の場合、`item_id`は`1`「より大きい`g`か、同じ`e`」整数でなれけばなりません。 +{* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} -{* ../../docs_src/path_params_numeric_validations/tutorial004.py hl[8] *} +## 数値の検証: 以上 { #number-validations-greater-than-or-equal } -## 数値の検証: より大きいと小なりイコール +`Query`と`Path`(、そして後述する他のもの)を用いて、数値の制約を宣言できます。 + +ここで、`ge=1`の場合、`item_id`は`1`「より大きい`g`か、同じ`e`」整数でなければなりません。 + +{* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} + +## 数値の検証: より大きいと小なりイコール { #number-validations-greater-than-and-less-than-or-equal } 以下も同様です: -* `gt`: より大きい(`g`reater `t`han) -* `le`: 小なりイコール(`l`ess than or `e`qual) +* `gt`: `g`reater `t`han +* `le`: `l`ess than or `e`qual -{* ../../docs_src/path_params_numeric_validations/tutorial005.py hl[9] *} +{* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *} -## 数値の検証: 浮動小数点、 大なり小なり +## 数値の検証: 浮動小数点、 大なり小なり { #number-validations-floats-greater-than-and-less-than } 数値のバリデーションは`float`の値に対しても有効です。 -ここで重要になってくるのはgtだけでなくgeも宣言できることです。これと同様に、例えば、値が`1`より小さくても`0`より大きくなければならないことを要求することができます。 +ここで重要になってくるのはgtだけでなくgeも宣言できることです。これと同様に、例えば、値が`1`より小さくても`0`より大きくなければならないことを要求することができます。 したがって、`0.5`は有効な値ですが、`0.0`や`0`はそうではありません。 -これはltも同じです。 +これはltも同じです。 -{* ../../docs_src/path_params_numeric_validations/tutorial006.py hl[11] *} +{* ../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py hl[13] *} -## まとめ +## まとめ { #recap } `Query`と`Path`(そしてまだ見たことない他のもの)では、[クエリパラメータと文字列の検証](query-params-str-validations.md){.internal-link target=_blank}と同じようにメタデータと文字列の検証を宣言することができます。 また、数値のバリデーションを宣言することもできます: -* `gt`: より大きい(`g`reater `t`han) -* `ge`: 以上(`g`reater than or `e`qual) -* `lt`: より小さい(`l`ess `t`han) -* `le`: 以下(`l`ess than or `e`qual) +* `gt`: `g`reater `t`han +* `ge`: `g`reater than or `e`qual +* `lt`: `l`ess `t`han +* `le`: `l`ess than or `e`qual /// info | 情報 -`Query`、`Path`などは後に共通の`Param`クラスのサブクラスを見ることになります。(使う必要はありません) +`Query`、`Path`、および後で見る他のクラスは、共通の`Param`クラスのサブクラスです。 -そして、それらすべては、これまで見てきた追加のバリデーションとメタデータと同じパラメータを共有しています。 +それらはすべて、これまで見てきた追加のバリデーションとメタデータの同じパラメータを共有しています。 /// diff --git a/docs/ja/docs/tutorial/path-params.md b/docs/ja/docs/tutorial/path-params.md index 1893ec12f..96a1fe9d1 100644 --- a/docs/ja/docs/tutorial/path-params.md +++ b/docs/ja/docs/tutorial/path-params.md @@ -1,22 +1,22 @@ -# パスパラメータ +# パスパラメータ { #path-parameters } Pythonのformat文字列と同様のシンタックスで「パスパラメータ」や「パス変数」を宣言できます: -{* ../../docs_src/path_params/tutorial001.py hl[6,7] *} +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} パスパラメータ `item_id` の値は、引数 `item_id` として関数に渡されます。 -しがたって、この例を実行して http://127.0.0.1:8000/items/foo にアクセスすると、次のレスポンスが表示されます。 +したがって、この例を実行して http://127.0.0.1:8000/items/foo にアクセスすると、次のレスポンスが表示されます。 ```JSON {"item_id":"foo"} ``` -## パスパラメータと型 +## 型付きパスパラメータ { #path-parameters-with-types } 標準のPythonの型アノテーションを使用して、関数内のパスパラメータの型を宣言できます: -{* ../../docs_src/path_params/tutorial002.py hl[7] *} +{* ../../docs_src/path_params/tutorial002_py39.py hl[7] *} ここでは、 `item_id` は `int` として宣言されています。 @@ -26,7 +26,7 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー /// -## データ変換 +## データ変換 { #data-conversion } この例を実行し、ブラウザで http://127.0.0.1:8000/items/3 を開くと、次のレスポンスが表示されます: @@ -38,68 +38,69 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー 関数が受け取った(および返した)値は、文字列の `"3"` ではなく、Pythonの `int` としての `3` であることに注意してください。 -したがって、型宣言を使用すると、**FastAPI**は自動リクエスト "解析" を行います。 +したがって、その型宣言を使うと、**FastAPI**は自動リクエスト "解析" を行います。 /// -## データバリデーション +## データバリデーション { #data-validation } しかしブラウザで http://127.0.0.1:8000/items/foo を開くと、次のHTTPエラーが表示されます: ```JSON { - "detail": [ - { - "loc": [ - "path", - "item_id" - ], - "msg": "value is not a valid integer", - "type": "type_error.integer" - } - ] + "detail": [ + { + "type": "int_parsing", + "loc": [ + "path", + "item_id" + ], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo" + } + ] } ``` これは、パスパラメータ `item_id` が `int` ではない値 `"foo"` だからです。 -http://127.0.0.1:8000/items/4.2 で見られるように、intのかわりに `float` が与えられた場合にも同様なエラーが表示されます。 +http://127.0.0.1:8000/items/4.2 で見られるように、`int` のかわりに `float` が与えられた場合にも同様なエラーが表示されます。 /// check | 確認 -したがって、Pythonの型宣言を使用することで、**FastAPI**はデータのバリデーションを行います。 +したがって、同じPythonの型宣言を使用することで、**FastAPI**はデータのバリデーションを行います。 -表示されたエラーには問題のある箇所が明確に指摘されていることに注意してください。 +表示されたエラーには、バリデーションが通らなかった箇所が明確に示されていることに注意してください。 -これは、APIに関連するコードの開発およびデバッグに非常に役立ちます。 +これは、APIとやり取りするコードを開発・デバッグする際に非常に役立ちます。 /// -## ドキュメント +## ドキュメント { #documentation } -そしてブラウザで http://127.0.0.1:8000/docs を開くと、以下の様な自動的に生成された対話的なドキュメントが表示されます。 +そしてブラウザで http://127.0.0.1:8000/docs を開くと、以下の様な自動的に生成された対話的なAPIドキュメントが表示されます。 /// check | 確認 -繰り返しになりますが、Python型宣言を使用するだけで、**FastAPI**は対話的なAPIドキュメントを自動的に生成します(Swagger UIを統合)。 +繰り返しになりますが、同じPython型宣言を使用するだけで、**FastAPI**は対話的なドキュメントを自動的に生成します(Swagger UIを統合)。 パスパラメータが整数として宣言されていることに注意してください。 /// -## 標準であることのメリット、ドキュメンテーションの代替物 +## 標準ベースのメリット、ドキュメンテーションの代替物 { #standards-based-benefits-alternative-documentation } -また、生成されたスキーマが OpenAPI 標準に従っているので、互換性のあるツールが多数あります。 +また、生成されたスキーマが OpenAPI 標準に従っているので、互換性のあるツールが多数あります。 このため、**FastAPI**自体が代替のAPIドキュメントを提供します(ReDocを使用)。これは、 http://127.0.0.1:8000/redoc にアクセスすると確認できます。 -同様に、互換性のあるツールが多数あります(多くの言語用のコード生成ツールを含む)。 +同様に、互換性のあるツールが多数あります。多くの言語用のコード生成ツールを含みます。 -## Pydantic +## Pydantic { #pydantic } すべてのデータバリデーションは Pydantic によって内部で実行されるため、Pydanticの全てのメリットが得られます。そして、安心して利用することができます。 @@ -107,7 +108,7 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー これらのいくつかについては、チュートリアルの次の章で説明します。 -## 順序の問題 +## 順序の問題 { #order-matters } *path operations* を作成する際、固定パスをもつ状況があり得ます。 @@ -117,29 +118,29 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー *path operations* は順に評価されるので、 `/users/me` が `/users/{user_id}` よりも先に宣言されているか確認する必要があります: -{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} +{* ../../docs_src/path_params/tutorial003_py39.py hl[6,11] *} -それ以外の場合、 `/users/{users_id}` は `/users/me` としてもマッチします。値が「"me"」であるパラメータ `user_id` を受け取ると「考え」ます。 +それ以外の場合、 `/users/{user_id}` は `/users/me` としてもマッチします。値が `"me"` であるパラメータ `user_id` を受け取ると「考え」ます。 -## 定義済みの値 +同様に、path operation を再定義することはできません: -*パスパラメータ*を受け取る *path operation* をもち、有効な*パスパラメータ*の値を事前に定義したい場合は、標準のPython `Enum` を利用できます。 +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} -### `Enum` クラスの作成 +パスは最初にマッチしたものが常に使われるため、最初のものが常に使用されます。 + +## 定義済みの値 { #predefined-values } + +*パスパラメータ*を受け取る *path operation* をもち、有効な*パスパラメータ*の値を事前に定義したい場合は、標準のPython `Enum` を利用できます。 + +### `Enum` クラスの作成 { #create-an-enum-class } `Enum` をインポートし、 `str` と `Enum` を継承したサブクラスを作成します。 -`str` を継承することで、APIドキュメントは値が `文字列` でなければいけないことを知り、正確にレンダリングできるようになります。 +`str` を継承することで、APIドキュメントは値が `string` 型でなければいけないことを知り、正確にレンダリングできるようになります。 そして、固定値のクラス属性を作ります。すると、その値が使用可能な値となります: -{* ../../docs_src/path_params/tutorial005.py hl[1,6,7,8,9] *} - -/// info | 情報 - -Enumerations (もしくは、enums)はPython 3.4以降で利用できます。 - -/// +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | 豆知識 @@ -147,33 +148,33 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー /// -### *パスパラメータ*の宣言 +### *パスパラメータ*の宣言 { #declare-a-path-parameter } 次に、作成したenumクラスである`ModelName`を使用した型アノテーションをもつ*パスパラメータ*を作成します: -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} -### ドキュメントの確認 +### ドキュメントの確認 { #check-the-docs } *パスパラメータ*の利用可能な値が事前に定義されているので、対話的なドキュメントで適切に表示できます: -### Python*列挙型*の利用 +### Python*列挙型*の利用 { #working-with-python-enumerations } *パスパラメータ*の値は*列挙型メンバ*となります。 -#### *列挙型メンバ*の比較 +#### *列挙型メンバ*の比較 { #compare-enumeration-members } これは、作成した列挙型 `ModelName` の*列挙型メンバ*と比較できます: -{* ../../docs_src/path_params/tutorial005.py hl[17] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *} -#### *列挙値*の取得 +#### *列挙値*の取得 { #get-the-enumeration-value } `model_name.value` 、もしくは一般に、 `your_enum_member.value` を使用して実際の値 (この場合は `str`) を取得できます。 -{* ../../docs_src/path_params/tutorial005.py hl[20] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *} /// tip | 豆知識 @@ -181,13 +182,13 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー /// -#### *列挙型メンバ*の返却 +#### *列挙型メンバ*の返却 { #return-enumeration-members } -*path operation* から*列挙型メンバ*を返すことができます。JSONボディ(`dict` など)でネストすることもできます。 +*path operation* から*列挙型メンバ*を返すことができます。JSONボディ(例: `dict`)でネストすることもできます。 それらはクライアントに返される前に適切な値 (この場合は文字列) に変換されます。 -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} クライアントは以下の様なJSONレスポンスを得ます: @@ -198,23 +199,23 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー } ``` -## パスを含んだパスパラメータ +## パスを含んだパスパラメータ { #path-parameters-containing-paths } パス `/files/{file_path}` となる *path operation* を持っているとしましょう。 ただし、 `home/johndoe/myfile.txt` のような*パス*を含んだ `file_path` が必要です。 -したがって、URLは `/files/home/johndoe/myfile.txt` の様になります。 +したがって、そのファイルのURLは `/files/home/johndoe/myfile.txt` の様になります。 -### OpenAPIサポート +### OpenAPIサポート { #openapi-support } OpenAPIはテストや定義が困難なシナリオにつながる可能性があるため、内部に*パス*を含む*パスパラメータ*の宣言をサポートしていません。 それにも関わらず、Starletteの内部ツールのひとつを使用することで、**FastAPI**はそれが実現できます。 -そして、パラメータがパスを含むべきであることを明示的にドキュメントに追加することなく、機能します。 +そして、パラメータがパスを含むべきであることを示すドキュメントを追加しなくても、ドキュメントは動作します。 -### パス変換 +### パスコンバーター { #path-convertor } Starletteのオプションを直接使用することで、以下のURLの様な*パス*を含んだ、*パスパラメータ*の宣言ができます: @@ -226,17 +227,17 @@ Starletteのオプションを直接使用することで、以下のURLの様 したがって、以下の様に使用できます: -{* ../../docs_src/path_params/tutorial004.py hl[6] *} +{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *} /// tip | 豆知識 -最初のスラッシュ (`/`)が付いている `/home/johndoe/myfile.txt` をパラメータが含んでいる必要があります。 +最初のスラッシュ (`/`)が付いている `/home/johndoe/myfile.txt` をパラメータが含んでいる必要があるかもしれません。 この場合、URLは `files` と `home` の間にダブルスラッシュ (`//`) のある、 `/files//home/johndoe/myfile.txt` になります。 /// -## まとめ +## まとめ { #recap } 簡潔で、本質的で、標準的なPythonの型宣言を使用することで、**FastAPI**は以下を行います: diff --git a/docs/ja/docs/tutorial/query-param-models.md b/docs/ja/docs/tutorial/query-param-models.md index 053d0740b..d892a57d2 100644 --- a/docs/ja/docs/tutorial/query-param-models.md +++ b/docs/ja/docs/tutorial/query-param-models.md @@ -1,8 +1,8 @@ -# クエリパラメータモデル +# クエリパラメータモデル { #query-parameter-models } もし関連する**複数のクエリパラメータ**から成るグループがあるなら、それらを宣言するために、**Pydanticモデル**を作成できます。 -こうすることで、**複数の場所**で**そのPydanticモデルを再利用**でき、バリデーションやメタデータを、すべてのクエリパラメータに対して一度に宣言できます。😎 +こうすることで、**複数の場所**で**そのモデルを再利用**でき、バリデーションやメタデータを、すべてのパラメータに対して一度に宣言できます。😎 /// note | 備考 @@ -10,15 +10,15 @@ /// -## クエリパラメータにPydanticモデルを使用する +## Pydanticモデルを使ったクエリパラメータ { #query-parameters-with-a-pydantic-model } -必要な**複数のクエリパラメータ**を**Pydanticモデル**で宣言し、さらに、それを `Query` として宣言しましょう: +必要な**クエリパラメータ**を**Pydanticモデル**で宣言し、さらに、そのパラメータを `Query` として宣言しましょう: {* ../../docs_src/query_param_models/tutorial001_an_py310.py hl[9:13,17] *} -**FastAPI**は、リクエストの**クエリパラメータ**からそれぞれの**フィールド**のデータを**抽出**し、定義された**Pydanticモデル**を提供します。 +**FastAPI**は、リクエストの**クエリパラメータ**からそれぞれの**フィールド**のデータを**抽出**し、定義したPydanticモデルを提供します。 -## ドキュメントの確認 +## ドキュメントの確認 { #check-the-docs } 対話的APIドキュメント `/docs` でクエリパラメータを確認できます: @@ -26,11 +26,11 @@ -## 余分なクエリパラメータを禁止する +## 余分なクエリパラメータを禁止する { #forbid-extra-query-parameters } -特定の(あまり一般的ではないかもしれない)ケースで、受け付けるクエリパラメータを**制限**する必要があるかもしれません。 +特定の(あまり一般的ではないかもしれない)ユースケースで、受け取りたいクエリパラメータを**制限**したい場合があります。 -Pydanticのモデルの Configuration を利用して、 `extra` フィールドを `forbid` とすることができます。 +Pydanticのモデル設定を使って、あらゆる `extra` フィールドを `forbid` にできます。 {* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} @@ -42,7 +42,7 @@ Pydanticのモデルの Configuration を利用して、 `extra` フィールド https://example.com/items/?limit=10&tool=plumbus ``` -クエリパラメータ `tool` が許可されていないことを通知する**エラー**レスポンスが返されます。 +クエリパラメータ `tool` が許可されていないことを伝える**エラー**レスポンスが返されます。 ```json { @@ -57,7 +57,7 @@ https://example.com/items/?limit=10&tool=plumbus } ``` -## まとめ +## まとめ { #summary } **FastAPI**では、**クエリパラメータ**を宣言するために、**Pydanticモデル**を使用できます。😎 diff --git a/docs/ja/docs/tutorial/query-params-str-validations.md b/docs/ja/docs/tutorial/query-params-str-validations.md index 22b89e452..e230ef29a 100644 --- a/docs/ja/docs/tutorial/query-params-str-validations.md +++ b/docs/ja/docs/tutorial/query-params-str-validations.md @@ -1,120 +1,228 @@ -# クエリパラメータと文字列の検証 +# クエリパラメータと文字列の検証 { #query-parameters-and-string-validations } **FastAPI** ではパラメータの追加情報とバリデーションを宣言することができます。 以下のアプリケーションを例にしてみましょう: -{* ../../docs_src/query_params_str_validations/tutorial001.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} -クエリパラメータ `q` は `Optional[str]` 型で、`None` を許容する `str` 型を意味しており、デフォルトは `None` です。そのため、FastAPIはそれが必須ではないと理解します。 +クエリパラメータ `q` は `str | None` 型で、`str` 型ですが `None` にもなり得ることを意味し、実際にデフォルト値は `None` なので、FastAPIはそれが必須ではないと理解します。 /// note | 備考 -FastAPIは、 `q` はデフォルト値が `=None` であるため、必須ではないと理解します。 +FastAPIは、 `q` はデフォルト値が `= None` であるため、必須ではないと理解します。 -`Optional[str]` における `Optional` はFastAPIには利用されませんが、エディターによるより良いサポートとエラー検出を可能にします。 +`str | None` を使うことで、エディターによるより良いサポートとエラー検出を可能にします。 /// -## バリデーションの追加 +## バリデーションの追加 { #additional-validation } -`q`はオプショナルですが、もし値が渡されてきた場合には、**50文字を超えないこと**を強制してみましょう。 +`q`はオプショナルですが、もし値が渡されてきた場合には、**長さが50文字を超えないこと**を強制してみましょう。 -### `Query`のインポート +### `Query` と `Annotated` のインポート { #import-query-and-annotated } -そのために、まずは`fastapi`から`Query`をインポートします: +そのために、まずは以下をインポートします: -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[3] *} +* `fastapi` から `Query` +* `typing` から `Annotated` -## デフォルト値として`Query`を使用 +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} -パラメータのデフォルト値として使用し、パラメータ`max_length`を50に設定します: +/// info | 情報 -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[9] *} +FastAPI はバージョン 0.95.0 で `Annotated` のサポートを追加し(推奨し始め)ました。 -デフォルト値`None`を`Query(default=None)`に置き換える必要があるので、`Query`の最初の引数はデフォルト値を定義するのと同じです。 +古いバージョンの場合、`Annotated` を使おうとするとエラーになります。 + +`Annotated` を使う前に、FastAPI のバージョンを少なくとも 0.95.1 にするために、[FastAPI のバージョンをアップグレード](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}してください。 + +/// + +## `q` パラメータの型で `Annotated` を使う { #use-annotated-in-the-type-for-the-q-parameter } + +以前、[Python Types Intro](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank} で `Annotated` を使ってパラメータにメタデータを追加できると説明したことを覚えていますか? + +いよいよ FastAPI で使うときです。 🚀 + +次の型アノテーションがありました: + +//// tab | Python 3.10+ + +```Python +q: str | None = None +``` + +//// + +//// tab | Python 3.9+ + +```Python +q: Union[str, None] = None +``` + +//// + +これを `Annotated` で包んで、次のようにします: + +//// tab | Python 3.10+ + +```Python +q: Annotated[str | None] = None +``` + +//// + +//// tab | Python 3.9+ + +```Python +q: Annotated[Union[str, None]] = None +``` + +//// + +どちらも同じ意味で、`q` は `str` または `None` になり得るパラメータで、デフォルトでは `None` です。 + +では、面白いところに進みましょう。 🎉 + +## `q` パラメータの `Annotated` に `Query` を追加する { #add-query-to-annotated-in-the-q-parameter } + +追加情報(この場合は追加のバリデーション)を入れられる `Annotated` ができたので、`Annotated` の中に `Query` を追加し、パラメータ `max_length` を `50` に設定します: + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} + +デフォルト値は引き続き `None` なので、このパラメータは依然としてオプショナルです。 + +しかし、`Annotated` の中に `Query(max_length=50)` を入れることで、この値に **追加のバリデーション** をしたい、最大 50 文字にしたい、と FastAPI に伝えています。 😎 + +/// tip | 豆知識 + +ここでは **クエリパラメータ** なので `Query()` を使っています。後で `Path()`、`Body()`、`Header()`、`Cookie()` など、`Query()` と同じ引数を受け取れるものも見ていきます。 + +/// + +FastAPI は次を行います: + +* 最大長が 50 文字であることを確かめるようデータを **検証** する +* データが有効でないときに、クライアントに **明確なエラー** を表示する +* OpenAPI スキーマの *path operation* にパラメータを **ドキュメント化** する(その結果、**自動ドキュメント UI** に表示されます) + +## 代替(古い方法): デフォルト値としての `Query` { #alternative-old-query-as-the-default-value } + +FastAPI の以前のバージョン(0.95.0 より前)では、パラメータのデフォルト値として `Query` を使う必要があり、`Annotated` の中に入れるのではありませんでした。これを使ったコードを見かける可能性が高いので、説明します。 + +/// tip | 豆知識 + +新しいコードでは、可能な限り上で説明したとおり `Annotated` を使ってください。複数の利点(後述)があり、欠点はありません。 🍰 + +/// + +関数パラメータのデフォルト値として `Query()` を使い、パラメータ `max_length` を 50 に設定する方法は次のとおりです: + +{* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} + +この場合(`Annotated` を使わない場合)、関数内のデフォルト値 `None` を `Query()` に置き換える必要があるため、`Query(default=None)` のパラメータでデフォルト値を設定する必要があります。これは(少なくとも FastAPI にとっては)そのデフォルト値を定義するのと同じ目的を果たします。 なので: ```Python -q: Optional[str] = Query(default=None) +q: str | None = Query(default=None) ``` -...を以下と同じようにパラメータをオプションにします: +...はデフォルト値 `None` を持つオプショナルなパラメータになり、以下と同じです: + ```Python -q: Optional[str] = None +q: str | None = None ``` -しかし、これはクエリパラメータとして明示的に宣言しています。 - -/// info | 情報 - -FastAPIは以下の部分を気にすることを覚えておいてください: - -```Python -= None -``` - -もしくは: - -```Python -= Query(default=None) -``` - -そして、 `None` を利用することでクエリパラメータが必須ではないと検知します。 - -`Optional` の部分は、エディターによるより良いサポートを可能にします。 - -/// +ただし `Query` のバージョンでは、クエリパラメータであることを明示的に宣言しています。 そして、さらに多くのパラメータを`Query`に渡すことができます。この場合、文字列に適用される、`max_length`パラメータを指定します。 ```Python -q: Union[str, None] = Query(default=None, max_length=50) +q: str | None = Query(default=None, max_length=50) ``` これにより、データを検証し、データが有効でない場合は明確なエラーを表示し、OpenAPIスキーマの *path operation* にパラメータを記載します。 -## バリデーションをさらに追加する +### デフォルト値としての `Query` または `Annotated` 内の `Query` { #query-as-the-default-value-or-in-annotated } + +`Annotated` の中で `Query` を使う場合、`Query` の `default` パラメータは使えないことに注意してください。 + +その代わりに、関数パラメータの実際のデフォルト値を使います。そうしないと整合性が取れなくなります。 + +例えば、これは許可されません: + +```Python +q: Annotated[str, Query(default="rick")] = "morty" +``` + +...なぜなら、デフォルト値が `"rick"` なのか `"morty"` なのかが不明確だからです。 + +そのため、(できれば)次のようにします: + +```Python +q: Annotated[str, Query()] = "rick" +``` + +...または、古いコードベースでは次のようなものが見つかるでしょう: + +```Python +q: str = Query(default="rick") +``` + +### `Annotated` の利点 { #advantages-of-annotated } + +関数パラメータのデフォルト値スタイルではなく、**`Annotated` を使うことが推奨** されます。複数の理由で **より良い** からです。 🤓 + +**関数パラメータ** の **デフォルト値** は **実際のデフォルト値** であり、Python 全般としてより直感的です。 😌 + +FastAPI なしで同じ関数を **別の場所** から **呼び出しても**、**期待どおりに動作** します。**必須** パラメータ(デフォルト値がない)があれば、**エディター** がエラーで知らせてくれますし、**Python** も必須パラメータを渡さずに実行すると文句を言います。 + +`Annotated` を使わずに **(古い)デフォルト値スタイル** を使う場合、FastAPI なしでその関数を **別の場所** で呼び出すとき、正しく動かすために関数へ引数を渡すことを **覚えておく** 必要があります。そうしないと値が期待と異なります(例えば `str` の代わりに `QueryInfo` か、それに類するものになります)。また、エディターも警告せず、Python もその関数の実行で文句を言いません。内部の処理がエラーになるときに初めて問題が出ます。 + +`Annotated` は複数のメタデータアノテーションを持てるので、Typer のような別ツールと同じ関数を使うこともできます。 🚀 + +## バリデーションをさらに追加する { #add-more-validations } パラメータ`min_length`も追加することができます: -{* ../../docs_src/query_params_str_validations/tutorial003.py hl[10] *} +{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} -## 正規表現の追加 +## 正規表現の追加 { #add-regular-expressions } -パラメータが一致するべき正規表現を定義することができます: +パラメータが一致するべき 正規表現 `pattern` を定義することができます: -{* ../../docs_src/query_params_str_validations/tutorial004.py hl[11] *} +{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} -この特定の正規表現は受け取ったパラメータの値をチェックします: +この特定の正規表現パターンは受け取ったパラメータの値をチェックします: * `^`: は、これ以降の文字で始まり、これより以前には文字はありません。 * `fixedquery`: は、正確な`fixedquery`を持っています. * `$`: で終わる場合、`fixedquery`以降には文字はありません. -もしこれらすべての **正規表現**のアイデアについて迷っていても、心配しないでください。多くの人にとって難しい話題です。正規表現を必要としなくても、まだ、多くのことができます。 +もしこれらすべての **「正規表現」** のアイデアについて迷っていても、心配しないでください。多くの人にとって難しい話題です。正規表現を必要としなくても、まだ、多くのことができます。 -しかし、あなたがそれらを必要とし、学ぶときにはすでに、 **FastAPI**で直接それらを使用することができます。 +これで、必要になったときにはいつでも **FastAPI** で使えることが分かりました。 -## デフォルト値 +## デフォルト値 { #default-values } -第一引数に`None`を渡して、デフォルト値として使用するのと同じように、他の値を渡すこともできます。 +もちろん、`None` 以外のデフォルト値も使えます。 -クエリパラメータ`q`の`min_length`を`3`とし、デフォルト値を`fixedquery`としてみましょう: +クエリパラメータ `q` の `min_length` を `3` とし、デフォルト値を `"fixedquery"` として宣言したいとします: -{* ../../docs_src/query_params_str_validations/tutorial005.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *} /// note | 備考 -デフォルト値を指定すると、パラメータは任意になります。 +`None` を含む任意の型のデフォルト値があると、パラメータはオプショナル(必須ではない)になります。 /// -## 必須にする +## 必須パラメータ { #required-parameters } -これ以上、バリデーションやメタデータを宣言する必要のない場合は、デフォルト値を指定しないだけでクエリパラメータ`q`を必須にすることができます。以下のように: +これ以上、バリデーションやメタデータを宣言する必要がない場合は、デフォルト値を宣言しないだけでクエリパラメータ `q` を必須にできます。以下のように: ```Python q: str @@ -123,42 +231,42 @@ q: str 以下の代わりに: ```Python -q: Union[str, None] = None +q: str | None = None ``` -現在は以下の例のように`Query`で宣言しています: +しかし今は、例えば次のように `Query` で宣言しています: ```Python -q: Union[str, None] = Query(default=None, min_length=3) +q: Annotated[str | None, Query(min_length=3)] = None ``` -そのため、`Query`を使用して必須の値を宣言する必要がある場合は、第一引数に`...`を使用することができます: +そのため、`Query` を使いながら値を必須として宣言したい場合は、単にデフォルト値を宣言しません: -{* ../../docs_src/query_params_str_validations/tutorial006.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -/// info | 情報 +### 必須、`None` にできる { #required-can-be-none } -これまで`...`を見たことがない方へ: これは特殊な単一値です。Pythonの一部であり、"Ellipsis"と呼ばれています。 +パラメータが `None` を受け付けるが、それでも必須である、と宣言できます。これにより、値が `None` であってもクライアントは値を送らなければならなくなります。 -/// +そのために、`None` が有効な型であることを宣言しつつ、単にデフォルト値を宣言しません: -これは **FastAPI** にこのパラメータが必須であることを知らせます。 +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} -## クエリパラメータのリスト / 複数の値 +## クエリパラメータのリスト / 複数の値 { #query-parameter-list-multiple-values } -クエリパラメータを明示的に`Query`で宣言した場合、値のリストを受け取るように宣言したり、複数の値を受け取るように宣言したりすることもできます。 +クエリパラメータを明示的に `Query` で定義すると、値のリストを受け取るように宣言したり、言い換えると複数の値を受け取るように宣言したりすることもできます。 例えば、URL内に複数回出現するクエリパラメータ`q`を宣言するには以下のように書きます: -{* ../../docs_src/query_params_str_validations/tutorial011.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *} -そしてURLは以下です: +そして、次のような URL なら: ``` http://localhost:8000/items/?q=foo&q=bar ``` -複数の*クエリパラメータ*の値`q`(`foo`と`bar`)を*path operation関数*内で*関数パラメータ*`q`としてPythonの`list`を受け取ることになります。 +*path operation function* 内の *function parameter* `q` で、複数の `q` *query parameters'* 値(`foo` と `bar`)を Python の `list` として受け取ります。 そのため、このURLのレスポンスは以下のようになります: @@ -179,15 +287,15 @@ http://localhost:8000/items/?q=foo&q=bar 対話的APIドキュメントは複数の値を許可するために自動的に更新されます。 - + -### デフォルト値を持つ、クエリパラメータのリスト / 複数の値 +### デフォルト値を持つ、クエリパラメータのリスト / 複数の値 { #query-parameter-list-multiple-values-with-defaults } -また、値が指定されていない場合はデフォルトの`list`を定義することもできます。 +また、値が指定されていない場合はデフォルトの `list` を定義することもできます。 -{* ../../docs_src/query_params_str_validations/tutorial012.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} -以下のURLを開くと: +以下にアクセスすると: ``` http://localhost:8000/items/ @@ -204,21 +312,21 @@ http://localhost:8000/items/ } ``` -#### `list`を使う +#### `list` だけを使う { #using-just-list } -`List[str]`の代わりに直接`list`を使うこともできます: +`list[str]` の代わりに直接 `list` を使うこともできます: -{* ../../docs_src/query_params_str_validations/tutorial013.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} /// note | 備考 この場合、FastAPIはリストの内容をチェックしないことを覚えておいてください。 -例えば`List[int]`はリストの内容が整数であるかどうかをチェックします(そして、文書化します)。しかし`list`だけではそうしません。 +例えば`list[int]`はリストの内容が整数であるかどうかをチェックします(そして、文書化します)。しかし`list`だけではそうしません。 /// -## より多くのメタデータを宣言する +## より多くのメタデータを宣言する { #declare-more-metadata } パラメータに関する情報をさらに追加することができます。 @@ -234,13 +342,13 @@ http://localhost:8000/items/ `title`を追加できます: -{* ../../docs_src/query_params_str_validations/tutorial007.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *} `description`を追加できます: -{* ../../docs_src/query_params_str_validations/tutorial008.py hl[13] *} +{* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} -## エイリアスパラメータ +## エイリアスパラメータ { #alias-parameters } パラメータに`item-query`を指定するとします. @@ -258,23 +366,91 @@ http://127.0.0.1:8000/items/?item-query=foobaritems それならば、`alias`を宣言することができます。エイリアスはパラメータの値を見つけるのに使用されます: -{* ../../docs_src/query_params_str_validations/tutorial009.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} -## 非推奨パラメータ +## パラメータを非推奨にする { #deprecating-parameters } -さて、このパラメータが気に入らなくなったとしましょう +さて、このパラメータが気に入らなくなったとしましょう。 -それを使っているクライアントがいるので、しばらくは残しておく必要がありますが、ドキュメントには非推奨と明記しておきたいです。 +それを使っているクライアントがいるので、しばらくは残しておく必要がありますが、ドキュメントにはdeprecatedと明記しておきたいです。 その場合、`Query`にパラメータ`deprecated=True`を渡します: -{* ../../docs_src/query_params_str_validations/tutorial010.py hl[18] *} +{* ../../docs_src/query_params_str_validations/tutorial010_an_py310.py hl[19] *} ドキュメントは以下のようになります: - + -## まとめ +## OpenAPI からパラメータを除外する { #exclude-parameters-from-openapi } + +生成される OpenAPI スキーマ(つまり自動ドキュメントシステム)からクエリパラメータを除外するには、`Query` のパラメータ `include_in_schema` を `False` に設定します: + +{* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} + +## カスタムバリデーション { #custom-validation } + +上で示したパラメータではできない **カスタムバリデーション** が必要になる場合があります。 + +その場合、通常のバリデーション(例: 値が `str` であることの検証)の後に適用される **カスタムバリデータ関数** を使えます。 + +これを行うには、`Annotated` の中で Pydantic の `AfterValidator` を使います。 + +/// tip | 豆知識 + +Pydantic には `BeforeValidator` などもあります。 🤓 + +/// + +例えば、このカスタムバリデータは、ISBN の書籍番号なら item ID が `isbn-` で始まること、IMDB の movie URL ID なら `imdb-` で始まることをチェックします: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} + +/// info | 情報 + +これは Pydantic バージョン 2 以上で利用できます。 😎 + +/// + +/// tip | 豆知識 + +データベースや別の API など、何らかの **外部コンポーネント** との通信が必要なタイプのバリデーションを行う必要がある場合は、代わりに **FastAPI Dependencies** を使うべきです。これについては後で学びます。 + +これらのカスタムバリデータは、リクエストで提供された **同じデータのみ** でチェックできるもの向けです。 + +/// + +### そのコードを理解する { #understand-that-code } + +重要なのは、**`Annotated` の中で関数と一緒に `AfterValidator` を使うこと** だけです。この部分は飛ばしても構いません。 🤸 + +--- + +ただし、この具体的なコード例が気になっていて、まだ興味が続くなら、追加の詳細を示します。 + +#### `value.startswith()` を使う文字列 { #string-with-value-startswith } + +気づきましたか?`value.startswith()` を使う文字列はタプルを受け取れ、そのタプル内の各値をチェックします: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *} + +#### ランダムなアイテム { #a-random-item } + +`data.items()` で、辞書の各アイテムのキーと値を含むタプルを持つ 反復可能オブジェクト を取得します。 + +この反復可能オブジェクトを `list(data.items())` で適切な `list` に変換します。 + +そして `random.choice()` でその `list` から **ランダムな値** を取得するので、`(id, name)` のタプルを得ます。これは `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")` のようになります。 + +次に、そのタプルの **2つの値を代入** して、変数 `id` と `name` に入れます。 + +つまり、ユーザーが item ID を提供しなかった場合でも、ランダムな提案を受け取ります。 + +...これを **単一のシンプルな1行** で行っています。 🤯 Python が好きになりませんか? 🐍 + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} + +## まとめ { #recap } パラメータに追加のバリデーションとメタデータを宣言することができます。 @@ -285,12 +461,14 @@ http://127.0.0.1:8000/items/?item-query=foobaritems * `description` * `deprecated` -文字列のためのバリデーション: +文字列に固有のバリデーション: * `min_length` * `max_length` -* `regex` +* `pattern` -この例では、`str`の値のバリデーションを宣言する方法を見てきました。 +`AfterValidator` を使ったカスタムバリデーション。 + +この例では、`str` の値のバリデーションを宣言する方法を見てきました。 数値のような他の型のバリデーションを宣言する方法は次の章を参照してください。 diff --git a/docs/ja/docs/tutorial/query-params.md b/docs/ja/docs/tutorial/query-params.md index 74e455579..41e51ed78 100644 --- a/docs/ja/docs/tutorial/query-params.md +++ b/docs/ja/docs/tutorial/query-params.md @@ -1,8 +1,8 @@ -# クエリパラメータ +# クエリパラメータ { #query-parameters } -パスパラメータではない関数パラメータを宣言すると、それらは自動的に "クエリ" パラメータとして解釈されます。 +パスパラメータではない関数パラメータを宣言すると、それらは自動的に「クエリ」パラメータとして解釈されます。 -{* ../../docs_src/query_params/tutorial001.py hl[9] *} +{* ../../docs_src/query_params/tutorial001_py39.py hl[9] *} クエリはURL内で `?` の後に続くキーとバリューの組で、 `&` で区切られています。 @@ -24,11 +24,11 @@ http://127.0.0.1:8000/items/?skip=0&limit=10 パスパラメータに適用される処理と完全に同様な処理がクエリパラメータにも施されます: * エディターサポート (明らかに) -* データ「解析」 +* データ 「解析」 * データバリデーション * 自動ドキュメント生成 -## デフォルト +## デフォルト { #defaults } クエリパラメータはパスの固定部分ではないので、オプショナルとしたり、デフォルト値をもつことができます。 @@ -55,13 +55,13 @@ http://127.0.0.1:8000/items/?skip=20 関数内のパラメータの値は以下の様になります: * `skip=20`: URL内にセットしたため -* `limit=10`: デフォルト値 +* `limit=10`: デフォルト値だったため -## オプショナルなパラメータ +## オプショナルなパラメータ { #optional-parameters } 同様に、デフォルト値を `None` とすることで、オプショナルなクエリパラメータを宣言できます: -{* ../../docs_src/query_params/tutorial002.py hl[9] *} +{* ../../docs_src/query_params/tutorial002_py310.py hl[7] *} この場合、関数パラメータ `q` はオプショナルとなり、デフォルトでは `None` になります。 @@ -71,11 +71,11 @@ http://127.0.0.1:8000/items/?skip=20 /// -## クエリパラメータの型変換 +## クエリパラメータの型変換 { #query-parameter-type-conversion } -`bool` 型も宣言できます。これは以下の様に変換されます: +`bool` 型も宣言でき、変換されます: -{* ../../docs_src/query_params/tutorial003.py hl[9] *} +{* ../../docs_src/query_params/tutorial003_py310.py hl[7] *} この場合、以下にアクセスすると: @@ -109,27 +109,28 @@ http://127.0.0.1:8000/items/foo?short=yes もしくは、他の大文字小文字のバリエーション (アッパーケース、最初の文字だけアッパーケース、など)で、関数は `short` パラメータを `True` な `bool` 値として扱います。それ以外は `False` になります。 -## 複数のパスパラメータとクエリパラメータ -複数のパスパラメータとクエリパラメータを同時に宣言できます。**FastAPI**は互いを区別できます。 +## 複数のパスパラメータとクエリパラメータ { #multiple-path-and-query-parameters } + +複数のパスパラメータとクエリパラメータを同時に宣言できます。**FastAPI**はどれがどれかを把握しています。 そして特定の順序で宣言しなくてもよいです。 名前で判別されます: -{* ../../docs_src/query_params/tutorial004.py hl[8,10] *} +{* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## 必須のクエリパラメータ +## 必須のクエリパラメータ { #required-query-parameters } -パスパラメータ以外のパラメータ (今のところ、クエリパラメータのみ説明しました) のデフォルト値を宣言した場合、そのパラメータは必須ではなくなります。 +パスパラメータ以外のパラメータ (今のところ、クエリパラメータのみ見てきました) のデフォルト値を宣言した場合、そのパラメータは必須ではなくなります。 特定の値を与えずにただオプショナルにしたい場合はデフォルト値を `None` にして下さい。 しかしクエリパラメータを必須にしたい場合は、ただデフォルト値を宣言しなければよいです: -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} -ここで、クエリパラメータ `needy` は `str` 型の必須のクエリパラメータです +ここで、クエリパラメータ `needy` は `str` 型の必須のクエリパラメータです。 以下のURLをブラウザで開くと: @@ -141,16 +142,17 @@ http://127.0.0.1:8000/items/foo-item ```JSON { - "detail": [ - { - "loc": [ - "query", - "needy" - ], - "msg": "field required", - "type": "value_error.missing" - } - ] + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "needy" + ], + "msg": "Field required", + "input": null + } + ] } ``` @@ -169,9 +171,9 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy } ``` -そして当然、あるパラメータを必須に、別のパラメータにデフォルト値を設定し、また別のパラメータをオプショナルにできます: +そして当然、あるパラメータを必須に、あるパラメータにデフォルト値を設定し、またあるパラメータを完全にオプショナルにできます: -{* ../../docs_src/query_params/tutorial006.py hl[10] *} +{* ../../docs_src/query_params/tutorial006_py310.py hl[8] *} この場合、3つのクエリパラメータがあります。: @@ -181,6 +183,6 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy /// tip | 豆知識 -[パスパラメータ](path-params.md#_8){.internal-link target=_blank}と同様に `Enum` を使用できます。 +[パスパラメータ](path-params.md#predefined-values){.internal-link target=_blank}と同様に `Enum` を使用できます。 /// diff --git a/docs/ja/docs/tutorial/request-forms-and-files.md b/docs/ja/docs/tutorial/request-forms-and-files.md index 110e3106a..09e1277c8 100644 --- a/docs/ja/docs/tutorial/request-forms-and-files.md +++ b/docs/ja/docs/tutorial/request-forms-and-files.md @@ -1,24 +1,28 @@ -# リクエストフォームとファイル +# リクエストフォームとファイル { #request-forms-and-files } `File`と`Form`を同時に使うことでファイルとフォームフィールドを定義することができます。 /// info | 情報 -アップロードされたファイルやフォームデータを受信するには、まず`python-multipart`をインストールします。 +アップロードされたファイルやフォームデータを受信するには、まず`python-multipart`をインストールします。 -例えば、`pip install python-multipart`のように。 +[仮想環境](../virtual-environments.md){.internal-link target=_blank}を作成し、それを有効化してから、例えば次のようにインストールしてください: + +```console +$ pip install python-multipart +``` /// -## `File`と`Form`のインポート +## `File`と`Form`のインポート { #import-file-and-form } -{* ../../docs_src/request_forms_and_files/tutorial001.py hl[1] *} +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[3] *} -## `File`と`Form`のパラメータの定義 +## `File`と`Form`のパラメータの定義 { #define-file-and-form-parameters } ファイルやフォームのパラメータは`Body`や`Query`の場合と同じように作成します: -{* ../../docs_src/request_forms_and_files/tutorial001.py hl[8] *} +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[10:12] *} ファイルとフォームフィールドがフォームデータとしてアップロードされ、ファイルとフォームフィールドを受け取ります。 @@ -32,6 +36,6 @@ /// -## まとめ +## まとめ { #recap } 同じリクエストでデータやファイルを受け取る必要がある場合は、`File` と`Form`を一緒に使用します。 diff --git a/docs/ja/docs/tutorial/request-forms.md b/docs/ja/docs/tutorial/request-forms.md index eca2cd6dc..1bdc28670 100644 --- a/docs/ja/docs/tutorial/request-forms.md +++ b/docs/ja/docs/tutorial/request-forms.md @@ -1,4 +1,4 @@ -# フォームデータ +# フォームデータ { #form-data } JSONの代わりにフィールドを受け取る場合は、`Form`を使用します。 @@ -6,27 +6,31 @@ JSONの代わりにフィールドを受け取る場合は、`Form`を使用し フォームを使うためには、まず`python-multipart`をインストールします。 -たとえば、`pip install python-multipart`のように。 +必ず[仮想環境](../virtual-environments.md){.internal-link target=_blank}を作成して有効化してから、例えば次のようにインストールしてください: + +```console +$ pip install python-multipart +``` /// -## `Form`のインポート +## `Form`のインポート { #import-form } `fastapi`から`Form`をインポートします: -{* ../../docs_src/request_forms/tutorial001.py hl[1] *} +{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} -## `Form`のパラメータの定義 +## `Form`のパラメータの定義 { #define-form-parameters } `Body`や`Query`の場合と同じようにフォームパラメータを作成します: -{* ../../docs_src/request_forms/tutorial001.py hl[7] *} +{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[9] *} 例えば、OAuth2仕様が使用できる方法の1つ(「パスワードフロー」と呼ばれる)では、フォームフィールドとして`username`と`password`を送信する必要があります。 -仕様では、フィールドの名前が`username`と`password`であることと、JSONではなくフォームフィールドとして送信されることを要求しています。 +specでは、フィールドの名前が`username`と`password`であることと、JSONではなくフォームフィールドとして送信されることを要求しています。 -`Form`では`Body`(および`Query`や`Path`、`Cookie`)と同じメタデータとバリデーションを宣言することができます。 +`Form`では`Body`(および`Query`や`Path`、`Cookie`)と同じ設定を宣言することができます。これには、バリデーション、例、エイリアス(例えば`username`の代わりに`user-name`)などが含まれます。 /// info | 情報 @@ -40,7 +44,7 @@ JSONの代わりにフィールドを受け取る場合は、`Form`を使用し /// -## 「フォームフィールド」について +## 「フォームフィールド」について { #about-form-fields } HTMLフォーム(`
`)がサーバにデータを送信する方法は、通常、そのデータに「特別な」エンコーディングを使用していますが、これはJSONとは異なります。 @@ -64,6 +68,6 @@ HTMLフォーム(`
`)がサーバにデータを送信する方 /// -## まとめ +## まとめ { #recap } フォームデータの入力パラメータを宣言するには、`Form`を使用する。 diff --git a/docs/ja/docs/tutorial/response-model.md b/docs/ja/docs/tutorial/response-model.md index b8464a4c7..258eac8e6 100644 --- a/docs/ja/docs/tutorial/response-model.md +++ b/docs/ja/docs/tutorial/response-model.md @@ -1,6 +1,35 @@ -# レスポンスモデル +# レスポンスモデル - 戻り値の型 { #response-model-return-type } -*path operations* のいずれにおいても、`response_model`パラメータを使用して、レスポンスのモデルを宣言することができます: +*path operation 関数*の**戻り値の型**にアノテーションを付けることで、レスポンスに使用される型を宣言できます。 + +関数**パラメータ**の入力データと同じように **型アノテーション** を使用できます。Pydanticモデル、リスト、辞書、整数や真偽値などのスカラー値を使用できます。 + +{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} + +FastAPIはこの戻り値の型を使って以下を行います: + +* 返却データを**検証**します。 + * データが不正(例: フィールドが欠けている)であれば、それは*あなた*のアプリコードが壊れていて、返すべきものを返していないことを意味し、不正なデータを返す代わりにサーバーエラーを返します。これにより、あなたとクライアントは、期待されるデータとデータ形状を受け取れることを確実にできます。 +* OpenAPIの *path operation* に、レスポンス用の **JSON Schema** を追加します。 + * これは**自動ドキュメント**で使用されます。 + * 自動クライアントコード生成ツールでも使用されます。 + +しかし、最も重要なのは: + +* 戻り値の型で定義された内容に合わせて、出力データを**制限しフィルタリング**します。 + * これは**セキュリティ**の観点で特に重要です。以下で詳しく見ていきます。 + +## `response_model`パラメータ { #response-model-parameter } + +型が宣言している内容とまったく同じではないデータを返す必要がある、またはそうしたいケースがあります。 + +例えば、**辞書を返す**、またはデータベースオブジェクトを返したいが、**Pydanticモデルとして宣言**したい場合があります。こうすることで、Pydanticモデルが返したオブジェクト(例: 辞書やデータベースオブジェクト)のドキュメント化、バリデーションなどをすべて行ってくれます。 + +戻り値の型アノテーションを追加すると、ツールやエディタが(正しく)エラーとして、関数が宣言した型(例: Pydanticモデル)とは異なる型(例: dict)を返していると警告します。 + +そのような場合、戻り値の型の代わりに、*path operation デコレータ*のパラメータ `response_model` を使用できます。 + +`response_model`パラメータは、いずれの *path operation* でも使用できます: * `@app.get()` * `@app.post()` @@ -8,104 +37,211 @@ * `@app.delete()` * など。 -{* ../../docs_src/response_model/tutorial001.py hl[17] *} +{* ../../docs_src/response_model/tutorial001_py310.py hl[17,22,24:27] *} /// note | 備考 -`response_model`は「デコレータ」メソッド(`get`、`post`など)のパラメータであることに注意してください。すべてのパラメータやボディのように、*path operation関数* のパラメータではありません。 +`response_model`は「デコレータ」メソッド(`get`、`post`など)のパラメータであることに注意してください。すべてのパラメータやボディのように、*path operation 関数* のパラメータではありません。 /// -Pydanticモデルの属性に対して宣言するのと同じ型を受け取るので、Pydanticモデルになることもできますが、例えば、`List[Item]`のようなPydanticモデルの`list`になることもできます。 +`response_model`は、Pydanticモデルのフィールドで宣言するのと同じ型を受け取ります。そのため、Pydanticモデルにもできますし、例えば `List[Item]` のように、Pydanticモデルの `list` にもできます。 -FastAPIは`response_model`を使って以下のことをします: +FastAPIはこの `response_model` を使って、データのドキュメント化や検証などを行い、さらに出力データを型宣言に合わせて**変換・フィルタリング**します。 -* 出力データを型宣言に変換します。 -* データを検証します。 -* OpenAPIの *path operation* で、レスポンス用のJSON Schemaを追加します。 -* 自動ドキュメントシステムで使用されます。 +/// tip | 豆知識 -しかし、最も重要なのは: +エディタやmypyなどで厳密な型チェックをしている場合、関数の戻り値の型を `Any` として宣言できます。 -* 出力データをモデルのデータに限定します。これがどのように重要なのか以下で見ていきましょう。 - -/// note | 技術詳細 - -レスポンスモデルは、関数の戻り値のアノテーションではなく、このパラメータで宣言されています。なぜなら、パス関数は実際にはそのレスポンスモデルを返すのではなく、`dict`やデータベースオブジェクト、あるいは他のモデルを返し、`response_model`を使用してフィールドの制限やシリアライズを行うからです。 +そうすると、意図的に何でも返していることをエディタに伝えられます。それでもFastAPIは `response_model` を使って、データのドキュメント化、検証、フィルタリングなどを行います。 /// -## 同じ入力データの返却 +### `response_model`の優先順位 { #response-model-priority } -ここでは`UserIn`モデルを宣言しています。それには平文のパスワードが含まれています: +戻り値の型と `response_model` の両方を宣言した場合、`response_model` が優先され、FastAPIで使用されます。 -{* ../../docs_src/response_model/tutorial002.py hl[9,11] *} +これにより、レスポンスモデルとは異なる型を返している場合でも、エディタやmypyなどのツールで使用するために関数へ正しい型アノテーションを追加できます。それでもFastAPIは `response_model` を使用してデータの検証やドキュメント化などを実行できます。 + +また `response_model=None` を使用して、その*path operation*のレスポンスモデル生成を無効化することもできます。これは、Pydanticのフィールドとして有効ではないものに対して型アノテーションを追加する場合に必要になることがあります。以下のセクションのいずれかで例を示します。 + +## 同じ入力データの返却 { #return-the-same-input-data } + +ここでは `UserIn` モデルを宣言しています。これには平文のパスワードが含まれます: + +{* ../../docs_src/response_model/tutorial002_py310.py hl[7,9] *} + +/// info | 情報 + +`EmailStr` を使用するには、最初に `email-validator` をインストールしてください。 + +[仮想環境](../virtual-environments.md){.internal-link target=_blank}を作成して有効化してから、例えば次のようにインストールしてください: + +```console +$ pip install email-validator +``` + +または次のようにします: + +```console +$ pip install "pydantic[email]" +``` + +/// そして、このモデルを使用して入力を宣言し、同じモデルを使って出力を宣言しています: -{* ../../docs_src/response_model/tutorial002.py hl[17,18] *} +{* ../../docs_src/response_model/tutorial002_py310.py hl[16] *} これで、ブラウザがパスワードを使ってユーザーを作成する際に、APIがレスポンスで同じパスワードを返すようになりました。 -この場合、ユーザー自身がパスワードを送信しているので問題ないかもしれません。 +この場合、同じユーザーがパスワードを送信しているので問題ないかもしれません。 -しかし、同じモデルを別の*path operation*に使用すると、すべてのクライアントにユーザーのパスワードを送信してしまうことになります。 +しかし、同じモデルを別の*path operation*に使用すると、すべてのクライアントにユーザーのパスワードを送信してしまう可能性があります。 -/// danger | 危険 +/// danger | 警告 -ユーザーの平文のパスワードを保存したり、レスポンスで送信したりすることは絶対にしないでください。 +すべての注意点を理解していて、自分が何をしているか分かっている場合を除き、ユーザーの平文のパスワードを保存したり、このようにレスポンスで送信したりしないでください。 /// -## 出力モデルの追加 +## 出力モデルの追加 { #add-an-output-model } -代わりに、平文のパスワードを持つ入力モデルと、パスワードを持たない出力モデルを作成することができます: +代わりに、平文のパスワードを持つ入力モデルと、パスワードを持たない出力モデルを作成できます: -{* ../../docs_src/response_model/tutorial003.py hl[9,11,16] *} +{* ../../docs_src/response_model/tutorial003_py310.py hl[9,11,16] *} -ここでは、*path operation関数*がパスワードを含む同じ入力ユーザーを返しているにもかかわらず: +ここでは、*path operation 関数*がパスワードを含む同じ入力ユーザーを返しているにもかかわらず: -{* ../../docs_src/response_model/tutorial003.py hl[24] *} +{* ../../docs_src/response_model/tutorial003_py310.py hl[24] *} -...`response_model`を`UserOut`と宣言したことで、パスワードが含まれていません: +...`response_model`を、パスワードを含まない `UserOut` モデルとして宣言しました: -{* ../../docs_src/response_model/tutorial003.py hl[22] *} +{* ../../docs_src/response_model/tutorial003_py310.py hl[22] *} -そのため、**FastAPI** は出力モデルで宣言されていない全てのデータをフィルタリングしてくれます(Pydanticを使用)。 +そのため、**FastAPI** は出力モデルで宣言されていないすべてのデータをフィルタリングしてくれます(Pydanticを使用)。 -## ドキュメントを見る +### `response_model`または戻り値の型 { #response-model-or-return-type } -自動ドキュメントを見ると、入力モデルと出力モデルがそれぞれ独自のJSON Schemaを持っていることが確認できます。 +このケースでは2つのモデルが異なるため、関数の戻り値の型を `UserOut` としてアノテーションすると、エディタやツールは、異なるクラスなので不正な型を返していると警告します。 - +そのため、この例では `response_model` パラメータで宣言する必要があります。 -そして、両方のモデルは、対話型のAPIドキュメントに使用されます: +...しかし、これを解決する方法を以下で確認しましょう。 - +## 戻り値の型とデータフィルタリング { #return-type-and-data-filtering } -## レスポンスモデルのエンコーディングパラメータ +前の例から続けます。**関数に1つの型をアノテーション**したい一方で、関数からは実際には**より多くのデータ**を含むものを返せるようにしたいとします。 -レスポンスモデルにはデフォルト値を設定することができます: +FastAPIにはレスポンスモデルを使用してデータを**フィルタリング**し続けてほしいです。つまり、関数がより多くのデータを返しても、レスポンスにはレスポンスモデルで宣言されたフィールドのみが含まれます。 -{* ../../docs_src/response_model/tutorial004.py hl[11,13,14] *} +前の例ではクラスが異なるため `response_model` パラメータを使う必要がありました。しかしそれは、エディタやツールによる関数の戻り値の型チェックのサポートを受けられないことも意味します。 -* `description: str = None`は`None`がデフォルト値です。 -* `tax: float = 10.5`は`10.5`がデフォルト値です。 -* `tags: List[str] = []` は空のリスト(`[]`)がデフォルト値です。 +しかし、このようなことが必要になる多くのケースでは、この例のようにモデルでデータの一部を**フィルタ/削除**したいだけです。 -しかし、実際に保存されていない場合には結果からそれらを省略した方が良いかもしれません。 +そのような場合、クラスと継承を利用して関数の**型アノテーション**を活用し、エディタやツールのサポートを改善しつつ、FastAPIの**データフィルタリング**も得られます。 + +{* ../../docs_src/response_model/tutorial003_01_py310.py hl[7:10,13:14,18] *} + +これにより、このコードは型として正しいためエディタやmypyからのツール支援を得られますし、FastAPIによるデータフィルタリングも得られます。 + +これはどのように動作するのでしょうか?確認してみましょう。🤓 + +### 型アノテーションとツール支援 { #type-annotations-and-tooling } + +まず、エディタ、mypy、その他のツールがこれをどう見るかを見てみます。 + +`BaseUser` には基本フィールドがあります。次に `UserIn` が `BaseUser` を継承して `password` フィールドを追加するため、両方のモデルのフィールドがすべて含まれます。 + +関数の戻り値の型を `BaseUser` としてアノテーションしますが、実際には `UserIn` インスタンスを返しています。 + +エディタやmypyなどのツールはこれに文句を言いません。typingの観点では、`UserIn` は `BaseUser` のサブクラスであり、期待されるものが `BaseUser` であれば `UserIn` は*有効*な型だからです。 + +### FastAPIのデータフィルタリング { #fastapi-data-filtering } + +一方FastAPIでは、戻り値の型を見て、返す内容にその型で宣言されたフィールド**だけ**が含まれることを確認します。 + +FastAPIは、返却データのフィルタリングにクラス継承の同じルールが使われてしまわないようにするため、内部でPydanticを使っていくつかの処理を行っています。そうでないと、期待以上に多くのデータを返してしまう可能性があります。 + +この方法で、**ツール支援**付きの型アノテーションと**データフィルタリング**の両方という、いいとこ取りができます。 + +## ドキュメントを見る { #see-it-in-the-docs } + +自動ドキュメントを見ると、入力モデルと出力モデルがそれぞれ独自のJSON Schemaを持っていることが確認できます: + + + +そして、両方のモデルは対話型のAPIドキュメントに使用されます: + + + +## その他の戻り値の型アノテーション { #other-return-type-annotations } + +Pydanticフィールドとして有効ではないものを返し、ツール(エディタやmypyなど)が提供するサポートを得るためだけに、関数でそれをアノテーションするケースがあるかもしれません。 + +### レスポンスを直接返す { #return-a-response-directly } + +最も一般的なケースは、[高度なドキュメントで後述する「Responseを直接返す」](../advanced/response-directly.md){.internal-link target=_blank}場合です。 + +{* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} + +このシンプルなケースは、戻り値の型アノテーションが `Response` のクラス(またはサブクラス)であるため、FastAPIが自動的に処理します。 + +また `RedirectResponse` と `JSONResponse` の両方は `Response` のサブクラスなので、ツールも型アノテーションが正しいとして問題にしません。 + +### `Response`のサブクラスをアノテーションする { #annotate-a-response-subclass } + +型アノテーションで `Response` のサブクラスを使うこともできます: + +{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} + +これは `RedirectResponse` が `Response` のサブクラスであり、FastAPIがこのシンプルなケースを自動処理するため、同様に動作します。 + +### 無効な戻り値の型アノテーション { #invalid-return-type-annotations } + +しかし、Pydantic型として有効ではない別の任意のオブジェクト(例: データベースオブジェクト)を返し、関数でそのようにアノテーションすると、FastAPIはその型アノテーションからPydanticレスポンスモデルを作成しようとして失敗します。 + +同様に、unionのように、複数の型のうち1つ以上がPydantic型として有効でないものを含む場合も起こります。例えば次は失敗します 💥: + +{* ../../docs_src/response_model/tutorial003_04_py310.py hl[8] *} + +...これは、型アノテーションがPydantic型ではなく、単一の `Response` クラス(またはサブクラス)でもないために失敗します。`Response` と `dict` の間のunion(どちらか)になっているからです。 + +### レスポンスモデルを無効化する { #disable-response-model } + +上の例を続けると、FastAPIが実行するデフォルトのデータ検証、ドキュメント化、フィルタリングなどを行いたくないこともあるでしょう。 + +しかし、エディタや型チェッカー(例: mypy)などのツール支援を得るために、関数の戻り値の型アノテーションは残したいかもしれません。 + +その場合、`response_model=None` を設定することでレスポンスモデルの生成を無効にできます: + +{* ../../docs_src/response_model/tutorial003_05_py310.py hl[7] *} + +これによりFastAPIはレスポンスモデル生成をスキップし、FastAPIアプリケーションに影響させずに必要な戻り値の型アノテーションを付けられます。🤓 + +## レスポンスモデルのエンコーディングパラメータ { #response-model-encoding-parameters } + +レスポンスモデルには次のようにデフォルト値を設定できます: + +{* ../../docs_src/response_model/tutorial004_py310.py hl[9,11:12] *} + +* `description: Union[str, None] = None`(またはPython 3.10では `str | None = None`)はデフォルトが `None` です。 +* `tax: float = 10.5` はデフォルトが `10.5` です。 +* `tags: List[str] = []` はデフォルトが空のリスト `[]` です。 + +ただし、それらが実際には保存されていない場合、結果から省略したいことがあります。 例えば、NoSQLデータベースに多くのオプション属性を持つモデルがあるが、デフォルト値でいっぱいの非常に長いJSONレスポンスを送信したくない場合です。 -### `response_model_exclude_unset`パラメータの使用 +### `response_model_exclude_unset`パラメータの使用 { #use-the-response-model-exclude-unset-parameter } -*path operation デコレータ*に`response_model_exclude_unset=True`パラメータを設定することができます: +*path operation デコレータ*のパラメータ `response_model_exclude_unset=True` を設定できます: -{* ../../docs_src/response_model/tutorial004.py hl[24] *} +{* ../../docs_src/response_model/tutorial004_py310.py hl[22] *} -そして、これらのデフォルト値はレスポンスに含まれず、実際に設定された値のみが含まれます。 +そうすると、デフォルト値はレスポンスに含まれず、実際に設定された値のみが含まれます。 -そのため、*path operation*にID`foo`が設定されたitemのリクエストを送ると、レスポンスは以下のようになります(デフォルト値を含まない): +そのため、ID `foo` のitemに対してその *path operation* へリクエストを送ると、レスポンスは以下のようになります(デフォルト値を含まない): ```JSON { @@ -116,26 +252,20 @@ FastAPIは`response_model`を使って以下のことをします: /// info | 情報 -FastAPIはこれをするために、Pydanticモデルの`.dict()`をその`exclude_unset`パラメータで使用しています。 - -/// - -/// info | 情報 - -以下も使用することができます: +以下も使用できます: * `response_model_exclude_defaults=True` * `response_model_exclude_none=True` -`exclude_defaults`と`exclude_none`については、Pydanticのドキュメントで説明されている通りです。 +`exclude_defaults` と `exclude_none` については、Pydanticのドキュメントで説明されている通りです。 /// -#### デフォルト値を持つフィールドの値を持つデータ +#### デフォルト値を持つフィールドに値があるデータ { #data-with-values-for-fields-with-defaults } -しかし、ID`bar`のitemのように、デフォルト値が設定されているモデルのフィールドに値が設定されている場合: +しかし、ID `bar` のitemのように、デフォルト値が設定されているモデルのフィールドに値が設定されている場合: -```Python hl_lines="3 5" +```Python hl_lines="3 5" { "name": "Bar", "description": "The bartenders", @@ -146,11 +276,11 @@ FastAPIはこれをするために、Pydanticモデルの`.dict()`を + /// note | 備考 @@ -39,7 +39,7 @@ FastAPIはこれを知っていて、レスポンスボディがないというO /// -## HTTPステータスコードについて +## HTTPステータスコードについて { #about-http-status-codes } /// note | 備考 @@ -47,34 +47,34 @@ FastAPIはこれを知っていて、レスポンスボディがないというO /// -HTTPでは、レスポンスの一部として3桁の数字のステータスコードを送信します。 +HTTPでは、レスポンスの一部として3桁の数字のステータスコードを送信します。 これらのステータスコードは、それらを認識するために関連付けられた名前を持っていますが、重要な部分は番号です。 つまり: -* `100`以上は「情報」のためのものです。。直接使うことはほとんどありません。これらのステータスコードを持つレスポンスはボディを持つことができません。 -* **`200`** 以上は「成功」のレスポンスのためのものです。これらは最も利用するであろうものです。 +* `100 - 199` は「情報」のためのものです。直接使うことはほとんどありません。これらのステータスコードを持つレスポンスはボディを持つことができません。 +* **`200 - 299`** は「成功」のレスポンスのためのものです。これらは最も利用するであろうものです。 * `200`はデフォルトのステータスコードで、すべてが「OK」であったことを意味します。 * 別の例としては、`201`(Created)があります。これはデータベースに新しいレコードを作成した後によく使用されます。 - * 特殊なケースとして、`204`(No Content)があります。このレスポンスはクライアントに返すコンテンツがない場合に使用されます。そしてこのレスポンスはボディを持つことはできません。 -* **`300`** 以上は「リダイレクト」のためのものです。これらのステータスコードを持つレスポンスは`304`(Not Modified)を除き、ボディを持つことも持たないこともできます。 -* **`400`** 以上は「クライアントエラー」のレスポンスのためのものです。これらは、おそらく最も多用するであろう2番目のタイプです。 + * 特殊なケースとして、`204`(No Content)があります。このレスポンスはクライアントに返すコンテンツがない場合に使用されるため、レスポンスはボディを持ってはいけません。 +* **`300 - 399`** は「リダイレクト」のためのものです。これらのステータスコードを持つレスポンスは`304`(Not Modified)を除き、ボディを持つことも持たないこともできます。`304`はボディを持ってはいけません。 +* **`400 - 499`** は「クライアントエラー」のレスポンスのためのものです。これらは、おそらく最も多用するであろう2番目のタイプです。 * 例えば、`404`は「Not Found」レスポンスです。 * クライアントからの一般的なエラーについては、`400`を使用することができます。 -* `500`以上はサーバーエラーのためのものです。これらを直接使うことはほとんどありません。アプリケーションコードやサーバーのどこかで何か問題が発生した場合、これらのステータスコードのいずれかが自動的に返されます。 +* `500 - 599` はサーバーエラーのためのものです。これらを直接使うことはほとんどありません。アプリケーションコードやサーバーのどこかで何か問題が発生した場合、これらのステータスコードのいずれかが自動的に返されます。 /// tip | 豆知識 -それぞれのステータスコードとどのコードが何のためのコードなのかについて詳細はMDN HTTP レスポンスステータスコードについてのドキュメントを参照してください。 +それぞれのステータスコードとどのコードが何のためのコードなのかについて詳細はMDN documentation about HTTP status codesを参照してください。 /// -## 名前を覚えるための近道 +## 名前を覚えるための近道 { #shortcut-to-remember-the-names } 先ほどの例をもう一度見てみましょう: -{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} `201`は「作成完了」のためのステータスコードです。 @@ -82,11 +82,11 @@ HTTPでは、レスポンスの一部として3桁の数字のステータス `fastapi.status`の便利な変数を利用することができます。 -{* ../../docs_src/response_status_code/tutorial002.py hl[1,6] *} +{* ../../docs_src/response_status_code/tutorial002_py39.py hl[1,6] *} -それらは便利です。それらは同じ番号を保持しており、その方法ではエディタの自動補完を使用してそれらを見つけることができます。 +それらは単なる便利なものであり、同じ番号を保持しています。しかし、その方法ではエディタの自動補完を使用してそれらを見つけることができます。 - + /// note | 技術詳細 @@ -96,6 +96,6 @@ HTTPでは、レスポンスの一部として3桁の数字のステータス /// -## デフォルトの変更 +## デフォルトの変更 { #changing-the-default } 後に、[高度なユーザーガイド](../advanced/response-change-status-code.md){.internal-link target=_blank}で、ここで宣言しているデフォルトとは異なるステータスコードを返す方法を見ていきます。 diff --git a/docs/ja/docs/tutorial/schema-extra-example.md b/docs/ja/docs/tutorial/schema-extra-example.md index 1834e67b2..e526685c2 100644 --- a/docs/ja/docs/tutorial/schema-extra-example.md +++ b/docs/ja/docs/tutorial/schema-extra-example.md @@ -1,55 +1,202 @@ -# スキーマの追加 - 例 +# リクエストのExampleデータの宣言 { #declare-request-example-data } -JSON Schemaに追加する情報を定義することができます。 +アプリが受け取れるデータの例を宣言できます。 -一般的なユースケースはこのドキュメントで示されているように`example`を追加することです。 +ここでは、それを行ういくつかの方法を紹介します。 -JSON Schemaの追加情報を宣言する方法はいくつかあります。 +## Pydanticモデルでの追加JSON Schemaデータ { #extra-json-schema-data-in-pydantic-models } -## Pydanticの`schema_extra` +生成されるJSON Schemaに追加されるPydanticモデルの`examples`を宣言できます。 -Pydanticのドキュメント: スキーマのカスタマイズで説明されているように、`Config`と`schema_extra`を使ってPydanticモデルの例を宣言することができます: +{* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -{* ../../docs_src/schema_extra_example/tutorial001.py hl[15,16,17,18,19,20,21,22,23] *} +その追加情報は、そのモデルの出力**JSON Schema**にそのまま追加され、APIドキュメントで使用されます。 -その追加情報はそのまま出力され、JSON Schemaに追加されます。 +Pydanticのドキュメント: Configurationで説明されているように、`dict`を受け取る属性`model_config`を使用できます。 -## `Field`の追加引数 +生成されるJSON Schemaに表示したい追加データ(`examples`を含む)を含む`dict`を使って、`"json_schema_extra"`を設定できます。 -後述する`Field`、`Path`、`Query`、`Body`などでは、任意の引数を関数に渡すことでJSON Schemaの追加情報を宣言することもできます: +/// tip | 豆知識 -{* ../../docs_src/schema_extra_example/tutorial002.py hl[4,10,11,12,13] *} +同じ手法を使ってJSON Schemaを拡張し、独自のカスタム追加情報を追加できます。 -/// warning | 注意 - -これらの追加引数が渡されても、文書化のためのバリデーションは追加されず、注釈だけが追加されることを覚えておいてください。 +例えば、フロントエンドのユーザーインターフェースのためのメタデータを追加する、などに使えます。 /// -## `Body`の追加引数 +/// info | 情報 -追加情報を`Field`に渡すのと同じように、`Path`、`Query`、`Body`などでも同じことができます。 +OpenAPI 3.1.0(FastAPI 0.99.0以降で使用)では、**JSON Schema**標準の一部である`examples`がサポートされました。 -例えば、`Body`にボディリクエストの`example`を渡すことができます: +それ以前は、単一の例を持つキーワード`example`のみがサポートされていました。これはOpenAPI 3.1.0でも引き続きサポートされていますが、非推奨であり、JSON Schema標準の一部ではありません。そのため、`example`から`examples`への移行が推奨されます。🤓 -{* ../../docs_src/schema_extra_example/tutorial003.py hl[21,22,23,24,25,26] *} +詳細はこのページの最後で読めます。 -## ドキュメントのUIの例 +/// + +## `Field`の追加引数 { #field-additional-arguments } + +Pydanticモデルで`Field()`を使う場合、追加の`examples`も宣言できます: + +{* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *} + +## JSON Schema内の`examples` - OpenAPI { #examples-in-json-schema-openapi } + +以下のいずれかを使用する場合: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +追加情報を含む`examples`のグループを宣言でき、それらは**OpenAPI**内のそれぞれの**JSON Schemas**に追加されます。 + +### `examples`を使う`Body` { #body-with-examples } + +ここでは、`Body()`で期待されるデータの例を1つ含む`examples`を渡します: + +{* ../../docs_src/schema_extra_example/tutorial003_an_py310.py hl[22:29] *} + +### ドキュメントUIでの例 { #example-in-the-docs-ui } 上記のいずれの方法でも、`/docs`の中では以下のようになります: - + -## 技術詳細 +### 複数の`examples`を使う`Body` { #body-with-multiple-examples } -`example` と `examples`について... +もちろん、複数の`examples`を渡すこともできます: -JSON Schemaの最新バージョンでは`examples`というフィールドを定義していますが、OpenAPIは`examples`を持たない古いバージョンのJSON Schemaをベースにしています。 +{* ../../docs_src/schema_extra_example/tutorial004_an_py310.py hl[23:38] *} -そのため、OpenAPIでは同じ目的のために`example`を独自に定義しており(`examples`ではなく`example`として)、それがdocs UI(Swagger UIを使用)で使用されています。 +この場合、examplesはそのボディデータの内部**JSON Schema**の一部になります。 -つまり、`example`はJSON Schemaの一部ではありませんが、OpenAPIの一部であり、それがdocs UIで使用されることになります。 +それでも、執筆時点では、ドキュメントUIの表示を担当するツールであるSwagger UIは、**JSON Schema**内のデータに対して複数の例を表示することをサポートしていません。しかし、回避策については以下を読んでください。 -## その他の情報 +### OpenAPI固有の`examples` { #openapi-specific-examples } -同じように、フロントエンドのユーザーインターフェースなどをカスタマイズするために、各モデルのJSON Schemaに追加される独自の追加情報を追加することができます。 +**JSON Schema**が`examples`をサポートする前から、OpenAPIは同じく`examples`という別のフィールドをサポートしていました。 + +この**OpenAPI固有**の`examples`は、OpenAPI仕様の別のセクションに入ります。各JSON Schemaの中ではなく、**各*path operation*の詳細**に入ります。 + +そしてSwagger UIは、この特定の`examples`フィールドを以前からサポートしています。そのため、これを使って**ドキュメントUIに異なる例を表示**できます。 + +このOpenAPI固有フィールド`examples`の形は**複数の例**(`list`ではなく)を持つ`dict`であり、それぞれに追加情報が含まれ、その追加情報は**OpenAPI**にも追加されます。 + +これはOpenAPIに含まれる各JSON Schemaの中には入らず、外側の、*path operation*に直接入ります。 + +### `openapi_examples`パラメータの使用 { #using-the-openapi-examples-parameter } + +FastAPIでは、以下に対してパラメータ`openapi_examples`を使って、OpenAPI固有の`examples`を宣言できます: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +`dict`のキーは各例を識別し、各値は別の`dict`です。 + +`examples`内の各特定の例`dict`には、次の内容を含められます: + +* `summary`: 例の短い説明。 +* `description`: Markdownテキストを含められる長い説明。 +* `value`: 実際に表示される例(例: `dict`)。 +* `externalValue`: `value`の代替で、例を指すURLです。ただし、`value`ほど多くのツールでサポートされていない可能性があります。 + +次のように使えます: + +{* ../../docs_src/schema_extra_example/tutorial005_an_py310.py hl[23:49] *} + +### ドキュメントUIのOpenAPI Examples { #openapi-examples-in-the-docs-ui } + +`Body()`に`openapi_examples`を追加すると、`/docs`は次のようになります: + + + +## 技術詳細 { #technical-details } + +/// tip | 豆知識 + +すでに**FastAPI**バージョン**0.99.0以上**を使用している場合、おそらくこれらの詳細は**スキップ**できます。 + +これらは、OpenAPI 3.1.0が利用可能になる前の古いバージョンにより関連します。 + +これは簡単なOpenAPIとJSON Schemaの**歴史の授業**だと考えられます。🤓 + +/// + +/// warning | 注意 + +ここでは、標準である**JSON Schema**と**OpenAPI**についての非常に技術的な詳細を扱います。 + +上のアイデアがすでにうまく動いているなら、それで十分かもしれませんし、おそらくこの詳細は不要です。気軽にスキップしてください。 + +/// + +OpenAPI 3.1.0より前は、OpenAPIは古く改変されたバージョンの**JSON Schema**を使用していました。 + +JSON Schemaには`examples`がなかったため、OpenAPIは自身が改変したバージョンに独自の`example`フィールドを追加しました。 + +OpenAPIは、仕様の他の部分にも`example`と`examples`フィールドを追加しました: + +* `Parameter Object`(仕様内)。FastAPIの以下で使用されました: + * `Path()` + * `Query()` + * `Header()` + * `Cookie()` +* `Request Body Object`。仕様内の`Media Type Object`の`content`フィールド(仕様内)。FastAPIの以下で使用されました: + * `Body()` + * `File()` + * `Form()` + +/// info | 情報 + +この古いOpenAPI固有の`examples`パラメータは、FastAPI `0.103.0`以降は`openapi_examples`になりました。 + +/// + +### JSON Schemaの`examples`フィールド { #json-schemas-examples-field } + +しかしその後、JSON Schemaは新しいバージョンの仕様に`examples`フィールドを追加しました。 + +そして、新しいOpenAPI 3.1.0は、この新しいフィールド`examples`を含む最新バージョン(JSON Schema 2020-12)に基づくようになりました。 + +そして現在、この新しい`examples`フィールドは、古い単一(かつカスタム)の`example`フィールドより優先され、`example`は現在非推奨です。 + +JSON Schemaのこの新しい`examples`フィールドは、OpenAPIの他の場所(上で説明)にあるような追加メタデータを持つdictではなく、**単なる例の`list`**です。 + +/// info | 情報 + +OpenAPI 3.1.0がこのJSON Schemaとの新しいよりシンプルな統合とともにリリースされた後も、しばらくの間、自動ドキュメントを提供するツールであるSwagger UIはOpenAPI 3.1.0をサポートしていませんでした(バージョン5.0.0からサポートされています🎉)。 + +そのため、FastAPI 0.99.0より前のバージョンは、OpenAPI 3.1.0より低いバージョンのOpenAPIをまだ使用していました。 + +/// + +### PydanticとFastAPIの`examples` { #pydantic-and-fastapi-examples } + +Pydanticモデル内で、`schema_extra`または`Field(examples=["something"])`を使って`examples`を追加すると、その例はそのPydanticモデルの**JSON Schema**に追加されます。 + +そしてそのPydanticモデルの**JSON Schema**はAPIの**OpenAPI**に含まれ、ドキュメントUIで使用されます。 + +FastAPI 0.99.0より前のバージョン(0.99.0以上は新しいOpenAPI 3.1.0を使用)では、他のユーティリティ(`Query()`、`Body()`など)で`example`または`examples`を使っても、それらの例はそのデータを説明するJSON Schema(OpenAPI独自版のJSON Schemaでさえ)には追加されず、OpenAPI内の*path operation*宣言に直接追加されていました(JSON Schemaを使用するOpenAPIの部分の外側)。 + +しかし、FastAPI 0.99.0以上ではOpenAPI 3.1.0を使用し、それはJSON Schema 2020-12とSwagger UI 5.0.0以上を使うため、すべてがより一貫し、例はJSON Schemaに含まれます。 + +### Swagger UIとOpenAPI固有の`examples` { #swagger-ui-and-openapi-specific-examples } + +Swagger UIは複数のJSON Schema examplesをサポートしていなかった(2023-08-26時点)ため、ユーザーはドキュメントで複数の例を表示する手段がありませんでした。 + +それを解決するため、FastAPI `0.103.0`は、新しいパラメータ`openapi_examples`で、同じ古い**OpenAPI固有**の`examples`フィールドを宣言するための**サポートを追加**しました。🤓 + +### まとめ { #summary } + +昔は歴史があまり好きではないと言っていました...が、今の私は「技術の歴史」の授業をしています。😅 + +要するに、**FastAPI 0.99.0以上にアップグレード**してください。そうすれば、物事はもっと**シンプルで一貫性があり直感的**になり、これらの歴史的詳細を知る必要もありません。😎 diff --git a/docs/ja/docs/tutorial/security/first-steps.md b/docs/ja/docs/tutorial/security/first-steps.md index 0ce0f929b..76ef04db8 100644 --- a/docs/ja/docs/tutorial/security/first-steps.md +++ b/docs/ja/docs/tutorial/security/first-steps.md @@ -1,4 +1,4 @@ -# セキュリティ - 最初の一歩 +# セキュリティ - 最初の一歩 { #security-first-steps } あるドメインに、**バックエンド** APIを持っているとしましょう。 @@ -12,25 +12,31 @@ **FastAPI**が提供するツールを使って、セキュリティを制御してみましょう。 -## どう見えるか +## どう見えるか { #how-it-looks } まずはこのコードを使って、どう動くか観察します。その後で、何が起こっているのか理解しましょう。 -## `main.py`を作成 +## `main.py`を作成 { #create-main-py } `main.py`に、下記の例をコピーします: -{* ../../docs_src/security/tutorial001.py *} +{* ../../docs_src/security/tutorial001_an_py39.py *} -## 実行 +## 実行 { #run-it } /// info | 情報 -まず`python-multipart`をインストールします。 +`python-multipart` パッケージは、`pip install "fastapi[standard]"` コマンドを実行すると **FastAPI** と一緒に自動的にインストールされます。 -例えば、`pip install python-multipart`。 +しかし、`pip install fastapi` コマンドを使用する場合、`python-multipart` パッケージはデフォルトでは含まれません。 -これは、**OAuth2**が `ユーザー名` や `パスワード` を送信するために、「フォームデータ」を使うからです。 +手動でインストールするには、[仮想環境](../../virtual-environments.md){.internal-link target=_blank}を作成して有効化し、次のコマンドでインストールしてください: + +```console +$ pip install python-multipart +``` + +これは、**OAuth2**が `username` と `password` を送信するために、「フォームデータ」を使うからです。 /// @@ -39,14 +45,14 @@
```console -$ uvicorn main:app --reload +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ```
-## 確認 +## 確認 { #check-it } 次のインタラクティブなドキュメントにアクセスしてください: http://127.0.0.1:8000/docs。 @@ -62,7 +68,7 @@ $ uvicorn main:app --reload /// -それをクリックすると、`ユーザー名`と`パスワード` (およびその他のオプションフィールド) を入力する小さな認証フォームが表示されます: +それをクリックすると、`username` と `password`(およびその他のオプションフィールド)を入力する小さな認可フォームが表示されます: @@ -80,11 +86,11 @@ $ uvicorn main:app --reload また、同じアプリケーションのデバッグ、チェック、テストのためにも利用できます。 -## `パスワード` フロー +## `password` フロー { #the-password-flow } では、少し話を戻して、どうなっているか理解しましょう。 -`パスワード`の「フロー」は、OAuth2で定義されているセキュリティと認証を扱う方法 (「フロー」) の1つです。 +`password`の「フロー」は、OAuth2で定義されているセキュリティと認証を扱う方法 (「フロー」) の1つです。 OAuth2は、バックエンドやAPIがユーザーを認証するサーバーから独立したものとして設計されていました。 @@ -92,9 +98,9 @@ OAuth2は、バックエンドやAPIがユーザーを認証するサーバー そこで、簡略化した箇所から見直してみましょう: -* ユーザーはフロントエンドで`ユーザー名`と`パスワード`を入力し、`Enter`を押します。 -* フロントエンド (ユーザーのブラウザで実行中) は、`ユーザー名`と`パスワード`をAPIの特定のURL (`tokenUrl="token"`で宣言された) に送信します。 -* APIは`ユーザー名`と`パスワード`をチェックし、「トークン」を返却します (まだ実装していません)。 +* ユーザーはフロントエンドで`username`と`password`を入力し、`Enter`を押します。 +* フロントエンド (ユーザーのブラウザで実行中) は、`username`と`password`をAPIの特定のURL (`tokenUrl="token"`で宣言された) に送信します。 +* APIは`username`と`password`をチェックし、「トークン」を返却します (まだ実装していません)。 * 「トークン」はただの文字列であり、あとでこのユーザーを検証するために使用します。 * 通常、トークンは時間が経つと期限切れになるように設定されています。 * トークンが期限切れの場合は、再度ログインする必要があります。 @@ -106,11 +112,11 @@ OAuth2は、バックエンドやAPIがユーザーを認証するサーバー * したがって、APIで認証するため、HTTPヘッダー`Authorization`に`Bearer`の文字列とトークンを加えた値を送信します。 * トークンに`foobar`が含まれている場合、`Authorization`ヘッダーの内容は次のようになります: `Bearer foobar`。 -## **FastAPI**の`OAuth2PasswordBearer` +## **FastAPI**の`OAuth2PasswordBearer` { #fastapis-oauth2passwordbearer } **FastAPI**は、これらのセキュリティ機能を実装するために、抽象度の異なる複数のツールを提供しています。 -この例では、**Bearer**トークンを使用して**OAuth2**を**パスワード**フローで使用します。これには`OAuth2PasswordBearer`クラスを使用します。 +この例では、**Bearer**トークンを使用して**OAuth2**を**Password**フローで使用します。これには`OAuth2PasswordBearer`クラスを使用します。 /// info | 情報 @@ -124,9 +130,9 @@ OAuth2は、バックエンドやAPIがユーザーを認証するサーバー /// -`OAuth2PasswordBearer` クラスのインスタンスを作成する時に、パラメーター`tokenUrl`を渡します。このパラメーターには、クライアント (ユーザーのブラウザで動作するフロントエンド) がトークンを取得するために`ユーザー名`と`パスワード`を送信するURLを指定します。 +`OAuth2PasswordBearer` クラスのインスタンスを作成する時に、パラメーター`tokenUrl`を渡します。このパラメーターには、クライアント (ユーザーのブラウザで動作するフロントエンド) がトークンを取得するために`username`と`password`を送信するURLを指定します。 -{* ../../docs_src/security/tutorial001.py hl[6] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[8] *} /// tip | 豆知識 @@ -134,13 +140,13 @@ OAuth2は、バックエンドやAPIがユーザーを認証するサーバー 相対URLを使っているので、APIが`https://example.com/`にある場合、`https://example.com/token`を参照します。しかし、APIが`https://example.com/api/v1/`にある場合は`https://example.com/api/v1/token`を参照することになります。 -相対 URL を使うことは、[プロキシと接続](../../advanced/behind-a-proxy.md){.internal-link target=_blank}のような高度なユースケースでもアプリケーションを動作させ続けるために重要です。 +相対 URL を使うことは、[プロキシの背後](../../advanced/behind-a-proxy.md){.internal-link target=_blank}のような高度なユースケースでもアプリケーションを動作させ続けるために重要です。 /// このパラメーターはエンドポイント/ *path operation*を作成しません。しかし、URL`/token`はクライアントがトークンを取得するために使用するものであると宣言します。この情報は OpenAPI やインタラクティブな API ドキュメントシステムで使われます。 -実際のpath operationもすぐに作ります。 +実際の path operation もすぐに作ります。 /// info | 情報 @@ -160,13 +166,13 @@ oauth2_scheme(some, parameters) そのため、`Depends`と一緒に使うことができます。 -### 使い方 +### 使い方 { #use-it } これで`oauth2_scheme`を`Depends`で依存関係に渡すことができます。 -{* ../../docs_src/security/tutorial001.py hl[10] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} -この依存関係は、*path operation function*のパラメーター`token`に代入される`str`を提供します。 +この依存関係は、*path operation 関数*のパラメーター`token`に代入される`str`を提供します。 **FastAPI**は、この依存関係を使用してOpenAPIスキーマ (および自動APIドキュメント) で「セキュリティスキーム」を定義できることを知っています。 @@ -178,13 +184,13 @@ OpenAPIと統合するセキュリティユーティリティ (および自動AP /// -## どのように動作するか +## 何をするか { #what-it-does } -リクエストの中に`Authorization`ヘッダーを探しに行き、その値が`Bearer`と何らかのトークンを含んでいるかどうかをチェックし、そのトークンを`str`として返します。 +リクエストの中に`Authorization`ヘッダーを探しに行き、その値が`Bearer `と何らかのトークンを含んでいるかどうかをチェックし、そのトークンを`str`として返します。 -もし`Authorization`ヘッダーが見つからなかったり、値が`Bearer`トークンを持っていなかったりすると、401 ステータスコードエラー (`UNAUTHORIZED`) で直接応答します。 +もし`Authorization`ヘッダーが見つからなかったり、値が`Bearer `トークンを持っていなかったりすると、401 ステータスコードエラー (`UNAUTHORIZED`) で直接応答します。 -トークンが存在するかどうかをチェックしてエラーを返す必要はありません。関数が実行された場合、そのトークンに`str`が含まれているか確認できます。 +トークンが存在するかどうかをチェックしてエラーを返す必要はありません。関数が実行された場合、そのトークンに`str`が含まれていることを確信できます。 インタラクティブなドキュメントですでに試すことができます: @@ -192,6 +198,6 @@ OpenAPIと統合するセキュリティユーティリティ (および自動AP まだトークンの有効性を検証しているわけではありませんが、これはもう始まっています。 -## まとめ +## まとめ { #recap } つまり、たった3~4行の追加で、すでに何らかの基礎的なセキュリティの形になっています。 diff --git a/docs/ja/docs/tutorial/security/get-current-user.md b/docs/ja/docs/tutorial/security/get-current-user.md index 9fc46c07c..39b97cca5 100644 --- a/docs/ja/docs/tutorial/security/get-current-user.md +++ b/docs/ja/docs/tutorial/security/get-current-user.md @@ -1,22 +1,22 @@ -# 現在のユーザーの取得 +# 現在のユーザーの取得 { #get-current-user } -一つ前の章では、(依存性注入システムに基づいた)セキュリティシステムは、 *path operation関数* に `str` として `token` を与えていました: +一つ前の章では、(依存性注入システムに基づいた)セキュリティシステムは、 *path operation 関数* に `str` として `token` を与えていました: -{* ../../docs_src/security/tutorial001.py hl[10] *} +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} しかし、それはまだそんなに有用ではありません。 現在のユーザーを取得するようにしてみましょう。 -## ユーザーモデルの作成 +## ユーザーモデルの作成 { #create-a-user-model } まずは、Pydanticのユーザーモデルを作成しましょう。 ボディを宣言するのにPydanticを使用するのと同じやり方で、Pydanticを別のどんなところでも使うことができます: -{* ../../docs_src/security/tutorial002.py hl[5,12:16] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *} -## 依存関係 `get_current_user` を作成 +## 依存関係 `get_current_user` を作成 { #create-a-get-current-user-dependency } 依存関係 `get_current_user` を作ってみましょう。 @@ -24,21 +24,21 @@ `get_current_user` は前に作成した `oauth2_scheme` と同じ依存関係を持ちます。 -以前直接 *path operation* の中でしていたのと同じように、新しい依存関係である `get_current_user` は `str` として `token` を受け取るようになります: +以前直接 *path operation* の中でしていたのと同じように、新しい依存関係である `get_current_user` はサブ依存関係である `oauth2_scheme` から `str` として `token` を受け取るようになります: -{* ../../docs_src/security/tutorial002.py hl[25] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *} -## ユーザーの取得 +## ユーザーの取得 { #get-the-user } `get_current_user` は作成した(偽物の)ユーティリティ関数を使って、 `str` としてトークンを受け取り、先ほどのPydanticの `User` モデルを返却します: -{* ../../docs_src/security/tutorial002.py hl[19:22,26:27] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *} -## 現在のユーザーの注入 +## 現在のユーザーの注入 { #inject-the-current-user } ですので、 `get_current_user` に対して同様に *path operation* の中で `Depends` を利用できます。 -{* ../../docs_src/security/tutorial002.py hl[31] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} Pydanticモデルの `User` として、 `current_user` の型を宣言することに注意してください。 @@ -54,15 +54,15 @@ Pydanticモデルの `User` として、 `current_user` の型を宣言するこ /// check | 確認 -依存関係システムがこのように設計されているおかげで、 `User` モデルを返却する別の依存関係(別の"dependables")を持つことができます。 +依存関係システムがこのように設計されているおかげで、 `User` モデルを返却する別の依存関係(別の「dependables」)を持つことができます。 同じデータ型を返却する依存関係は一つだけしか持てない、という制約が入ることはないのです。 /// -## 別のモデル +## 別のモデル { #other-models } -これで、*path operation関数* の中で現在のユーザーを直接取得し、`Depends` を使って、 **依存性注入** レベルでセキュリティメカニズムを処理できるようになりました。 +これで、*path operation 関数* の中で現在のユーザーを直接取得し、`Depends` を使って、 **依存性注入** レベルでセキュリティメカニズムを処理できるようになりました。 そして、セキュリティ要件のためにどんなモデルやデータでも利用することができます。(この場合は、 Pydanticモデルの `User`) @@ -76,10 +76,9 @@ Pydanticモデルの `User` として、 `current_user` の型を宣言するこ あなたのアプリケーションに必要なのがどんな種類のモデル、どんな種類のクラス、どんな種類のデータベースであったとしても、 **FastAPI** は依存性注入システムでカバーしてくれます。 +## コードサイズ { #code-size } -## コードサイズ - -この例は冗長に見えるかもしれません。セキュリティとデータモデルユーティリティ関数および *path operations* が同じファイルに混在しているということを覚えておいてください。 +この例は冗長に見えるかもしれません。セキュリティとデータモデルユーティリティ関数および *path operation* が同じファイルに混在しているということを覚えておいてください。 しかし、ここに重要なポイントがあります。 @@ -87,20 +86,20 @@ Pydanticモデルの `User` として、 `current_user` の型を宣言するこ そして、それは好きなだけ複雑にすることができます。それでも、一箇所に、一度だけ書くのです。すべての柔軟性を備えます。 -しかし、同じセキュリティシステムを使って何千ものエンドポイント(*path operations*)を持つことができます。 +しかし、同じセキュリティシステムを使って何千ものエンドポイント(*path operation*)を持つことができます。 そして、それらエンドポイントのすべて(必要な、どの部分でも)がこうした依存関係や、あなたが作成する別の依存関係を再利用する利点を享受できるのです。 -さらに、こうした何千もの *path operations* は、たった3行で表現できるのです: +さらに、こうした何千もの *path operation* は、たった3行で表現できるのです: -{* ../../docs_src/security/tutorial002.py hl[30:32] *} +{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} -## まとめ +## まとめ { #recap } -これで、 *path operation関数* の中で直接現在のユーザーを取得できるようになりました。 +これで、 *path operation 関数* の中で直接現在のユーザーを取得できるようになりました。 既に半分のところまで来ています。 -あとは、 `username` と `password` を実際にそのユーザーやクライアントに送る、 *path operation* を追加する必要があるだけです。 +あとは、ユーザー/クライアントが実際に `username` と `password` を送信するための *path operation* を追加する必要があるだけです。 次はそれを説明します。 diff --git a/docs/ja/docs/tutorial/security/oauth2-jwt.md b/docs/ja/docs/tutorial/security/oauth2-jwt.md index 599fc7b06..186936f1b 100644 --- a/docs/ja/docs/tutorial/security/oauth2-jwt.md +++ b/docs/ja/docs/tutorial/security/oauth2-jwt.md @@ -1,4 +1,4 @@ -# パスワード(およびハッシュ化)によるOAuth2、JWTトークンによるBearer +# パスワード(およびハッシュ化)によるOAuth2、JWTトークンによるBearer { #oauth2-with-password-and-hashing-bearer-with-jwt-tokens } これでセキュリティの流れが全てわかったので、JWTトークンと安全なパスワードのハッシュ化を使用して、実際にアプリケーションを安全にしてみましょう。 @@ -6,7 +6,7 @@ 本章では、前章の続きから始めて、コードをアップデートしていきます。 -## JWT について +## JWT について { #about-jwt } JWTとは「JSON Web Tokens」の略称です。 @@ -26,33 +26,31 @@ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4 JWT トークンを使って遊んでみたいという方は、https://jwt.io をチェックしてください。 -## `python-jose` のインストール +## `PyJWT` のインストール { #install-pyjwt } -PythonでJWTトークンの生成と検証を行うために、`python-jose`をインストールする必要があります: +PythonでJWTトークンの生成と検証を行うために、`PyJWT`をインストールする必要があります。 + +[仮想環境](../../virtual-environments.md){.internal-link target=_blank}を作成し、アクティベートしてから、`pyjwt`をインストールしてください。
```console -$ pip install python-jose[cryptography] +$ pip install pyjwt ---> 100% ```
-また、Python-joseだけではなく、暗号を扱うためのパッケージを追加で必要とします。 +/// info | 情報 -ここでは、推奨されているものを使用します:pyca/cryptography。 +RSAやECDSAのようなデジタル署名アルゴリズムを使用する予定がある場合は、cryptographyライブラリの依存関係`pyjwt[crypto]`をインストールしてください。 -/// tip | 豆知識 - -このチュートリアルでは以前、PyJWTを使用していました。 - -しかし、Python-joseは、PyJWTのすべての機能に加えて、後に他のツールと統合して構築する際におそらく必要となる可能性のあるいくつかの追加機能を提供しています。そのため、代わりにPython-joseを使用するように更新されました。 +詳細はPyJWT Installation docsで確認できます。 /// -## パスワードのハッシュ化 +## パスワードのハッシュ化 { #password-hashing } 「ハッシュ化」とは、あるコンテンツ(ここではパスワード)を、規則性のないバイト列(単なる文字列)に変換することです。 @@ -60,26 +58,26 @@ $ pip install python-jose[cryptography] しかし、規則性のないバイト列から元のパスワードに戻すことはできません。 -### パスワードのハッシュ化を使う理由 +### パスワードのハッシュ化を使う理由 { #why-use-password-hashing } データベースが盗まれても、ユーザーの平文のパスワードは盗まれず、ハッシュ値だけが盗まれます。 したがって、泥棒はそのパスワードを別のシステムで使えません(多くのユーザーはどこでも同じパスワードを使用しているため、危険性があります)。 -## `passlib` のインストール +## `pwdlib` のインストール { #install-pwdlib } -PassLib は、パスワードのハッシュを処理するための優れたPythonパッケージです。 +pwdlib は、パスワードのハッシュを処理するための優れたPythonパッケージです。 このパッケージは、多くの安全なハッシュアルゴリズムとユーティリティをサポートします。 -推奨されるアルゴリズムは「Bcrypt」です。 +推奨されるアルゴリズムは「Argon2」です。 -そのため、Bcryptを指定してPassLibをインストールします: +[仮想環境](../../virtual-environments.md){.internal-link target=_blank}を作成し、アクティベートしてから、Argon2付きでpwdlibをインストールしてください。
```console -$ pip install passlib[bcrypt] +$ pip install "pwdlib[argon2]" ---> 100% ``` @@ -88,7 +86,7 @@ $ pip install passlib[bcrypt] /// tip | 豆知識 -`passlib`を使用すると、**Django**や**Flask**のセキュリティプラグインなどで作成されたパスワードを読み取れるように設定できます。 +`pwdlib`を使用すると、**Django**や**Flask**のセキュリティプラグインなどで作成されたパスワードを読み取れるように設定できます。 例えば、Djangoアプリケーションからデータベース内の同じデータをFastAPIアプリケーションと共有できるだけではなく、同じデータベースを使用してDjangoアプリケーションを徐々に移行することもできます。 @@ -96,17 +94,17 @@ $ pip install passlib[bcrypt] /// -## パスワードのハッシュ化と検証 +## パスワードのハッシュ化と検証 { #hash-and-verify-the-passwords } -必要なツールを `passlib`からインポートします。 +必要なツールを `pwdlib`からインポートします。 -PassLib の「context」を作成します。これは、パスワードのハッシュ化と検証に使用されるものです。 +推奨設定でPasswordHashインスタンスを作成します。これは、パスワードのハッシュ化と検証に使用されます。 /// tip | 豆知識 -PassLibのcontextには、検証だけが許された非推奨の古いハッシュアルゴリズムを含む、様々なハッシュアルゴリズムを使用した検証機能もあります。 +pwdlibはbcryptハッシュアルゴリズムもサポートしていますが、レガシーアルゴリズムは含みません。古いハッシュを扱うには、passlibライブラリを使用することが推奨されます。 -例えば、この機能を使用して、別のシステム(Djangoなど)によって生成されたパスワードを読み取って検証し、Bcryptなどの別のアルゴリズムを使用して新しいパスワードをハッシュするといったことができます。 +例えば、この機能を使用して、別のシステム(Djangoなど)によって生成されたパスワードを読み取って検証し、Argon2やBcryptなどの別のアルゴリズムを使用して新しいパスワードをハッシュするといったことができます。 そして、同時にそれらはすべてに互換性があります。 @@ -118,15 +116,15 @@ PassLibのcontextには、検証だけが許された非推奨の古いハッシ さらに、ユーザーを認証して返す関数も作成します。 -{* ../../docs_src/security/tutorial004.py hl[7,48,55:56,59:60,69:75] *} +{* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,56:57,60:61,70:76] *} /// note | 備考 -新しい(偽の)データベース`fake_users_db`を確認すると、ハッシュ化されたパスワードが次のようになっていることがわかります:`"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"` +新しい(偽の)データベース`fake_users_db`を確認すると、ハッシュ化されたパスワードが次のようになっていることがわかります:`"$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc"`。 /// -## JWTトークンの取り扱い +## JWTトークンの取り扱い { #handle-jwt-tokens } インストールした複数のモジュールをインポートします。 @@ -148,33 +146,33 @@ $ openssl rand -hex 32 JWTトークンの署名に使用するアルゴリズム`"HS256"`を指定した変数`ALGORITHM`を作成します。 -トークンの有効期限を指定した変数`ACCESS_TOKEN_EXPIRE_MINUTES`を作成します。 +トークンの有効期限を指定した変数を作成します。 レスポンスのトークンエンドポイントで使用するPydanticモデルを定義します。 新しいアクセストークンを生成するユーティリティ関数を作成します。 -{* ../../docs_src/security/tutorial004.py hl[6,12:14,28:30,78:86] *} +{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *} -## 依存関係の更新 +## 依存関係の更新 { #update-the-dependencies } `get_current_user`を更新して、先ほどと同じトークンを受け取るようにしますが、今回はJWTトークンを使用します。 -受け取ったトークンを復号して検証し、現在のユーザーを返します。 +受け取ったトークンをデコードして検証し、現在のユーザーを返します。 トークンが無効な場合は、すぐにHTTPエラーを返します。 -{* ../../docs_src/security/tutorial004.py hl[89:106] *} +{* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *} -## `/token` パスオペレーションの更新 +## `/token` *path operation* の更新 { #update-the-token-path-operation } トークンの有効期限を表す`timedelta`を作成します。 -JWTアクセストークンを作成し、それを返します。 +実際のJWTアクセストークンを作成し、それを返します。 -{* ../../docs_src/security/tutorial004.py hl[115:130] *} +{* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *} -### JWTの"subject" `sub` についての技術的な詳細 +### JWTの「subject」`sub` についての技術的な詳細 { #technical-details-about-the-jwt-subject-sub } JWTの仕様では、トークンのsubjectを表すキー`sub`があるとされています。 @@ -192,13 +190,13 @@ JWTは、ユーザーを識別して、そのユーザーがAPI上で直接操 しかしながら、それらのエンティティのいくつかが同じIDを持つ可能性があります。例えば、`foo`(ユーザー`foo`、車 `foo`、ブログ投稿`foo`)などです。 -IDの衝突を回避するために、ユーザーのJWTトークンを作成するとき、subキーの値にプレフィックスを付けることができます(例えば、`username:`)。したがって、この例では、`sub`の値は次のようになっている可能性があります:`username:johndoe` +IDの衝突を回避するために、ユーザーのJWTトークンを作成するとき、subキーの値にプレフィックスを付けることができます(例えば、`username:`)。したがって、この例では、`sub`の値は次のようになっている可能性があります:`username:johndoe`。 覚えておくべき重要なことは、`sub`キーはアプリケーション全体で一意の識別子を持ち、文字列である必要があるということです。 -## 確認 +## 確認 { #check-it } -サーバーを実行し、ドキュメントに移動します:http://127.0.0.1:8000/docs +サーバーを実行し、ドキュメントに移動します:http://127.0.0.1:8000/docs。 次のようなユーザーインターフェイスが表示されます: @@ -232,17 +230,17 @@ Password: `secret` -開発者ツールを開くと、送信されるデータにはトークンだけが含まれており、パスワードはユーザーを認証してアクセストークンを取得する最初のリクエストでのみ送信され、その後は送信されないことがわかります。 +開発者ツールを開くと、送信されるデータにはトークンだけが含まれており、パスワードはユーザーを認証してアクセストークンを取得する最初のリクエストでのみ送信され、その後は送信されないことがわかります: /// note | 備考 -ヘッダーの`Authorization`には、`Bearer`で始まる値があります。 +ヘッダーの`Authorization`には、`Bearer `で始まる値があります。 /// -## `scopes` を使った高度なユースケース +## `scopes` を使った高度なユースケース { #advanced-usage-with-scopes } OAuth2には、「スコープ」という概念があります。 @@ -252,7 +250,7 @@ OAuth2には、「スコープ」という概念があります。 これらの使用方法や**FastAPI**への統合方法については、**高度なユーザーガイド**で後ほど説明します。 -## まとめ +## まとめ { #recap } ここまでの説明で、OAuth2やJWTなどの規格を使った安全な**FastAPI**アプリケーションを設定することができます。 @@ -266,7 +264,7 @@ OAuth2には、「スコープ」という概念があります。 そのため、プロジェクトに合わせて自由に選択することができます。 -また、**FastAPI**は外部パッケージを統合するために複雑な仕組みを必要としないため、`passlib`や`python-jose`のようなよく整備され広く使われている多くのパッケージを直接使用することができます。 +また、**FastAPI**は外部パッケージを統合するために複雑な仕組みを必要としないため、`pwdlib`や`PyJWT`のようなよく整備され広く使われている多くのパッケージを直接使用することができます。 しかし、柔軟性、堅牢性、セキュリティを損なうことなく、可能な限りプロセスを簡素化するためのツールを提供します。 diff --git a/docs/ja/docs/tutorial/static-files.md b/docs/ja/docs/tutorial/static-files.md index f910d7e36..c79789494 100644 --- a/docs/ja/docs/tutorial/static-files.md +++ b/docs/ja/docs/tutorial/static-files.md @@ -1,13 +1,13 @@ -# 静的ファイル +# 静的ファイル { #static-files } `StaticFiles` を使用して、ディレクトリから静的ファイルを自動的に提供できます。 -## `StaticFiles` の使用 +## `StaticFiles` の使用 { #use-staticfiles } * `StaticFiles` をインポート。 -* `StaticFiles()` インスタンスを生成し、特定のパスに「マウント」。 +* `StaticFiles()` インスタンスを特定のパスに「マウント」。 -{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} +{* ../../docs_src/static_files/tutorial001_py39.py hl[2,6] *} /// note | 技術詳細 @@ -17,15 +17,15 @@ /// -### 「マウント」とは +### 「マウント」とは { #what-is-mounting } 「マウント」とは、特定のパスに完全な「独立した」アプリケーションを追加することを意味します。これにより、すべてのサブパスの処理がなされます。 これは、マウントされたアプリケーションが完全に独立しているため、`APIRouter` とは異なります。メインアプリケーションのOpenAPIとドキュメントには、マウントされたアプリケーションの内容などは含まれません。 -これについて詳しくは、**高度なユーザーガイド** をご覧ください。 +これについて詳しくは、[高度なユーザーガイド](../advanced/index.md){.internal-link target=_blank} をご覧ください。 -## 詳細 +## 詳細 { #details } 最初の `"/static"` は、この「サブアプリケーション」が「マウント」されるサブパスを指します。したがって、`"/static"` から始まるパスはすべてサブアプリケーションによって処理されます。 @@ -33,8 +33,8 @@ `name="static"` は、**FastAPI** が内部で使用できる名前を付けます。 -これらのパラメータはすべて「`静的`」とは異なる場合があり、独自のアプリケーションのニーズと詳細に合わせて調整します。 +これらのパラメータはすべて「`static`」とは異なる場合があり、独自のアプリケーションのニーズと詳細に合わせて調整します。 -## より詳しい情報 +## より詳しい情報 { #more-info } 詳細とオプションについては、Starletteの静的ファイルに関するドキュメントを確認してください。 diff --git a/docs/ja/docs/tutorial/testing.md b/docs/ja/docs/tutorial/testing.md index 4e8ad4f7c..0ec6250f3 100644 --- a/docs/ja/docs/tutorial/testing.md +++ b/docs/ja/docs/tutorial/testing.md @@ -1,12 +1,24 @@ -# テスト +# テスト { #testing } Starlette のおかげで、**FastAPI** アプリケーションのテストは簡単で楽しいものになっています。 -HTTPX がベースなので、非常に使いやすく直感的です。 +HTTPX がベースで、さらにその設計は Requests をベースにしているため、とても馴染みがあり直感的です。 これを使用すると、**FastAPI** と共に pytest を直接利用できます。 -## `TestClient` を使用 +## `TestClient` を使用 { #using-testclient } + +/// info | 情報 + +`TestClient` を使用するには、まず `httpx` をインストールします。 + +[仮想環境](../virtual-environments.md){.internal-link target=_blank} を作成し、それを有効化してから、例えば以下のようにインストールしてください: + +```console +$ pip install httpx +``` + +/// `TestClient` をインポートします。 @@ -16,9 +28,9 @@ `httpx` と同じ様に `TestClient` オブジェクトを使用します。 -チェックしたい Python の標準的な式と共に、シンプルに `assert` 文を記述します。 +チェックしたい Python の標準的な式と共に、シンプルに `assert` 文を記述します (これも `pytest` の標準です)。 -{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *} +{* ../../docs_src/app_testing/tutorial001_py39.py hl[2,12,15:18] *} /// tip | 豆知識 @@ -44,48 +56,81 @@ FastAPIアプリケーションへのリクエストの送信とは別に、テ /// -## テストの分離 +## テストの分離 { #separating-tests } 実際のアプリケーションでは、おそらくテストを別のファイルに保存します。 また、**FastAPI** アプリケーションは、複数のファイル/モジュールなどで構成されている場合もあります。 -### **FastAPI** アプリファイル +### **FastAPI** アプリファイル { #fastapi-app-file } -**FastAPI** アプリに `main.py` ファイルがあるとします: +[Bigger Applications](bigger-applications.md){.internal-link target=_blank} で説明されている、次のようなファイル構成があるとします: -{* ../../docs_src/app_testing/main.py *} +``` +. +├── app +│   ├── __init__.py +│   └── main.py +``` -### テストファイル +ファイル `main.py` に **FastAPI** アプリがあります: -次に、テストを含む `test_main.py` ファイルを作成し、`main` モジュール (`main.py`) から `app` をインポートします: -{* ../../docs_src/app_testing/test_main.py *} +{* ../../docs_src/app_testing/app_a_py39/main.py *} -## テスト: 例の拡張 +### テストファイル { #testing-file } + +次に、テストを含む `test_main.py` ファイルを用意できます。これは同じ Python パッケージ (`__init__.py` ファイルがある同じディレクトリ) に置けます: + +``` hl_lines="5" +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +このファイルは同じパッケージ内にあるため、相対インポートを使って `main` モジュール (`main.py`) からオブジェクト `app` をインポートできます: + +{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *} + + +...そして、これまでと同じようにテストコードを書けます。 + +## テスト: 例の拡張 { #testing-extended-example } 次に、この例を拡張し、詳細を追加して、さまざまなパーツをテストする方法を確認しましょう。 +### 拡張版 **FastAPI** アプリファイル { #extended-fastapi-app-file } -### 拡張版 **FastAPI** アプリファイル +先ほどと同じファイル構成で続けます: -**FastAPI** アプリに `main_b.py` ファイルがあるとします。 +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` -そのファイルには、エラーを返す可能性のある `GET` オペレーションがあります。 +ここで、**FastAPI** アプリがある `main.py` ファイルには、他の path operation があります。 -また、いくつかのエラーを返す可能性のある `POST` オペレーションもあります。 +エラーを返す可能性のある `GET` オペレーションがあります。 -これらの *path operation* には `X-Token` ヘッダーが必要です。 +いくつかのエラーを返す可能性のある `POST` オペレーションもあります。 -{* ../../docs_src/app_testing/app_b_py310/main.py *} +両方の *path operation* には `X-Token` ヘッダーが必要です。 -### 拡張版テストファイル +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} -次に、先程のものに拡張版のテストを加えた、`test_main_b.py` を作成します。 +### 拡張版テストファイル { #extended-testing-file } -{* ../../docs_src/app_testing/app_b/test_main.py *} +次に、拡張版のテストで `test_main.py` を更新できます: -リクエストに情報を渡せるクライアントが必要で、その方法がわからない場合はいつでも、`httpx` での実現方法を検索 (Google) できます。 +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} + + +リクエストに情報を渡せるクライアントが必要で、その方法がわからない場合はいつでも、`httpx` での実現方法、あるいは HTTPX の設計が Requests の設計をベースにしているため `requests` での実現方法を検索 (Google) できます。 テストでも同じことを行います。 @@ -107,9 +152,11 @@ FastAPIアプリケーションへのリクエストの送信とは別に、テ /// -## 実行 +## 実行 { #run-it } -後は、`pytest` をインストールするだけです: +その後、`pytest` をインストールするだけです。 + +[仮想環境](../virtual-environments.md){.internal-link target=_blank} を作成し、それを有効化してから、例えば以下のようにインストールしてください:
@@ -121,7 +168,7 @@ $ pip install pytest
-ファイルを検知し、自動テストを実行し、結果のレポートを返します。 +ファイルとテストを自動的に検出し、実行して、結果のレポートを返します。 以下でテストを実行します: diff --git a/docs/ja/docs/virtual-environments.md b/docs/ja/docs/virtual-environments.md index 791cf64a8..672323063 100644 --- a/docs/ja/docs/virtual-environments.md +++ b/docs/ja/docs/virtual-environments.md @@ -1,10 +1,10 @@ -# 仮想環境 +# 仮想環境 { #virtual-environments } Pythonプロジェクトの作業では、**仮想環境**(または類似の仕組み)を使用し、プロジェクトごとにインストールするパッケージを分離するべきでしょう。 /// info | 情報 -もし、仮想環境の概要や作成方法、使用方法について既にご存知なら、このセクションをスキップすることができます。🤓 +もし、仮想環境の概要や作成方法、使用方法について既にご存知なら、このセクションをスキップした方がよいかもしれません。🤓 /// @@ -25,7 +25,7 @@ Pythonプロジェクトの作業では、**仮想環境**(または類似の /// -## プロジェクトの作成 +## プロジェクトの作成 { #create-a-project } まず、プロジェクト用のディレクトリを作成します。 @@ -48,9 +48,9 @@ $ cd awesome-project
-## 仮想環境の作成 +## 仮想環境の作成 { #create-a-virtual-environment } -Pythonプロジェクトでの**初めての**作業を開始する際には、**プロジェクト内**に仮想環境を作成してください。 +Pythonプロジェクトでの**初めての**作業を開始する際には、**プロジェクト内**に仮想環境を作成してください。 /// tip | 豆知識 @@ -72,10 +72,10 @@ $ python -m venv .venv /// details | このコマンドの意味 -- `python` : `python` というプログラムを呼び出します -- `-m` : モジュールをスクリプトとして呼び出します。どのモジュールを呼び出すのか、この次に指定します -- `venv` : 通常Pythonに付随してインストールされる `venv`モジュールを使用します -- `.venv` : 仮想環境を`.venv`という新しいディレクトリに作成します +* `python`: `python` というプログラムを呼び出します +* `-m`: モジュールをスクリプトとして呼び出します。どのモジュールを呼び出すのか、この次に指定します +* `venv`: 通常Pythonに付随してインストールされる `venv`モジュールを使用します +* `.venv`: 仮想環境を`.venv`という新しいディレクトリに作成します /// @@ -111,13 +111,13 @@ $ uv venv /// -## 仮想環境の有効化 +## 仮想環境の有効化 { #activate-the-virtual-environment } 実行されるPythonコマンドやインストールされるパッケージが新しく作成した仮想環境を使用するよう、その仮想環境を有効化しましょう。 /// tip | 豆知識 -そのプロジェクトの作業で**新しいターミナルセッション**を開始する際には、**毎回**有効化が必要です。 +そのプロジェクトの作業で**新しいターミナルセッション**を開始する際には、**毎回**有効化してください。 /// @@ -161,13 +161,13 @@ $ source .venv/Scripts/activate /// tip | 豆知識 -**新しいパッケージ**を仮想環境にインストールするときには、再度**有効化**してください。 +**新しいパッケージ**を仮想環境にインストールするたびに、環境をもう一度**有効化**してください。 こうすることで、そのパッケージがインストールした**ターミナル(CLI)プログラム**を使用する場合に、仮想環境内のものが確実に使われ、グローバル環境にインストールされている別のもの(おそらく必要なものとは異なるバージョン)を誤って使用することを防ぎます。 /// -## 仮想環境が有効であることを確認する +## 仮想環境が有効であることを確認する { #check-the-virtual-environment-is-active } 仮想環境が有効である(前のコマンドが正常に機能した)ことを確認します。 @@ -197,7 +197,7 @@ $ which python
-``` console +```console $ Get-Command python C:\Users\user\code\awesome-project\.venv\Scripts\python @@ -209,7 +209,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python //// -## `pip` をアップグレードする +## `pip` をアップグレードする { #upgrade-pip } /// tip | 豆知識 @@ -239,7 +239,27 @@ $ python -m pip install --upgrade pip
-## `.gitignore` を追加する +/// tip | 豆知識 + +ときどき、pip をアップグレードしようとすると **`No module named pip`** エラーが表示されることがあります。 + +その場合は、以下のコマンドで pip をインストールしてアップグレードしてください: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +このコマンドは、pip がまだインストールされていなければ pip をインストールし、また、インストールされる pip のバージョンが `ensurepip` で利用可能なもの以上に新しいことも保証します。 + +/// + +## `.gitignore` を追加する { #add-gitignore } **Git**を使用している場合(使用するべきでしょう)、 `.gitignore` ファイルを追加して、 `.venv` 内のあらゆるファイルをGitの管理対象から除外します。 @@ -265,9 +285,9 @@ $ echo "*" > .venv/.gitignore /// details | このコマンドの意味 -- `echo "*"` : ターミナルに `*` というテキストを「表示」しようとします。(次の部分によってその動作が少し変わります) -- `>` : `>` の左側のコマンドがターミナルに表示しようとする内容を、ターミナルには表示せず、 `>` の右側のファイルに書き込みます。 -- `.gitignore` : `*` を書き込むファイル名。 +* `echo "*"`: ターミナルに `*` というテキストを「表示」しようとします。(次の部分によってその動作が少し変わります) +* `>`: `>` の左側のコマンドがターミナルに表示しようとする内容を、ターミナルには表示せず、 `>` の右側のファイルに書き込みます。 +* `.gitignore`: `*` を書き込むファイル名。 ここで、Gitにおける `*` は「すべて」を意味するので、このコマンドによって `.venv` ディレクトリ内のすべてがGitに無視されるようになります。 @@ -279,7 +299,7 @@ $ echo "*" > .venv/.gitignore /// -## パッケージのインストール +## パッケージのインストール { #install-packages } 仮想環境を有効化した後、その中でパッケージをインストールできます。 @@ -291,7 +311,7 @@ $ echo "*" > .venv/.gitignore /// -### パッケージを直接インストールする +### パッケージを直接インストールする { #install-packages-directly } 急いでいて、プロジェクトのパッケージ要件を宣言するファイルを使いたくない場合、パッケージを直接インストールできます。 @@ -330,7 +350,7 @@ $ uv pip install "fastapi[standard]" //// -### `requirements.txt` からインストールする +### `requirements.txt` からインストールする { #install-from-requirements-txt } もし `requirements.txt` があるなら、パッケージのインストールに使用できます。 @@ -373,7 +393,7 @@ pydantic==2.8.0 /// -## プログラムを実行する +## プログラムを実行する { #run-your-program } 仮想環境を有効化した後、プログラムを実行できます。この際、仮想環境内のPythonと、そこにインストールしたパッケージが使用されます。 @@ -387,7 +407,7 @@ Hello World -## エディタの設定 +## エディタの設定 { #configure-your-editor } プロジェクトではおそらくエディタを使用するでしょう。コード補完やインラインエラーの表示ができるように、作成した仮想環境をエディタでも使えるよう設定してください。(多くの場合、自動検出されます) @@ -402,7 +422,7 @@ Hello World /// -## 仮想環境の無効化 +## 仮想環境の無効化 { #deactivate-the-virtual-environment } プロジェクトの作業が終了したら、その仮想環境を**無効化**できます。 @@ -416,9 +436,11 @@ $ deactivate これにより、 `python` コマンドを実行しても、そのプロジェクト用(のパッケージがインストールされた)仮想環境から `python` プログラムを呼び出そうとはしなくなります。 -## 作業準備完了 +## 作業準備完了 { #ready-to-work } + +これで、プロジェクトの作業を始める準備が整いました。 + -ここまでで、プロジェクトの作業を始める準備が整いました。 /// tip | 豆知識 @@ -428,9 +450,9 @@ $ deactivate /// -## なぜ仮想環境? +## なぜ仮想環境? { #why-virtual-environments } -FastAPIを使った作業をするには、 [Python](https://www.python.org/) のインストールが必要です。 +FastAPIを使った作業をするには、Python のインストールが必要です。 それから、FastAPIや、使用したいその他の**パッケージ**を**インストール**する必要があります。 @@ -438,7 +460,7 @@ FastAPIを使った作業をするには、 [Python](https://www.python.org/) ただし、`pip` を直接使用すると、パッケージは**グローバルなPython環境**(OS全体にインストールされたPython環境)にインストールされます。 -### 問題点 +### 問題点 { #the-problem } では、グローバルPython環境にパッケージをインストールすることの問題点は何でしょうか? @@ -521,7 +543,7 @@ Pythonのパッケージでは、**新しいバージョン**で**互換性を また、使用しているOS(Linux、Windows、macOS など)によっては、Pythonがすでにインストールされていることがあります。この場合、特定のバージョンのパッケージが**OSの動作に必要である**ことがあります。グローバル環境にパッケージをインストールすると、OSに付属するプログラムを**壊してしまう**可能性があります。 -## パッケージのインストール先 +## パッケージのインストール先 { #where-are-packages-installed } Pythonをインストールしたとき、ファイルを含んだいくつかのディレクトリが作成されます。 @@ -539,7 +561,7 @@ $ pip install "fastapi[standard]" -FastAPIのコードを含む圧縮ファイルが、通常は [PyPI](https://pypi.org/project/fastapi/) からダウンロードされます。 +FastAPIのコードを含む圧縮ファイルが、通常は PyPI からダウンロードされます。 また、FastAPIが依存する他のパッケージも**ダウンロード**されます。 @@ -547,7 +569,7 @@ FastAPIのコードを含む圧縮ファイルが、通常は [PyPI](https://pyp デフォルトでは、これらのファイルはPythonのインストール時に作成されるディレクトリ、つまり**グローバル環境**に配置されます。 -## 仮想環境とは +## 仮想環境とは { #what-are-virtual-environments } すべてのパッケージをグローバル環境に配置することによって生じる問題の解決策は、作業する**プロジェクトごとの仮想環境**を使用することです。 @@ -572,7 +594,7 @@ flowchart TB stone-project ~~~ azkaban-project ``` -## 仮想環境の有効化とは +## 仮想環境の有効化とは { #what-does-activating-a-virtual-environment-mean } 仮想環境を有効にしたとき、例えば次のコマンドを実行した場合を考えます: @@ -620,7 +642,7 @@ $ source .venv/Scripts/activate /// tip | 豆知識 -`PATH` 変数についての詳細は [環境変数](environment-variables.md#path環境変数){.internal-link target=_blank} を参照してください。 +`PATH` 変数についての詳細は [環境変数](environment-variables.md#path-environment-variable){.internal-link target=_blank} を参照してください。 /// @@ -701,7 +723,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python 仮想環境を有効にして変更されることは他にもありますが、これが最も重要な変更のひとつです。 -## 仮想環境の確認 +## 仮想環境の確認 { #checking-a-virtual-environment } 仮想環境が有効かどうか、例えば次のように確認できます。: @@ -753,7 +775,7 @@ LinuxやmacOSでは `which` を、Windows PowerShellでは `Get-Command` を使 /// -## なぜ仮想環境を無効化するのか +## なぜ仮想環境を無効化するのか { #why-deactivate-a-virtual-environment } 例えば、`philosophers-stone` (賢者の石)というプロジェクトで作業をしていて、**その仮想環境を有効にし**、必要なパッケージをインストールしてその環境内で作業を進めているとします。 @@ -786,7 +808,7 @@ Traceback (most recent call last): -しかし、その仮想環境を無効化し、 `prisoner-of-azkaban` (アズカバンの囚人)のための新しい仮想環境を有効にすれば、 `python` を実行したときに `prisoner-of-azkaban` (アズカバンの囚人)の仮想環境の Python が使用されるようになります。 +しかし、その仮想環境を無効化し、 `prisoner-of-askaban` のための新しい仮想環境を有効にすれば、 `python` を実行したときに `prisoner-of-azkaban` (アズカバンの囚人)の仮想環境の Python が使用されるようになります。
@@ -807,7 +829,7 @@ I solemnly swear 🐺
-## 代替手段 +## 代替手段 { #alternatives } これは、あらゆる仕組みを**根本から**学ぶためのシンプルな入門ガイドです。 @@ -824,7 +846,7 @@ I solemnly swear 🐺 * パッケージとそのバージョンの、依存関係を含めた**厳密な**組み合わせを保持し、これによって、本番環境で、開発環境と全く同じようにプロジェクトを実行できる(これは**locking**と呼ばれます) * その他のさまざまな機能 -## まとめ +## まとめ { #conclusion } ここまで読みすべて理解したなら、世間の多くの開発者と比べて、仮想環境について**あなたはより多くのことを知っています**。🤓 diff --git a/docs/ja/llm-prompt.md b/docs/ja/llm-prompt.md index 18909cd59..de2616746 100644 --- a/docs/ja/llm-prompt.md +++ b/docs/ja/llm-prompt.md @@ -30,8 +30,7 @@ Use the following preferred translations when they apply in documentation prose: - request (HTTP): リクエスト - response (HTTP): レスポンス -- path operation: パスオペレーション -- path operation function: パスオペレーション関数 +- path operation: path operation (do not translate) ### `///` admonitions diff --git a/scripts/docs.py b/scripts/docs.py index 20bf1c168..ca2baf97d 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -23,6 +23,7 @@ SUPPORTED_LANGS = { "en", "de", "es", + "ja", "ko", "pt", "ru", From 71ceac20dad852db7741118325cdf2499bd30592 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 16:44:48 +0000 Subject: [PATCH 178/243] =?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 1a56e5eca..38d51d821 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -46,6 +46,7 @@ hide: ### Translations +* 🌐 Update translations for ja (update-outdated). PR [#14588](https://github.com/fastapi/fastapi/pull/14588) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for uk (update outdated, found by fixer tool). PR [#14739](https://github.com/fastapi/fastapi/pull/14739) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for tr (update-outdated). PR [#14745](https://github.com/fastapi/fastapi/pull/14745) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update `llm-prompt.md` for Korean language. PR [#14763](https://github.com/fastapi/fastapi/pull/14763) by [@seuthootDev](https://github.com/seuthootDev). From ad29e44c810c1fb1107d1cdc7b0409acc7e0feed Mon Sep 17 00:00:00 2001 From: Roman Mashevskyi Date: Wed, 4 Feb 2026 18:47:51 +0200 Subject: [PATCH 179/243] =?UTF-8?q?=F0=9F=8C=90=20Improve=20LLM=20prompt?= =?UTF-8?q?=20of=20`uk`=20documentation=20(#14795)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/uk/llm-prompt.md | 67 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/docs/uk/llm-prompt.md b/docs/uk/llm-prompt.md index f1c5377a4..e8cd3dabc 100644 --- a/docs/uk/llm-prompt.md +++ b/docs/uk/llm-prompt.md @@ -8,6 +8,7 @@ Language code: uk. - Use polite/formal address consistent with existing Ukrainian docs (use “ви/ваш”). - Keep the tone concise and technical. +- Use one style of dashes. For example, if text contains "-" then use only this symbol to represent a dash. ### Headings @@ -32,6 +33,71 @@ Use the following preferred translations when they apply in documentation prose: - response (HTTP): відповідь - path operation: операція шляху - path operation function: функція операції шляху +- prompt: підсказка +- check: перевірка +- Parallel Server Gateway Interface: Інтерфейс Шлюзу Паралельного Сервера +- Mozilla Developer Network: Мережа Розробників Mozilla +- tutorial: навчальний посібник +- advanced user guide: просунутий посібник користувача +- deep learning: глибоке навчання +- machine learning: машинне навчання +- dependency injection: впровадження залежностей +- digest (HTTP): дайджест +- basic authentication (HTTP): базова автентифікація +- JSON schema: Схема JSON +- password flow: потік паролю +- mobile: мобільний +- body: тіло +- form: форма +- path: шлях +- query: запит +- cookie: кукі +- header: заголовок +- startup: запуск +- shutdown: вимкнення +- lifespan: тривалість життя +- authorization: авторизація +- forwarded header: направлений заголовок +- dependable: залежний +- dependent: залежний +- bound: межа +- concurrency: рівночасність +- parallelism: паралелізм +- multiprocessing: багатопроцесорність +- env var: змінна оточення +- dict: словник +- enum: перелік +- issue: проблема +- server worker: серверний працівник +- worker: працівник +- software development kit: набір для розробки програмного забезпечення +- bearer token: токен носія +- breaking change: несумісна зміна +- bug: помилка +- button: кнопка +- callable: викликаємий +- code: код +- commit: фіксація +- context manager: менеджер контексту +- coroutine: співпрограма +- engine: рушій +- fake X: фальшивий X +- item: предмет +- lock: блокування +- middleware: проміжне програмне забезпечення +- mounting: монтування +- origin: джерело +- override: переписування +- payload: корисне навантаження +- processor: процесор +- property: властивість +- proxy: представник +- pull request: запит на витяг +- random-access memory: пам'ять з довільним доступом +- status code: код статусу +- string: строка +- tag: мітка +- wildcard: дика карта ### `///` admonitions @@ -44,3 +110,4 @@ Use the following preferred translations when they apply in documentation prose: - `/// warning | Попередження` - `/// info | Інформація` - `/// danger | Обережно` +- `/// check | Перевірте` From 0e064009fb02773cd90d515647cda177776b2fbb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 16:48:16 +0000 Subject: [PATCH 180/243] =?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 38d51d821..50dedf788 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -46,6 +46,7 @@ hide: ### Translations +* 🌐 Improve LLM prompt of `uk` documentation. PR [#14795](https://github.com/fastapi/fastapi/pull/14795) by [@roli2py](https://github.com/roli2py). * 🌐 Update translations for ja (update-outdated). PR [#14588](https://github.com/fastapi/fastapi/pull/14588) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for uk (update outdated, found by fixer tool). PR [#14739](https://github.com/fastapi/fastapi/pull/14739) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for tr (update-outdated). PR [#14745](https://github.com/fastapi/fastapi/pull/14745) by [@tiangolo](https://github.com/tiangolo). From 84a5bcf82d265eb72fc2845f6625785a1524f023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 4 Feb 2026 09:00:40 -0800 Subject: [PATCH 181/243] =?UTF-8?q?=E2=AC=87=EF=B8=8F=20Downgrade=20LLM=20?= =?UTF-8?q?translations=20model=20to=20GPT-5=20to=20reduce=20mistakes=20(#?= =?UTF-8?q?14823)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/translate.py b/scripts/translate.py index 072383514..31c44583d 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -86,7 +86,7 @@ def translate_page( print(f"Found existing translation: {out_path}") old_translation = out_path.read_text(encoding="utf-8") print(f"Translating {en_path} to {language} ({language_name})") - agent = Agent("openai:gpt-5.2") + agent = Agent("openai:gpt-5") prompt_segments = [ general_prompt, From 734c95b05a19aac1a87abce53fba3fda18fc9b9b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 17:01:08 +0000 Subject: [PATCH 182/243] =?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 50dedf788..0dd61444b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -70,6 +70,7 @@ hide: ### Internal +* ⬇️ Downgrade LLM translations model to GPT-5 to reduce mistakes. PR [#14823](https://github.com/fastapi/fastapi/pull/14823) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix translation script commit in place. PR [#14818](https://github.com/fastapi/fastapi/pull/14818) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update translation script to retry if LLM-response doesn't pass validation with Translation Fixer tool. PR [#14749](https://github.com/fastapi/fastapi/pull/14749) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Run tests only on relevant code changes (not on docs). PR [#14813](https://github.com/fastapi/fastapi/pull/14813) by [@tiangolo](https://github.com/tiangolo). From 1de0de56c8e9f90d1e8f5fe82323f343d0c2a936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 4 Feb 2026 18:33:29 +0100 Subject: [PATCH 183/243] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?8.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0dd61444b..e3a133c18 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.128.1 + ### Features * ✨ Add `viewport` meta tag to improve Swagger UI on mobile devices. PR [#14777](https://github.com/fastapi/fastapi/pull/14777) by [@Joab0](https://github.com/Joab0). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 6133787b0..160e7235c 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.128.0" +__version__ = "0.128.1" from starlette import status as status From 0e68d36cd5796384e833e97b57e98d17adc3c5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 4 Feb 2026 09:35:21 -0800 Subject: [PATCH 184/243] =?UTF-8?q?=F0=9F=94=A8=20Update=20docs=20and=20tr?= =?UTF-8?q?anslations=20scripts,=20enable=20Turkish=20(#14824)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/mkdocs.yml | 2 ++ scripts/docs.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 34d489d92..f713f2b12 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -325,6 +325,8 @@ extra: name: pt - português - link: /ru/ name: ru - русский язык + - link: /tr/ + name: tr - Türkçe - link: /uk/ name: uk - українська мова extra_css: diff --git a/scripts/docs.py b/scripts/docs.py index ca2baf97d..c1fb84bba 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -20,14 +20,18 @@ from slugify import slugify as py_slugify logging.basicConfig(level=logging.INFO) SUPPORTED_LANGS = { - "en", "de", + "en", "es", + # "fr", "ja", "ko", "pt", "ru", + "tr", "uk", + # "zh", + # "zh-hant", } From 5ca11c59e36e8a177cec8341861a26a084415a11 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 17:36:12 +0000 Subject: [PATCH 185/243] =?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 e3a133c18..983e19fdb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🔨 Update docs and translations scripts, enable Turkish. PR [#14824](https://github.com/fastapi/fastapi/pull/14824) by [@tiangolo](https://github.com/tiangolo). + ## 0.128.1 ### Features From 2e170b9585fbbf57120598632cd7e13a11028c57 Mon Sep 17 00:00:00 2001 From: Rayyan oumlil Date: Thu, 5 Feb 2026 07:48:51 -0500 Subject: [PATCH 186/243] =?UTF-8?q?=F0=9F=93=9D=20Fix=20duplicate=20word?= =?UTF-8?q?=20in=20`advanced-dependencies.md`=20(#14815)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/advanced-dependencies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md index 37f5c78f2..0d03507b7 100644 --- a/docs/en/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -120,7 +120,7 @@ The exit code, the automatic closing of the `Session` in: {* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} -...would be run after the the response finishes sending the slow data: +...would be run after the response finishes sending the slow data: {* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} From 64d0ee9104da25bfe11e0df5d560d3f989845b6f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 12:49:18 +0000 Subject: [PATCH 187/243] =?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 983e19fdb..6f3674911 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Fix duplicate word in `advanced-dependencies.md`. PR [#14815](https://github.com/fastapi/fastapi/pull/14815) by [@Rayyan-Oumlil](https://github.com/Rayyan-Oumlil). + ### Translations * 🔨 Update docs and translations scripts, enable Turkish. PR [#14824](https://github.com/fastapi/fastapi/pull/14824) by [@tiangolo](https://github.com/tiangolo). From 31faba8e41da15d3b71a46dadea8366f548e8892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 07:31:31 -0800 Subject: [PATCH 188/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20uk=20(update-outdated)=20(#14822)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: Maksym Zavalniuk Co-authored-by: Roman Mashevskyi --- docs/uk/docs/index.md | 42 ++++++++----------- docs/uk/docs/tutorial/body-multiple-params.md | 13 +++--- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md index 526409c5c..4f089c4e1 100644 --- a/docs/uk/docs/index.md +++ b/docs/uk/docs/index.md @@ -8,7 +8,7 @@ FastAPI

- Фреймворк FastAPI: висока продуктивність, легко вивчати, швидко писати код, готовий до продакшину + Фреймворк FastAPI - це висока продуктивність, легко вивчати, швидко писати код, готовий до продакшину

@@ -33,14 +33,14 @@ --- -FastAPI — це сучасний, швидкий (високопродуктивний) вебфреймворк для створення API за допомогою Python, що базується на стандартних підказках типів Python. +FastAPI - це сучасний, швидкий (високопродуктивний) вебфреймворк для створення API за допомогою Python, що базується на стандартних підказках типів Python. Ключові особливості: * **Швидкий**: дуже висока продуктивність, на рівні з **NodeJS** та **Go** (завдяки Starlette та Pydantic). [Один із найшвидших Python-фреймворків](#performance). * **Швидке написання коду**: пришвидшує розробку функціоналу приблизно на 200%–300%. * * **Менше помилок**: зменшує приблизно на 40% кількість помилок, спричинених людиною (розробником). * -* **Інтуїтивний**: чудова підтримка редакторами коду. Автодоповнення всюди. Менше часу на налагодження. +* **Інтуїтивний**: чудова підтримка редакторами коду. Автодоповнення всюди. Менше часу на налагодження. * **Простий**: спроєктований так, щоб бути простим у використанні та вивченні. Менше часу на читання документації. * **Короткий**: мінімізує дублювання коду. Кілька можливостей з кожного оголошення параметра. Менше помилок. * **Надійний**: ви отримуєте код, готовий до продакшину. З автоматичною інтерактивною документацією. @@ -127,9 +127,9 @@ FastAPI — це сучасний, швидкий (високопродукти -Якщо ви створюєте застосунок CLI для використання в терміналі замість веб-API, зверніть увагу на **Typer**. +Якщо ви створюєте застосунок CLI для використання в терміналі замість веб-API, зверніть увагу на **Typer**. -**Typer** — молодший брат FastAPI. І його задумано як **FastAPI для CLI**. ⌨️ 🚀 +**Typer** - молодший брат FastAPI. І його задумано як **FastAPI для CLI**. ⌨️ 🚀 ## Вимоги { #requirements } @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Створіть файл `main.py` з: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): Якщо ваш код використовує `async` / `await`, скористайтеся `async def`: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ INFO: Application startup complete. Оголосіть тіло, використовуючи стандартні типи Python, завдяки Pydantic. -```Python hl_lines="4 9-12 25-27" -from typing import Union - +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -300,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -309,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -374,7 +368,7 @@ item: Item * Валідацію даних: * Автоматичні та зрозумілі помилки, коли дані некоректні. * Валідацію навіть для глибоко вкладених JSON-обʼєктів. -* Перетворення вхідних даних: з мережі до даних і типів Python. Читання з: +* Перетворення вхідних даних: з мережі до даних і типів Python. Читання з: * JSON. * Параметрів шляху. * Параметрів запиту. @@ -382,7 +376,7 @@ item: Item * Headers. * Forms. * Files. -* Перетворення вихідних даних: перетворення з даних і типів Python у мережеві дані (як JSON): +* Перетворення вихідних даних: перетворення з даних і типів Python у мережеві дані (як JSON): * Перетворення типів Python (`str`, `int`, `float`, `bool`, `list`, тощо). * Обʼєктів `datetime`. * Обʼєктів `UUID`. @@ -439,9 +433,9 @@ item: Item ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -Для більш повного прикладу, що включає більше можливостей, перегляньте Туторіал — Посібник користувача. +Для більш повного прикладу, що включає більше можливостей, перегляньте Навчальний посібник - Посібник користувача. -**Spoiler alert**: туторіал — посібник користувача містить: +**Попередження про спойлер**: навчальний посібник - посібник користувача містить: * Оголошення **параметрів** з інших різних місць, як-от: **headers**, **cookies**, **form fields** та **files**. * Як встановлювати **обмеження валідації** як `maximum_length` або `regex`. @@ -500,11 +494,11 @@ Deploying to FastAPI Cloud... Він забезпечує той самий **developer experience** створення застосунків на FastAPI під час їх **розгортання** у хмарі. 🎉 -FastAPI Cloud — основний спонсор і джерело фінансування open source проєктів *FastAPI and friends*. ✨ +FastAPI Cloud - основний спонсор і джерело фінансування open source проєктів *FastAPI and friends*. ✨ #### Розгортання в інших хмарних провайдерів { #deploy-to-other-cloud-providers } -FastAPI — open source і базується на стандартах. Ви можете розгортати застосунки FastAPI в будь-якому хмарному провайдері, який ви оберете. +FastAPI - open source проект і базується на стандартах. Ви можете розгортати застосунки FastAPI в будь-якому хмарному провайдері, який ви оберете. Дотримуйтеся інструкцій вашого хмарного провайдера, щоб розгорнути застосунки FastAPI у нього. 🤓 @@ -530,7 +524,7 @@ FastAPI залежить від Pydantic і Starlette. * httpx - потрібно, якщо ви хочете використовувати `TestClient`. * jinja2 - потрібно, якщо ви хочете використовувати конфігурацію шаблонів за замовчуванням. -* python-multipart - потрібно, якщо ви хочете підтримувати «parsing» форм за допомогою `request.form()`. +* python-multipart - потрібно, якщо ви хочете підтримувати «parsing» форм за допомогою `request.form()`. Використовується FastAPI: diff --git a/docs/uk/docs/tutorial/body-multiple-params.md b/docs/uk/docs/tutorial/body-multiple-params.md index dc9a768c3..f541beea7 100644 --- a/docs/uk/docs/tutorial/body-multiple-params.md +++ b/docs/uk/docs/tutorial/body-multiple-params.md @@ -102,15 +102,16 @@ Оскільки за замовчуванням одиничні значення інтерпретуються як параметри запиту, вам не потрібно явно додавати `Query`, ви можете просто зробити: +```Python +q: str | None = None +``` + +Або в Python 3.9: + ```Python q: Union[str, None] = None ``` -Або в Python 3.10 і вище: - -```Python -q: str | None = None -``` Наприклад: @@ -129,7 +130,7 @@ q: str | None = None За замовчуванням **FastAPI** очікуватиме його тіло безпосередньо. -Але якщо ви хочете, щоб він очікував JSON з ключем `item`, а всередині нього — вміст моделі, як це відбувається, коли ви оголошуєте додаткові параметри тіла, ви можете використати спеціальний параметр `Body` — `embed`: +Але якщо ви хочете, щоб він очікував JSON з ключем `item`, а всередині нього - вміст моделі, як це відбувається, коли ви оголошуєте додаткові параметри тіла, ви можете використати спеціальний параметр `Body` - `embed`: ```Python item: Item = Body(embed=True) From c00cebc5c68331046741f862359b83d0f428e518 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 15:31:55 +0000 Subject: [PATCH 189/243] =?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 6f3674911..f52be20d3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Update translations for uk (update-outdated). PR [#14822](https://github.com/fastapi/fastapi/pull/14822) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update docs and translations scripts, enable Turkish. PR [#14824](https://github.com/fastapi/fastapi/pull/14824) by [@tiangolo](https://github.com/tiangolo). ## 0.128.1 From b4ed7e5b0644d180a2e437aa266f8696a0b89424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 07:32:56 -0800 Subject: [PATCH 190/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20zh-hant=20(update-outdated)=20(#14825)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/zh-hant/docs/about/index.md | 2 +- docs/zh-hant/docs/benchmarks.md | 6 +- docs/zh-hant/docs/deployment/cloud.md | 21 +- docs/zh-hant/docs/deployment/index.md | 8 +- docs/zh-hant/docs/environment-variables.md | 26 +- docs/zh-hant/docs/how-to/index.md | 2 +- docs/zh-hant/docs/index.md | 461 +++++++++++------- docs/zh-hant/docs/learn/index.md | 4 +- docs/zh-hant/docs/resources/index.md | 4 +- docs/zh-hant/docs/tutorial/first-steps.md | 205 +++++--- docs/zh-hant/docs/tutorial/index.md | 81 ++- .../docs/tutorial/query-param-models.md | 10 +- docs/zh-hant/docs/virtual-environments.md | 68 ++- 13 files changed, 532 insertions(+), 366 deletions(-) diff --git a/docs/zh-hant/docs/about/index.md b/docs/zh-hant/docs/about/index.md index 5dcee68f2..cf5b5742c 100644 --- a/docs/zh-hant/docs/about/index.md +++ b/docs/zh-hant/docs/about/index.md @@ -1,3 +1,3 @@ -# 關於 FastAPI +# 關於 { #about } 關於 FastAPI、其設計、靈感來源等更多資訊。 🤓 diff --git a/docs/zh-hant/docs/benchmarks.md b/docs/zh-hant/docs/benchmarks.md index c59e8e71c..df49621c5 100644 --- a/docs/zh-hant/docs/benchmarks.md +++ b/docs/zh-hant/docs/benchmarks.md @@ -1,10 +1,10 @@ -# 基準測試 +# 基準測試 { #benchmarks } 由第三方機構 TechEmpower 的基準測試表明在 Uvicorn 下運行的 **FastAPI** 應用程式是 最快的 Python 可用框架之一,僅次於 Starlette 和 Uvicorn 本身(於 FastAPI 內部使用)。 但是在查看基準得分和對比時,請注意以下幾點。 -## 基準測試和速度 +## 基準測試和速度 { #benchmarks-and-speed } 當你查看基準測試時,時常會見到幾個不同類型的工具被同時進行測試。 @@ -31,4 +31,4 @@ * FastAPI 在 Starlette 基礎之上提供了更多功能。包含建構 API 時所需要的功能,例如資料驗證和序列化。FastAPI 可以幫助你自動產生 API 文件,(應用程式啟動時將會自動生成文件,所以不會增加應用程式運行時的開銷)。 * 如果你沒有使用 FastAPI 而是直接使用 Starlette(或其他工具,如 Sanic、Flask、Responder 等),你將必須自行實現所有資料驗證和序列化。因此,你的最終應用程式仍然具有與使用 FastAPI 建置相同的開銷。在許多情況下,這種資料驗證和序列化是應用程式中編寫最大量的程式碼。 * 因此透過使用 FastAPI,你可以節省開發時間、錯誤與程式碼數量,並且相比不使用 FastAPI 你很大可能會獲得相同或更好的效能(因為那樣你必須在程式碼中實現所有相同的功能)。 - * 如果你要與 FastAPI 比較,請將其與能夠提供資料驗證、序列化和文件的網頁應用程式框架(或工具集)進行比較,例如 Flask-apispec、NestJS、Molten 等框架。 + * 如果你要與 FastAPI 比較,請將其與能夠提供資料驗證、序列化和文件的網頁應用程式框架(或工具集)進行比較,例如 Flask-apispec、NestJS、Molten 等框架。具備整合式自動資料驗證、序列化與文件的框架。 diff --git a/docs/zh-hant/docs/deployment/cloud.md b/docs/zh-hant/docs/deployment/cloud.md index 426937d3e..fffb2fcfe 100644 --- a/docs/zh-hant/docs/deployment/cloud.md +++ b/docs/zh-hant/docs/deployment/cloud.md @@ -1,13 +1,24 @@ -# 在雲端部署 FastAPI +# 在雲端供應商上部署 FastAPI { #deploy-fastapi-on-cloud-providers } 你幾乎可以使用**任何雲端供應商**來部署你的 FastAPI 應用程式。 在大多數情況下,主要的雲端供應商都有部署 FastAPI 的指南。 -## 雲端供應商 - 贊助商 +## FastAPI Cloud { #fastapi-cloud } -一些雲端供應商 ✨ [**贊助 FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨,這確保了 FastAPI 及其**生態系統**持續健康地**發展**。 +**FastAPI Cloud** 由 **FastAPI** 的同一位作者與團隊打造。 -這也展現了他們對 FastAPI 和其**社群**(包括你)的真正承諾,他們不僅希望為你提供**優質的服務**,還希望確保你擁有一個**良好且健康的框架**:FastAPI。🙇 +它讓你以最少的投入,簡化 **建置**、**部署** 與 **存取** API 的流程。 -你可能會想嘗試他們的服務,以下有他們的指南. +它把使用 FastAPI 開發應用的同樣**優秀的開發者體驗**,帶到將它們**部署**到雲端的過程中。🎉 + +FastAPI Cloud 是 *FastAPI and friends* 開源專案的主要贊助與資金提供者。✨ + +## 雲端供應商 - 贊助商 { #cloud-providers-sponsors } + +其他一些雲端供應商也會 ✨ [**贊助 FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨。🙇 + +你也可以參考他們的指南並試用其服務: + +* Render +* Railway diff --git a/docs/zh-hant/docs/deployment/index.md b/docs/zh-hant/docs/deployment/index.md index 1726562b4..9edd3368b 100644 --- a/docs/zh-hant/docs/deployment/index.md +++ b/docs/zh-hant/docs/deployment/index.md @@ -1,8 +1,8 @@ -# 部署 +# 部署 { #deployment } 部署 **FastAPI** 應用程式相對容易。 -## 部署是什麼意思 +## 部署是什麼意思 { #what-does-deployment-mean } **部署**應用程式指的是執行一系列必要的步驟,使其能夠**讓使用者存取和使用**。 @@ -10,12 +10,14 @@ 這與**開發**階段形成鮮明對比,在**開發**階段,你會不斷更改程式碼、破壞程式碼、修復程式碼,然後停止和重新啟動伺服器等。 -## 部署策略 +## 部署策略 { #deployment-strategies } 根據你的使用場景和使用工具,有多種方法可以實現此目的。 你可以使用一些工具自行**部署伺服器**,你也可以使用能為你完成部分工作的**雲端服務**,或其他可能的選項。 +例如,我們(FastAPI 的團隊)打造了 **FastAPI Cloud**,讓將 FastAPI 應用程式部署到雲端變得盡可能流暢,並保持與使用 FastAPI 開發時相同的開發者體驗。 + 我將向你展示在部署 **FastAPI** 應用程式時你可能應該記住的一些主要概念(儘管其中大部分適用於任何其他類型的 Web 應用程式)。 在接下來的部分中,你將看到更多需要記住的細節以及一些技巧。 ✨ diff --git a/docs/zh-hant/docs/environment-variables.md b/docs/zh-hant/docs/environment-variables.md index a1598fc01..5b684b9e6 100644 --- a/docs/zh-hant/docs/environment-variables.md +++ b/docs/zh-hant/docs/environment-variables.md @@ -1,4 +1,4 @@ -# 環境變數 +# 環境變數 { #environment-variables } /// tip @@ -10,7 +10,7 @@ 環境變數對於處理應用程式**設定**(作為 Python **安裝**的一部分等方面)非常有用。 -## 建立和使用環境變數 +## 建立和使用環境變數 { #create-and-use-env-vars } 你在 **shell(終端機)**中就可以**建立**和使用環境變數,並不需要用到 Python: @@ -50,7 +50,7 @@ Hello Wade Wilson //// -## 在 Python 中讀取環境變數 +## 在 Python 中讀取環境變數 { #read-env-vars-in-python } 你也可以在 Python **之外**的終端機中建立環境變數(或使用其他方法),然後在 Python 中**讀取**它們。 @@ -65,7 +65,7 @@ print(f"Hello {name} from Python") /// tip -第二個參數是 `os.getenv()` 的預設回傳值。 +第二個參數是 `os.getenv()` 的預設回傳值。 如果沒有提供,預設值為 `None`,這裡我們提供 `"World"` 作為預設值。 @@ -153,19 +153,19 @@ Hello World from Python /// tip -你可以在 The Twelve-Factor App: 配置中了解更多資訊。 +你可以在 The Twelve-Factor App: 配置中了解更多資訊。 /// -## 型別和驗證 +## 型別和驗證 { #types-and-validation } 這些環境變數只能處理**文字字串**,因為它們是位於 Python 範疇之外的,必須與其他程式和作業系統的其餘部分相容(甚至與不同的作業系統相容,如 Linux、Windows、macOS)。 這意味著從環境變數中讀取的**任何值**在 Python 中都將是一個 `str`,任何型別轉換或驗證都必須在程式碼中完成。 -你將在[進階使用者指南 - 設定和環境變數](./advanced/settings.md)中了解更多關於使用環境變數處理**應用程式設定**的資訊。 +你將在[進階使用者指南 - 設定和環境變數](./advanced/settings.md){.internal-link target=_blank}中了解更多關於使用環境變數處理**應用程式設定**的資訊。 -## `PATH` 環境變數 +## `PATH` 環境變數 { #path-environment-variable } 有一個**特殊的**環境變數稱為 **`PATH`**,作業系統(Linux、macOS、Windows)用它來查找要執行的程式。 @@ -209,7 +209,7 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3 如果找到了,那麼作業系統將**使用它**;否則,作業系統會繼續在**其他目錄**中查找。 -### 安裝 Python 並更新 `PATH` +### 安裝 Python 並更新 `PATH` { #installing-python-and-updating-the-path } 安裝 Python 時,可能會詢問你是否要更新 `PATH` 環境變數。 @@ -233,7 +233,7 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3 假設你安裝了 Python,並將其安裝在目錄 `C:\opt\custompython\bin` 中。 -如果你選擇更新 `PATH` 環境變數(在 Python 安裝程式中,這個選項是名為 `Add Python x.xx to PATH` 的勾選框——譯者註),那麼安裝程式會將 `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 @@ -285,13 +285,13 @@ $ C:\opt\custompython\bin\python //// -當學習[虛擬環境](virtual-environments.md)時,這些資訊將會很有用。 +當學習[虛擬環境](virtual-environments.md){.internal-link target=_blank}時,這些資訊將會很有用。 -## 結論 +## 結論 { #conclusion } 透過這個教學,你應該對**環境變數**是什麼以及如何在 Python 中使用它們有了基本的了解。 -你也可以在環境變數 - 維基百科 (Wikipedia for Environment Variable) 中了解更多關於它們的資訊。 +你也可以在 環境變數的維基百科條目 中閱讀更多。 在許多情況下,環境變數的用途和適用性可能不會立刻顯現。但是在開發過程中,它們會在許多不同的場景中出現,因此瞭解它們是非常必要的。 diff --git a/docs/zh-hant/docs/how-to/index.md b/docs/zh-hant/docs/how-to/index.md index db740140d..6c9a8202c 100644 --- a/docs/zh-hant/docs/how-to/index.md +++ b/docs/zh-hant/docs/how-to/index.md @@ -1,4 +1,4 @@ -# 使用指南 - 範例集 +# 使用指南 - 範例集 { #how-to-recipes } 在這裡,你將會看到**不同主題**的範例或「如何使用」的指南。 diff --git a/docs/zh-hant/docs/index.md b/docs/zh-hant/docs/index.md index 4390d9609..a31647f3c 100644 --- a/docs/zh-hant/docs/index.md +++ b/docs/zh-hant/docs/index.md @@ -1,5 +1,11 @@ +# FastAPI { #fastapi } + + +

- FastAPI + FastAPI

FastAPI 框架,高效能,易於學習,快速開發,適用於生產環境 @@ -21,138 +27,140 @@ --- -**文件**: https://fastapi.tiangolo.com +**文件**: https://fastapi.tiangolo.com/zh-hant **程式碼**: https://github.com/fastapi/fastapi --- -FastAPI 是一個現代、快速(高效能)的 web 框架,用於 Python 並採用標準 Python 型別提示。 +FastAPI 是一個現代、快速(高效能)的 Web 框架,用於以 Python 並基於標準的 Python 型別提示來構建 API。 主要特點包含: -- **快速**: 非常高的效能,可與 **NodeJS** 和 **Go** 效能相當 (歸功於 Starlette and Pydantic)。 [FastAPI 是最快的 Python web 框架之一](#performance)。 -- **極速開發**: 提高開發功能的速度約 200% 至 300%。 \* -- **更少的 Bug**: 減少約 40% 的人為(開發者)導致的錯誤。 \* -- **直覺**: 具有出色的編輯器支援,處處都有自動補全以減少偵錯時間。 -- **簡單**: 設計上易於使用和學習,大幅減少閱讀文件的時間。 -- **簡潔**: 最小化程式碼重複性。可以通過不同的參數聲明來實現更豐富的功能,和更少的錯誤。 -- **穩健**: 立即獲得生產級可用的程式碼,還有自動生成互動式文件。 -- **標準化**: 基於 (且完全相容於) OpenAPIs 的相關標準:OpenAPI(之前被稱為 Swagger)和JSON Schema。 +* **快速**:非常高的效能,可與 **NodeJS** 和 **Go** 相當(歸功於 Starlette 和 Pydantic)。[最快的 Python 框架之一](#performance)。 +* **極速開發**:開發功能的速度可提升約 200% 至 300%。* +* **更少的 Bug**:減少約 40% 的人為(開發者)錯誤。* +* **直覺**:具有出色的編輯器支援,處處都有 自動補全。更少的偵錯時間。 +* **簡單**:設計上易於使用與學習。更少的讀文件時間。 +* **簡潔**:最小化程式碼重複性。每個參數宣告可帶來多個功能。更少的錯誤。 +* **穩健**:立即獲得可投入生產的程式碼,並自動生成互動式文件。 +* **標準化**:基於(且完全相容於)API 的開放標準:OpenAPI(之前稱為 Swagger)和 JSON Schema。 -\* 基於內部開發團隊在建立生產應用程式時的測試預估。 +* 基於內部開發團隊在建立生產應用程式時的測試預估。 -## 贊助 +## 贊助 { #sponsors } -{% if sponsors %} +### 基石贊助商 { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### 金級與銀級贊助商 { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} -其他贊助商 +其他贊助商 -## 評價 +## 評價 { #opinions } -"_[...] 近期大量的使用 **FastAPI**。 [...] 目前正在計畫在**微軟**團隊的**機器學習**服務中導入。其中一些正在整合到核心的 **Windows** 產品和一些 **Office** 產品。_" +"_[...] 近期大量使用 **FastAPI**。[...] 我實際上打算在我在**微軟**團隊的所有**機器學習**服務上使用它。其中一些正在整合到核心的 **Windows** 產品,以及一些 **Office** 產品。_"

Kabir Khan - Microsoft (ref)
--- -"_我們使用 **FastAPI** 來建立產生**預測**結果的 **REST** 伺服器。 [for Ludwig]_" +"_我們採用了 **FastAPI** 函式庫來啟動一個 **REST** 伺服器,供查詢以取得**預測**。[for Ludwig]_"
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
--- -"_**Netflix** 很榮幸地宣布開源**危機管理**協調框架: **Dispatch**! [是使用 **FastAPI** 建構]_" +"_**Netflix** 很高興宣布我們的**危機管理**協調框架 **Dispatch** 開源![使用 **FastAPI** 建構]_"
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
--- -"_我對 **FastAPI** 興奮得不得了。它太有趣了!_" +"_我對 **FastAPI** 興奮得不得了。超好玩!_" -
Brian Okken - Python Bytes podcast host (ref)
+
Brian Okken - Python Bytes podcast 主持人 (ref)
--- -"_老實說,你建造的東西看起來非常堅固和精緻。在很多方面,這就是我想要的,看到有人建造它真的很鼓舞人心。_" +"_老實說,你們做的看起來非常穩健又精緻。很多方面都正是我希望 **Hug** 成為的樣子——看到有人把它建出來真的很鼓舞人心。_" -
Timothy Crosley - Hug creator (ref)
+
Timothy Crosley - Hug 創作者 (ref)
--- -"_如果您想學習一種用於構建 REST API 的**現代框架**,不能錯過 **FastAPI** [...] 它非常快速、且易於使用和學習 [...]_" +"_如果你想學一個用於構建 REST API 的**現代框架**,看看 **FastAPI** [...] 它很快、易用、也容易學習 [...]_" -"_我們的 **APIs** 已經改用 **FastAPI** [...] 我想你會喜歡它 [...]_" +"_我們的 **API** 已經改用 **FastAPI** [...] 我想你會喜歡它 [...]_" -
Ines Montani - Matthew Honnibal - Explosion AI 創辦人 - spaCy creators (ref) - (ref)
+
Ines Montani - Matthew Honnibal - Explosion AI 創辦人 - spaCy 創作者 (ref) - (ref)
--- -"_如果有人想要建立一個生產環境的 Python API,我強烈推薦 **FastAPI**,它**設計精美**,**使用簡單**且**高度可擴充**,它已成為我們 API 優先開發策略中的**關鍵組件**,並且驅動了許多自動化服務,例如我們的 Virtual TAC Engineer。_" +"_如果有人想要建立一個可投入生產的 Python API,我強烈推薦 **FastAPI**。它**設計精美**、**使用簡單**且**高度可擴充**,已成為我們 API 優先開發策略中的**關鍵組件**,推動了許多自動化與服務,例如我們的 Virtual TAC Engineer。_"
Deon Pillsbury - Cisco (ref)
--- -## **Typer**,命令列中的 FastAPI +## FastAPI 迷你紀錄片 { #fastapi-mini-documentary } + +在 2025 年底發布了一支 FastAPI 迷你紀錄片,你可以在線上觀看: + +FastAPI Mini Documentary + +## **Typer**,命令列的 FastAPI { #typer-the-fastapi-of-clis } -如果你不是在開發網頁 API,而是正在開發一個在終端機中運行的命令列應用程式,不妨嘗試 **Typer**。 +如果你不是在做 Web API,而是要建立一個在終端機中使用的 CLI 應用程式,可以看看 **Typer**。 -**Typer** 是 FastAPI 的小兄弟。他立志成為命令列的 **FastAPI**。 ⌨️ 🚀 +**Typer** 是 FastAPI 的小老弟。他立志成為命令列世界的 **FastAPI**。⌨️ 🚀 -## 安裝需求 +## 需求 { #requirements } FastAPI 是站在以下巨人的肩膀上: -- Starlette 負責網頁的部分 -- Pydantic 負責資料的部分 +* Starlette 負責 Web 部分。 +* Pydantic 負責資料部分。 -## 安裝 +## 安裝 { #installation } + +建立並啟用一個虛擬環境,然後安裝 FastAPI:
```console -$ pip install fastapi +$ pip install "fastapi[standard]" ---> 100% ```
-你同時也會需要 ASGI 伺服器用於生產環境,像是 UvicornHypercorn。 +**注意**:請務必將 `"fastapi[standard]"` 用引號包起來,以確保在所有終端機中都能正常運作。 -
+## 範例 { #example } -```console -$ pip install "uvicorn[standard]" +### 建立 { #create-it } ----> 100% -``` - -
- -## 範例 - -### 建立 - -- 建立一個 python 檔案 `main.py`,並寫入以下程式碼: +建立檔案 `main.py`,內容如下: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -164,18 +172,16 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ```
-或可以使用 async def... +或使用 async def... -如果你的程式使用 `async` / `await`,請使用 `async def`: - -```Python hl_lines="9 14" -from typing import Union +如果你的程式碼使用 `async` / `await`,請使用 `async def`: +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -187,28 +193,41 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` **注意**: -如果你不知道是否會用到,可以查看 _"In a hurry?"_ 章節中,關於 `async` 和 `await` 的部分。 +如果你不確定,請查看文件中 _"In a hurry?"_ 章節的 `async` 與 `await`
-### 運行 +### 運行 { #run-it } 使用以下指令運行伺服器:
```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. ``` @@ -216,21 +235,21 @@ INFO: Application startup complete.
-關於指令 uvicorn main:app --reload... +關於指令 fastapi dev main.py... -該指令 `uvicorn main:app` 指的是: +指令 `fastapi dev` 會讀取你的 `main.py`,偵測其中的 **FastAPI** 應用,並使用 Uvicorn 啟動伺服器。 -- `main`:`main.py` 檔案(一個 python 的 "模組")。 -- `app`:在 `main.py` 檔案中,使用 `app = FastAPI()` 建立的物件。 -- `--reload`:程式碼更改後會自動重新啟動,請僅在開發時使用此參數。 +預設情況下,`fastapi dev` 會在本機開發時啟用自動重新載入。 + +可在 FastAPI CLI 文件中閱讀更多資訊。
-### 檢查 +### 檢查 { #check-it } 使用瀏覽器開啟 http://127.0.0.1:8000/items/5?q=somequery。 -你將會看到以下的 JSON 回應: +你將會看到以下 JSON 回應: ```JSON {"item_id": 5, "q": "somequery"} @@ -238,36 +257,34 @@ INFO: Application startup complete. 你已經建立了一個具有以下功能的 API: -- 透過路徑 `/` 和 `/items/{item_id}` 接受 HTTP 請求。 -- 以上路經都接受 `GET` 請求(也被稱為 HTTP _方法_)。 -- 路徑 `/items/{item_id}` 有一個 `int` 型別的 `item_id` 參數。 -- 路徑 `/items/{item_id}` 有一個 `str` 型別的查詢參數 `q`。 +* 透過路徑 `/` 和 `/items/{item_id}` 接受 HTTP 請求。 +* 以上兩個路徑都接受 `GET` 操作(也被稱為 HTTP _方法_)。 +* 路徑 `/items/{item_id}` 有一個 `int` 型別的路徑參數 `item_id`。 +* 路徑 `/items/{item_id}` 有一個可選的 `str` 查詢參數 `q`。 -### 互動式 API 文件 +### 互動式 API 文件 { #interactive-api-docs } -使用瀏覽器開啟 http://127.0.0.1:8000/docs。 +接著前往 http://127.0.0.1:8000/docs。 -你會看到自動生成的互動式 API 文件(由 Swagger UI 生成): +你會看到自動生成的互動式 API 文件(由 Swagger UI 提供): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### ReDoc API 文件 +### 替代 API 文件 { #alternative-api-docs } -使用瀏覽器開啟 http://127.0.0.1:8000/redoc。 +現在前往 http://127.0.0.1:8000/redoc。 -你將看到 ReDoc 文件 (由 ReDoc 生成): +你會看到另一種自動文件(由 ReDoc 提供): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## 範例升級 +## 範例升級 { #example-upgrade } -現在繼續修改 `main.py` 檔案,來接收一個帶有 body 的 `PUT` 請求。 +現在修改 `main.py` 檔案,使其能從 `PUT` 請求接收 body。 -我們使用 Pydantic 來使用標準的 Python 型別聲明請求。 - -```Python hl_lines="4 9-12 25-27" -from typing import Union +多虧了 Pydantic,你可以用標準的 Python 型別來宣告 body。 +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -277,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -286,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -295,43 +312,43 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -伺服器將自動重新載入(因為在上一步中,你向 `uvicorn` 指令添加了 `--reload` 的選項)。 +`fastapi dev` 伺服器應會自動重新載入。 -### 互動式 API 文件升級 +### 互動式 API 文件升級 { #interactive-api-docs-upgrade } -使用瀏覽器開啟 http://127.0.0.1:8000/docs。 +前往 http://127.0.0.1:8000/docs。 -- 互動式 API 文件會自動更新,並加入新的 body 請求: +* 互動式 API 文件會自動更新,包含新的 body: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -- 點擊 "Try it out" 按鈕, 你可以填寫參數並直接與 API 互動: +* 點擊「Try it out」按鈕,你可以填寫參數並直接與 API 互動: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -- 然後點擊 "Execute" 按鈕,使用者介面將會向 API 發送請求,並將結果顯示在螢幕上: +* 然後點擊「Execute」按鈕,使用者介面會與你的 API 溝通、送出參數、取得結果並顯示在螢幕上: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### ReDoc API 文件升級 +### 替代 API 文件升級 { #alternative-api-docs-upgrade } -使用瀏覽器開啟 http://127.0.0.1:8000/redoc。 +現在前往 http://127.0.0.1:8000/redoc。 -- ReDoc API 文件會自動更新,並加入新的參數和 body 請求: +* 替代文件也會反映新的查詢參數與 body: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### 總結 +### 總結 { #recap } -總結來說, 你就像宣告函式的參數型別一樣,只宣告了一次請求參數和請求主體參數等型別。 +總結來說,你只需在函式參數中**一次**宣告參數、body 等的型別。 -你使用 Python 標準型別來完成聲明。 +你使用的是現代標準的 Python 型別。 -你不需要學習新的語法、類別、方法或函式庫等等。 +你不需要學新的語法、特定函式庫的方法或類別,等等。 -只需要使用 **Python 以上的版本**。 +就用標準的 **Python**。 -舉個範例,比如宣告 int 的型別: +例如,對於一個 `int`: ```Python item_id: int @@ -343,126 +360,200 @@ item_id: int item: Item ``` -在進行一次宣告後,你將獲得: +…透過一次宣告,你將獲得: -- 編輯器支援: - - 自動補全 - - 型別檢查 -- 資料驗證: - - 驗證失敗時自動生成清楚的錯誤訊息 - - 可驗證多層巢狀的 JSON 物件 -- 轉換輸入的資料: 轉換來自網路請求到 Python 資料型別。包含以下數據: - - JSON - - 路徑參數 - - 查詢參數 - - Cookies - - 請求標頭 - - 表單 - - 文件 -- 轉換輸出的資料: 轉換 Python 資料型別到網路傳輸的 JSON: - - 轉換 Python 型別 (`str`、 `int`、 `float`、 `bool`、 `list` 等) - - `datetime` 物件 - - `UUID` 物件 - - 數據模型 - - ...還有其他更多 -- 自動生成的 API 文件,包含 2 種不同的使用介面: - - Swagger UI - - ReDoc +* 編輯器支援,包括: + * 自動補全。 + * 型別檢查。 +* 資料驗證: + * 當資料無效時,自動且清楚的錯誤。 + * 即使是深度巢狀的 JSON 物件也能驗證。 +* 輸入資料的 轉換:從網路讀入到 Python 資料與型別。包含: + * JSON。 + * 路徑參數。 + * 查詢參數。 + * Cookies。 + * 標頭。 + * 表單。 + * 檔案。 +* 輸出資料的 轉換:從 Python 資料與型別轉換為網路資料(JSON): + * 轉換 Python 型別(`str`、`int`、`float`、`bool`、`list` 等)。 + * `datetime` 物件。 + * `UUID` 物件。 + * 資料庫模型。 + * ...還有更多。 +* 自動生成的互動式 API 文件,包含 2 種替代的使用者介面: + * Swagger UI。 + * ReDoc。 --- -回到前面的的程式碼範例,**FastAPI** 還會: +回到前面的程式碼範例,**FastAPI** 會: -- 驗證 `GET` 和 `PUT` 請求路徑中是否包含 `item_id`。 -- 驗證 `GET` 和 `PUT` 請求中的 `item_id` 是否是 `int` 型別。 - - 如果驗證失敗,將會返回清楚有用的錯誤訊息。 -- 查看 `GET` 請求中是否有命名為 `q` 的查詢參數 (例如 `http://127.0.0.1:8000/items/foo?q=somequery`)。 - - 因為 `q` 參數被宣告為 `= None`,所以是選填的。 - - 如果沒有宣告 `None`,則此參數將會是必填 (例如 `PUT` 範例的請求 body)。 -- 對於 `PUT` 的請求 `/items/{item_id}`,將會讀取 body 為 JSON: - - 驗證是否有必填屬性 `name` 且型別是 `str`。 - - 驗證是否有必填屬性 `price` 且型別是 `float`。 - - 驗證是否有選填屬性 `is_offer` 且型別是 `bool`。 - - 以上驗證都適用於多層次巢狀 JSON 物件。 -- 自動轉換 JSON 格式。 -- 透過 OpenAPI 文件來記錄所有內容,可以被用於: - - 互動式文件系統。 - - 自動為多種程式語言生成用戶端的程式碼。 -- 提供兩種交互式文件介面。 +* 驗證 `GET` 與 `PUT` 請求的路徑中是否包含 `item_id`。 +* 驗證 `GET` 與 `PUT` 請求中的 `item_id` 是否為 `int` 型別。 + * 如果不是,客戶端會看到清楚有用的錯誤。 +* 在 `GET` 請求中檢查是否有名為 `q` 的可選查詢參數(如 `http://127.0.0.1:8000/items/foo?q=somequery`)。 + * 因為 `q` 參數被宣告為 `= None`,所以它是可選的。 + * 若沒有 `None`,則它會是必填(就像 `PUT` 時的 body)。 +* 對於 `/items/{item_id}` 的 `PUT` 請求,以 JSON 讀取 body: + * 檢查是否有必填屬性 `name`,且為 `str`。 + * 檢查是否有必填屬性 `price`,且為 `float`。 + * 檢查是否有可選屬性 `is_offer`,若存在則應為 `bool`。 + * 以上也適用於深度巢狀的 JSON 物件。 +* 自動在 JSON 與 Python 之間轉換。 +* 以 OpenAPI 記錄所有內容,可用於: + * 互動式文件系統。 + * 為多種語言自動產生用戶端程式碼的系統。 +* 直接提供兩種互動式文件網頁介面。 --- -雖然我們只敘述了表面的功能,但其實你已經理解了它是如何執行。 +我們只觸及了表面,但你已經了解它的運作方式了。 -試著修改以下程式碼: +試著把這一行: ```Python return {"item_name": item.name, "item_id": item_id} ``` -從: +…從: ```Python ... "item_name": item.name ... ``` -修改為: +…改為: ```Python ... "item_price": item.price ... ``` -然後觀察你的編輯器,會自動補全並且還知道他們的型別: +…然後看看你的編輯器如何自動補全屬性並知道它們的型別: ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -有關更多功能的完整範例,可以參考 教學 - 使用者指南。 +若想看包含更多功能的完整範例,請參考 教學 - 使用者指南。 -**劇透警告**: 教學 - 使用者指南內容有: +**劇透警告**:教學 - 使用者指南包含: -- 對來自不同地方的**參數**進行宣告:像是 **headers**, **cookies**, **form 表單**以及**上傳檔案**。 -- 如何設定 **驗證限制** 像是 `maximum_length` or `regex`。 -- 簡單且非常容易使用的 **依賴注入** 系統。 -- 安全性和身份驗證,包含提供支援 **OAuth2**、**JWT tokens** 和 **HTTP Basic** 驗證。 -- 更進階 (但同樣簡單) 的宣告 **多層次的巢狀 JSON 格式** (感謝 Pydantic)。 -- **GraphQL** 與 Strawberry 以及其他的相關函式庫進行整合。 -- 更多其他的功能 (感謝 Starlette) 像是: - - **WebSockets** - - 於 HTTPX 和 `pytest` 的非常簡單測試 - - **CORS** - - **Cookie Sessions** - - ...以及更多 +* 來自不同來源的**參數**宣告:例如 **headers**、**cookies**、**form fields** 和 **files**。 +* 如何設定**驗證限制**,如 `maximum_length` 或 `regex`。 +* 一個非常強大且易用的 **依賴注入** 系統。 +* 安全與驗證,包含支援 **OAuth2** 搭配 **JWT tokens** 與 **HTTP Basic** 驗證。 +* 宣告**深度巢狀 JSON 模型**的進階(但同樣簡單)技巧(感謝 Pydantic)。 +* 與 Strawberry 及其他函式庫的 **GraphQL** 整合。 +* 許多額外功能(感謝 Starlette),例如: + * **WebSockets** + * 基於 HTTPX 與 `pytest` 的極其簡單的測試 + * **CORS** + * **Cookie Sessions** + * ...以及更多。 -## 效能 +### 部署你的應用(可選) { #deploy-your-app-optional } -來自獨立機構 TechEmpower 的測試結果,顯示在 Uvicorn 執行下的 **FastAPI** 是 最快的 Python 框架之一, 僅次於 Starlette 和 Uvicorn 本身 (兩者是 FastAPI 的底層)。 (\*) +你也可以選擇將 FastAPI 應用部署到 FastAPI Cloud,如果你還沒加入,去登記等候名單吧。🚀 -想了解更多訊息,可以參考 測試結果。 +如果你已經有 **FastAPI Cloud** 帳號(我們已從等候名單邀請你 😉),你可以用一個指令部署你的應用。 -## 可選的依賴套件 +部署前,先確認你已登入: -用於 Pydantic: +
-- email-validator - 用於電子郵件驗證。 -- pydantic-settings - 用於設定管理。 -- pydantic-extra-types - 用於與 Pydantic 一起使用的額外型別。 +```console +$ fastapi login -用於 Starlette: +You are logged in to FastAPI Cloud 🚀 +``` -- httpx - 使用 `TestClient`時必須安裝。 -- jinja2 - 使用預設的模板配置時必須安裝。 -- python-multipart - 需要使用 `request.form()` 對表單進行 "解析" 時安裝。 -- itsdangerous - 需要使用 `SessionMiddleware` 支援時安裝。 -- pyyaml - 用於支援 Starlette 的 `SchemaGenerator` (如果你使用 FastAPI,可能不需要它)。 +
-用於 FastAPI / Starlette: +接著部署你的應用: -- uvicorn - 用於加載和運行應用程式的服務器。 -- orjson - 使用 `ORJSONResponse`時必須安裝。 -- ujson - 使用 `UJSONResponse` 時必須安裝。 +
-你可以使用 `pip install "fastapi[all]"` 來安裝這些所有依賴套件。 +```console +$ fastapi deploy -## 授權 +Deploying to FastAPI Cloud... -該項目遵循 MIT 許可協議。 +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +就這樣!現在你可以在該 URL 造訪你的應用。✨ + +#### 關於 FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** 由 **FastAPI** 的同一位作者與團隊打造。 + +它讓你以最小的努力精簡地完成 API 的**建置**、**部署**與**存取**流程。 + +它把用 FastAPI 開發應用的**開發者體驗**帶到**部署**到雲端的流程中。🎉 + +FastAPI Cloud 是「FastAPI 與好朋友們」這些開源專案的主要贊助與資金來源。✨ + +#### 部署到其他雲端供應商 { #deploy-to-other-cloud-providers } + +FastAPI 是開源且基於標準。你可以把 FastAPI 應用部署到任何你選擇的雲端供應商。 + +依照你雲端供應商的指南來部署 FastAPI 應用吧。🤓 + +## 效能 { #performance } + +獨立的 TechEmpower 基準測試顯示,在 Uvicorn 下運行的 **FastAPI** 應用是最快的 Python 框架之一,僅次於 Starlette 與 Uvicorn 本身(FastAPI 內部使用它們)。(*) + +想了解更多,請參閱測試結果。 + +## 依賴套件 { #dependencies } + +FastAPI 依賴 Pydantic 與 Starlette。 + +### `standard` 依賴套件 { #standard-dependencies } + +當你以 `pip install "fastapi[standard]"` 安裝 FastAPI 時,會包含 `standard` 這組可選依賴套件: + +Pydantic 會使用: + +* email-validator - 用於電子郵件驗證。 + +Starlette 會使用: + +* httpx - 若要使用 `TestClient` 必須安裝。 +* jinja2 - 若要使用預設的模板設定必須安裝。 +* python-multipart - 若要支援表單 "解析",搭配 `request.form()`。 + +FastAPI 會使用: + +* uvicorn - 用於載入並服務你的應用的伺服器。這包含 `uvicorn[standard]`,其中含有一些高效能服務所需的依賴(例如 `uvloop`)。 +* `fastapi-cli[standard]` - 提供 `fastapi` 指令。 + * 其中包含 `fastapi-cloud-cli`,可讓你將 FastAPI 應用部署到 FastAPI Cloud。 + +### 不含 `standard` 依賴套件 { #without-standard-dependencies } + +如果你不想包含 `standard` 可選依賴,可以改用 `pip install fastapi`(而不是 `pip install "fastapi[standard]"`)。 + +### 不含 `fastapi-cloud-cli` { #without-fastapi-cloud-cli } + +如果你想安裝帶有 standard 依賴、但不包含 `fastapi-cloud-cli`,可以使用 `pip install "fastapi[standard-no-fastapi-cloud-cli]"`。 + +### 額外可選依賴套件 { #additional-optional-dependencies } + +有些額外依賴你可能也會想安裝。 + +Pydantic 的額外可選依賴: + +* pydantic-settings - 設定管理。 +* pydantic-extra-types - 與 Pydantic 一起使用的額外型別。 + +FastAPI 的額外可選依賴: + +* orjson - 若要使用 `ORJSONResponse` 必須安裝。 +* ujson - 若要使用 `UJSONResponse` 必須安裝。 + +## 授權 { #license } + +本專案以 MIT 授權條款釋出。 diff --git a/docs/zh-hant/docs/learn/index.md b/docs/zh-hant/docs/learn/index.md index eb7d7096a..43e4519a5 100644 --- a/docs/zh-hant/docs/learn/index.md +++ b/docs/zh-hant/docs/learn/index.md @@ -1,5 +1,5 @@ -# 學習 +# 學習 { #learn } -以下是學習 FastAPI 的入門介紹和教學。 +以下是學習 **FastAPI** 的入門介紹和教學。 你可以將其視為一本**書籍**或一門**課程**,這是**官方**認可並推薦的 FastAPI 學習方式。 😎 diff --git a/docs/zh-hant/docs/resources/index.md b/docs/zh-hant/docs/resources/index.md index f4c70a3a0..ea77cb728 100644 --- a/docs/zh-hant/docs/resources/index.md +++ b/docs/zh-hant/docs/resources/index.md @@ -1,3 +1,3 @@ -# 資源 +# 資源 { #resources } -額外的資源、外部連結、文章等。 ✈️ +額外的資源、外部連結等。 ✈️ diff --git a/docs/zh-hant/docs/tutorial/first-steps.md b/docs/zh-hant/docs/tutorial/first-steps.md index d6684bc51..6bcb453bf 100644 --- a/docs/zh-hant/docs/tutorial/first-steps.md +++ b/docs/zh-hant/docs/tutorial/first-steps.md @@ -1,8 +1,8 @@ -# 第一步 +# 第一步 { #first-steps } 最簡單的 FastAPI 檔案可能看起來像這樣: -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} 將其複製到一個名為 `main.py` 的文件中。 @@ -11,47 +11,39 @@
```console -$ fastapi dev main.py -INFO Using path main.py -INFO Resolved absolute path /home/user/code/awesomeapp/main.py -INFO Searching for package file structure from directories with __init__.py files -INFO Importing from /home/user/code/awesomeapp +$ fastapi dev main.py - ╭─ Python module file ─╮ - │ │ - │ 🐍 main.py │ - │ │ - ╰──────────────────────╯ + FastAPI Starting development server 🚀 -INFO Importing module main -INFO Found importable FastAPI app + Searching for package file structure from directories + with __init__.py files + Importing from /home/user/code/awesomeapp - ╭─ Importable FastAPI app ─╮ - │ │ - │ from main import app │ - │ │ - ╰──────────────────────────╯ + module 🐍 main.py -INFO Using import string main:app + code Importing the FastAPI app object from the module with + the following code: - ╭────────── FastAPI CLI - Development mode ───────────╮ - │ │ - │ Serving at: http://127.0.0.1:8000 │ - │ │ - │ API docs: http://127.0.0.1:8000/docs │ - │ │ - │ Running in development mode, for production use: │ - │ │ - fastapi run - │ │ - ╰─────────────────────────────────────────────────────╯ + from main import app -INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [2265862] using WatchFiles -INFO: Started server process [2265873] -INFO: Waiting for application startup. -INFO: Application startup complete. + app Using import string: main:app + + server Server started at http://127.0.0.1:8000 + server Documentation at http://127.0.0.1:8000/docs + + tip Running in development mode, for production use: + fastapi run + + Logs: + + INFO Will watch for changes in these directories: + ['/home/user/code/awesomeapp'] + INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C + to quit) + INFO Started reloader process [383138] using WatchFiles + INFO Started server process [383153] + INFO Waiting for application startup. + INFO Application startup complete. ```
@@ -64,7 +56,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) 那列顯示了你的應用程式正在本地端機器上運行的 URL。 -### 查看它 +### 查看它 { #check-it } 在瀏覽器中打開 http://127.0.0.1:8000. @@ -74,7 +66,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) {"message": "Hello World"} ``` -### 互動式 API 文件 +### 互動式 API 文件 { #interactive-api-docs } 現在,前往 http://127.0.0.1:8000/docs. @@ -82,7 +74,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### 替代 API 文件 +### 替代 API 文件 { #alternative-api-docs } 現在,前往 http://127.0.0.1:8000/redoc. @@ -90,33 +82,33 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -### OpenAPI +### OpenAPI { #openapi } -**FastAPI** 使用定義 API 的 **OpenAPI** 標準來生成一個 「schema」 與你的所有 API。 +**FastAPI** 使用定義 API 的 **OpenAPI** 標準來生成一個「schema」,涵蓋你的全部 API。 -#### 「Schema」 +#### 「Schema」 { #schema } 「schema」是對某個事物的定義或描述。它並不是實作它的程式碼,而僅僅是一個抽象的描述。 -#### API 「schema」 +#### API 「schema」 { #api-schema } 在這種情況下,OpenAPI 是一個規範,它規定了如何定義 API 的 schema。 這個 schema 定義包含了你的 API 路徑、可能接收的參數等內容。 -#### 資料 「schema」 +#### 資料「schema」 { #data-schema } 「schema」這個術語也可能指某些資料的結構,比如 JSON 內容的結構。 在這種情況下,它指的是 JSON 的屬性、資料型別等。 -#### OpenAPI 和 JSON Schema +#### OpenAPI 和 JSON Schema { #openapi-and-json-schema } -OpenAPI 定義了 API 的 schema。這個 schema 包含了使用 **JSON Schema** 定義的資料,這是 JSON 資料 schema 的標準。 +OpenAPI 為你的 API 定義了 API 的 schema。而該 schema 會包含你的 API 所傳送與接收資料的定義(或稱「schemas」),使用 **JSON Schema**,這是 JSON 資料 schema 的標準。 -#### 檢查 `openapi.json` +#### 檢查 `openapi.json` { #check-the-openapi-json } -如果你好奇原始的 OpenAPI schema 長什麼樣子,FastAPI 會自動生成一個包含所有 API 描述的 JSON (schema)。 +如果你好奇原始的 OpenAPI schema 長什麼樣子,FastAPI 會自動生成一個包含所有 API 描述的 JSON(schema)。 你可以直接在 http://127.0.0.1:8000/openapi.json 查看它。 @@ -143,23 +135,59 @@ OpenAPI 定義了 API 的 schema。這個 schema 包含了使用 **JSON Schema** ... ``` -#### OpenAPI 的用途 +#### OpenAPI 的用途 { #what-is-openapi-for } OpenAPI schema 驅動了兩個互動式文件系統。 而且有許多替代方案,所有這些都是基於 OpenAPI。你可以輕鬆地將任何這些替代方案添加到使用 **FastAPI** 建置的應用程式中。 -你也可以用它自動生成程式碼,讓前端、手機應用程式或物聯網設備等與你的 API 進行通訊。 +你也可以用它自動生成程式碼,讓用戶端與你的 API 通訊。例如前端、手機或物聯網(IoT)應用程式。 -## 逐步回顧 +### 部署你的應用程式(可選) { #deploy-your-app-optional } -### 第一步:引入 `FastAPI` +你可以選擇將你的 FastAPI 應用程式部署到 FastAPI Cloud,如果還沒有,去加入候補名單吧。🚀 -{* ../../docs_src/first_steps/tutorial001.py h1[1] *} +如果你已經有 **FastAPI Cloud** 帳號(我們已從候補名單邀請你 😉),你可以用一個指令部署你的應用程式。 + +部署之前,先確保你已登入: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +接著部署你的應用程式: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +就這樣!現在你可以透過該 URL 存取你的應用程式了。✨ + +## 逐步回顧 { #recap-step-by-step } + +### 第一步:引入 `FastAPI` { #step-1-import-fastapi } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} `FastAPI` 是一個 Python 類別,提供所有 API 的全部功能。 -/// note | Technical Details +/// note | 技術細節 `FastAPI` 是一個直接繼承自 `Starlette` 的類別。 @@ -167,17 +195,17 @@ OpenAPI schema 驅動了兩個互動式文件系統。 /// -### 第二步:建立一個 `FastAPI` 「實例」 +### 第二步:建立一個 `FastAPI`「實例」 { #step-2-create-a-fastapi-instance } -{* ../../docs_src/first_steps/tutorial001.py h1[3] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[3] *} 這裡的 `app` 變數將會是 `FastAPI` 類別的「實例」。 這將是你建立所有 API 的主要互動點。 -### 第三步:建立一個 *路徑操作* +### 第三步:建立一個「路徑操作」 { #step-3-create-a-path-operation } -#### 路徑 +#### 路徑 { #path } 這裡的「路徑」指的是 URL 中自第一個 `/` 以後的部分。 @@ -201,7 +229,7 @@ https://example.com/items/foo 在建置 API 時,「路徑」是分離「關注點」和「資源」的主要方式。 -#### 操作 +#### 操作 { #operation } 這裡的「操作」指的是 HTTP 的「方法」之一。 @@ -236,16 +264,16 @@ https://example.com/items/foo 我們將會稱它們為「**操作**」。 -#### 定義一個 *路徑操作裝飾器* +#### 定義一個「路徑操作裝飾器」 { #define-a-path-operation-decorator } -{* ../../docs_src/first_steps/tutorial001.py h1[6] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *} `@app.get("/")` 告訴 **FastAPI** 那個函式負責處理請求: * 路徑 `/` * 使用 get操作 -/// info | `@decorator` Info +/// info | `@decorator` 說明 Python 中的 `@something` 語法被稱為「裝飾器」。 @@ -253,7 +281,7 @@ Python 中的 `@something` 語法被稱為「裝飾器」。 一個「裝飾器」會對下面的函式做一些事情。 -在這種情況下,這個裝飾器告訴 **FastAPI** 那個函式對應於 **路徑** `/` 和 **操作** `get`. +在這種情況下,這個裝飾器告訴 **FastAPI** 那個函式對應於 **路徑** `/` 和 **操作** `get`。 這就是「**路徑操作裝飾器**」。 @@ -284,27 +312,27 @@ Python 中的 `@something` 語法被稱為「裝飾器」。 /// -### 第四步:定義 **路徑操作函式** +### 第四步:定義「路徑操作函式」 { #step-4-define-the-path-operation-function } 這是我們的「**路徑操作函式**」: -* **path**: 是 `/`. -* **operation**: 是 `get`. -* **function**: 是裝飾器下面的函式(在 `@app.get("/")` 下面)。 +* **path**:是 `/`。 +* **operation**:是 `get`。 +* **function**:是裝飾器下面的函式(在 `@app.get("/")` 下面)。 -{* ../../docs_src/first_steps/tutorial001.py h1[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} 這就是一個 Python 函式。 -它將會在 **FastAPI** 收到一個請求時被呼叫,使用 `GET` 操作。 +它將會在 **FastAPI** 收到一個使用 `GET` 操作、網址為「`/`」的請求時被呼叫。 在這種情況下,它是一個 `async` 函式。 --- -你可以將它定義為一個正常的函式,而不是 `async def`: +你也可以將它定義為一般函式,而不是 `async def`: -{* ../../docs_src/first_steps/tutorial003.py h1[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note @@ -312,9 +340,9 @@ Python 中的 `@something` 語法被稱為「裝飾器」。 /// -### 第五步:回傳內容 +### 第五步:回傳內容 { #step-5-return-the-content } -{* ../../docs_src/first_steps/tutorial001.py h1[8] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *} 你可以返回一個 `dict`、`list`、單個值作為 `str`、`int` 等。 @@ -322,10 +350,31 @@ Python 中的 `@something` 語法被稱為「裝飾器」。 有很多其他物件和模型會自動轉換為 JSON(包括 ORMs,等等)。試用你最喜歡的,很有可能它們已經有支援。 -## 回顧 +### 第六步:部署 { #step-6-deploy-it } -* 引入 `FastAPI`. +用一行指令將你的應用程式部署到 **FastAPI Cloud**:`fastapi deploy`。🎉 + +#### 關於 FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** 由 **FastAPI** 的作者與團隊打造。 + +它讓你以最小的成本完成 API 的**建置**、**部署**與**存取**流程。 + +它把用 FastAPI 開發應用的同樣**開發者體驗**帶到將應用**部署**到雲端的流程中。🎉 + +FastAPI Cloud 也是「FastAPI 與其好友」這些開源專案的主要贊助與資金提供者。✨ + +#### 部署到其他雲端供應商 { #deploy-to-other-cloud-providers } + +FastAPI 是開源並基於標準的。你可以把 FastAPI 應用部署到你選擇的任何雲端供應商。 + +依照你的雲端供應商的指南部署 FastAPI 應用吧。🤓 + +## 回顧 { #recap } + +* 引入 `FastAPI`。 * 建立一個 `app` 實例。 -* 寫一個 **路徑操作裝飾器** 使用裝飾器像 `@app.get("/")`。 -* 定義一個 **路徑操作函式**;例如,`def root(): ...`。 +* 寫一個「路徑操作裝飾器」,像是 `@app.get("/")`。 +* 定義一個「路徑操作函式」;例如,`def root(): ...`。 * 使用命令 `fastapi dev` 執行開發伺服器。 +* 可選:使用 `fastapi deploy` 部署你的應用程式。 diff --git a/docs/zh-hant/docs/tutorial/index.md b/docs/zh-hant/docs/tutorial/index.md index ae0056f52..5e1961fd7 100644 --- a/docs/zh-hant/docs/tutorial/index.md +++ b/docs/zh-hant/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# 教學 - 使用者指南 +# 教學 - 使用者指南 { #tutorial-user-guide } 本教學將一步一步展示如何使用 **FastAPI** 及其大多數功能。 @@ -6,7 +6,7 @@ 它也被設計成可作為未來的參考,讓你隨時回來查看所需的內容。 -## 運行程式碼 +## 運行程式碼 { #run-the-code } 所有程式碼區塊都可以直接複製和使用(它們實際上是經過測試的 Python 檔案)。 @@ -15,48 +15,39 @@
```console -$ fastapi dev main.py -INFO Using path main.py -INFO Resolved absolute path /home/user/code/awesomeapp/main.py -INFO Searching for package file structure from directories with __init__.py files -INFO Importing from /home/user/code/awesomeapp +$ fastapi dev main.py - ╭─ Python module file ─╮ - │ │ - │ 🐍 main.py │ - │ │ - ╰──────────────────────╯ + FastAPI Starting development server 🚀 -INFO Importing module main -INFO Found importable FastAPI app + Searching for package file structure from directories + with __init__.py files + Importing from /home/user/code/awesomeapp - ╭─ Importable FastAPI app ─╮ - │ │ - │ from main import app │ - │ │ - ╰──────────────────────────╯ + module 🐍 main.py -INFO Using import string main:app + code Importing the FastAPI app object from the module with + the following code: - ╭────────── FastAPI CLI - Development mode ───────────╮ - │ │ - │ Serving at: http://127.0.0.1:8000 │ - │ │ - │ API docs: http://127.0.0.1:8000/docs │ - │ │ - │ Running in development mode, for production use: │ - │ │ - fastapi run - │ │ - ╰─────────────────────────────────────────────────────╯ + from main import app -INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [2265862] using WatchFiles -INFO: Started server process [2265873] -INFO: Waiting for application startup. -INFO: Application startup complete. - + app Using import string: main:app + + server Server started at http://127.0.0.1:8000 + server Documentation at http://127.0.0.1:8000/docs + + tip Running in development mode, for production use: + fastapi run + + Logs: + + INFO Will watch for changes in these directories: + ['/home/user/code/awesomeapp'] + INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C + to quit) + INFO Started reloader process [383138] using WatchFiles + INFO Started server process [383153] + INFO Waiting for application startup. + INFO Application startup complete. ```
@@ -67,7 +58,7 @@ $ fastapi dev -/// note +/// note | 注意 -當你使用 `pip install "fastapi[standard]"` 安裝時,會包含一些預設的可選標準依賴項。 +當你使用 `pip install "fastapi[standard]"` 安裝時,會包含一些預設的可選標準依賴項,其中包括 `fastapi-cloud-cli`,它可以讓你部署到 FastAPI Cloud。 -如果你不想包含那些可選的依賴項,你可以使用 `pip install fastapi` 來安裝。 +如果你不想包含那些可選的依賴項,你可以改為安裝 `pip install fastapi`。 + +如果你想安裝標準依賴項,但不包含 `fastapi-cloud-cli`,可以使用 `pip install "fastapi[standard-no-fastapi-cloud-cli]"` 安裝。 /// -## 進階使用者指南 +## 進階使用者指南 { #advanced-user-guide } -還有一個**進階使用者指南**你可以稍後閱讀。 +還有一個**進階使用者指南**你可以在讀完這個**教學 - 使用者指南**後再閱讀。 **進階使用者指南**建立在這個教學之上,使用相同的概念,並教你一些額外的功能。 diff --git a/docs/zh-hant/docs/tutorial/query-param-models.md b/docs/zh-hant/docs/tutorial/query-param-models.md index 76ee74016..e36028199 100644 --- a/docs/zh-hant/docs/tutorial/query-param-models.md +++ b/docs/zh-hant/docs/tutorial/query-param-models.md @@ -1,4 +1,4 @@ -# 查詢參數模型 +# 查詢參數模型 { #query-parameter-models } 如果你有一組具有相關性的**查詢參數**,你可以建立一個 **Pydantic 模型**來聲明它們。 @@ -10,7 +10,7 @@ FastAPI 從 `0.115.0` 版本開始支援這個特性。🤓 /// -## 使用 Pydantic 模型的查詢參數 +## 使用 Pydantic 模型的查詢參數 { #query-parameters-with-a-pydantic-model } 在一個 **Pydantic 模型**中聲明你需要的**查詢參數**,然後將參數聲明為 `Query`: @@ -18,7 +18,7 @@ FastAPI 從 `0.115.0` 版本開始支援這個特性。🤓 **FastAPI** 將會從請求的**查詢參數**中**提取**出**每個欄位**的資料,並將其提供給你定義的 Pydantic 模型。 -## 查看文件 +## 查看文件 { #check-the-docs } 你可以在 `/docs` 頁面的 UI 中查看查詢參數: @@ -26,7 +26,7 @@ FastAPI 從 `0.115.0` 版本開始支援這個特性。🤓 -## 禁止額外的查詢參數 +## 禁止額外的查詢參數 { #forbid-extra-query-parameters } 在一些特殊的使用場景中(可能不是很常見),你可能希望**限制**你要收到的查詢參數。 @@ -57,7 +57,7 @@ https://example.com/items/?limit=10&tool=plumbus } ``` -## 總結 +## 總結 { #summary } 你可以使用 **Pydantic 模型**在 **FastAPI** 中聲明**查詢參數**。😎 diff --git a/docs/zh-hant/docs/virtual-environments.md b/docs/zh-hant/docs/virtual-environments.md index d8e31e08e..acd1d914e 100644 --- a/docs/zh-hant/docs/virtual-environments.md +++ b/docs/zh-hant/docs/virtual-environments.md @@ -1,4 +1,4 @@ -# 虛擬環境 +# 虛擬環境 { #virtual-environments } 當你在 Python 專案中工作時,你可能會需要使用一個**虛擬環境**(或類似的機制)來隔離你為每個專案安裝的套件。 @@ -26,7 +26,7 @@ /// -## 建立一個專案 +## 建立一個專案 { #create-a-project } 首先,為你的專案建立一個目錄。 @@ -51,7 +51,7 @@ $ cd awesome-project -## 建立一個虛擬環境 +## 建立一個虛擬環境 { #create-a-virtual-environment } 在開始一個 Python 專案的**第一時間**,**在你的專案內部**建立一個虛擬環境。 @@ -114,7 +114,7 @@ $ uv venv /// -## 啟動虛擬環境 +## 啟動虛擬環境 { #activate-the-virtual-environment } 啟動新的虛擬環境來確保你運行的任何 Python 指令或安裝的套件都能使用到它。 @@ -166,11 +166,11 @@ $ source .venv/Scripts/activate 每次你在這個環境中安裝一個**新的套件**時,都需要**重新啟動**這個環境。 -這麼做確保了當你使用一個由這個套件安裝的**終端(CLI)程式**時,你使用的是你的虛擬環境中的程式,而不是全域安裝、可能版本不同的程式。 +這麼做確保了當你使用一個由這個套件安裝的**終端(CLI)程式**時,你使用的是你的虛擬環境中的程式,而不是全域安裝、可能版本不同的程式。 /// -## 檢查虛擬環境是否啟動 +## 檢查虛擬環境是否啟動 { #check-the-virtual-environment-is-active } 檢查虛擬環境是否啟動(前面的指令是否生效)。 @@ -212,7 +212,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python //// -## 升級 `pip` +## 升級 `pip` { #upgrade-pip } /// tip @@ -242,7 +242,27 @@ $ python -m pip install --upgrade pip -## 加入 `.gitignore` +/// tip | 注意 + +有時你在嘗試升級 pip 時,可能會遇到 **`No module named pip`** 的錯誤。 + +如果發生這種情況,請用下面的指令安裝並升級 pip: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +此指令會在未安裝 pip 時為你安裝它,並確保安裝的 pip 版本至少與 `ensurepip` 所提供的版本一樣新。 + +/// + +## 加入 `.gitignore` { #add-gitignore } 如果你使用 **Git**(這是你應該使用的),加入一個 `.gitignore` 檔案來排除你的 `.venv` 中的所有內容。 @@ -282,7 +302,7 @@ $ echo "*" > .venv/.gitignore /// -## 安裝套件 +## 安裝套件 { #install-packages } 在啟用虛擬環境後,你可以在其中安裝套件。 @@ -294,7 +314,7 @@ $ echo "*" > .venv/.gitignore /// -### 直接安裝套件 +### 直接安裝套件 { #install-packages-directly } 如果你急於安裝,不想使用檔案來聲明專案的套件依賴,你可以直接安裝它們。 @@ -333,7 +353,7 @@ $ uv pip install "fastapi[standard]" //// -### 從 `requirements.txt` 安裝 +### 從 `requirements.txt` 安裝 { #install-from-requirements-txt } 如果你有一個 `requirements.txt` 檔案,你可以使用它來安裝其中的套件。 @@ -376,7 +396,7 @@ pydantic==2.8.0 /// -## 執行程式 +## 執行程式 { #run-your-program } 在啟用虛擬環境後,你可以執行你的程式,它將使用虛擬環境中的 Python 和你在其中安裝的套件。 @@ -390,7 +410,7 @@ Hello World -## 設定編輯器 +## 設定編輯器 { #configure-your-editor } 你可能會用到編輯器,請確保設定它使用你建立的相同虛擬環境(它可能會自動偵測到),以便你可以獲得自動完成和内嵌錯誤提示。 @@ -405,7 +425,7 @@ Hello World /// -## 退出虛擬環境 +## 退出虛擬環境 { #deactivate-the-virtual-environment } 當你完成工作後,你可以**退出**虛擬環境。 @@ -419,7 +439,7 @@ $ deactivate 這樣,當你執行 `python` 時它不會嘗試從已安裝套件的虛擬環境中執行。 -## 開始工作 +## 開始工作 { #ready-to-work } 現在你已經準備好開始你的工作了。 @@ -433,7 +453,7 @@ $ deactivate /// -## 為什麼要使用虛擬環境 +## 為什麼要使用虛擬環境 { #why-virtual-environments } 你需要安裝 Python 才能使用 FastAPI。 @@ -443,7 +463,7 @@ $ deactivate 然而,如果你直接使用 `pip`,套件將會安裝在你的**全域 Python 環境**中(即 Python 的全域安裝)。 -### 存在的問題 +### 存在的問題 { #the-problem } 那麼,在全域 Python 環境中安裝套件有什麼問題呢? @@ -526,7 +546,7 @@ Python 套件在推出**新版本**時通常會儘量**避免破壞性更改** 此外,取決於你的操作系統(例如 Linux、Windows、macOS),它可能已經預先安裝了 Python。在這種情況下,它可能已經有一些系統所需的套件和特定版本。如果你在全域 Python 環境中安裝套件,可能會**破壞**某些隨作業系統一起安裝的程式。 -## 套件安裝在哪裡 +## 套件安裝在哪裡 { #where-are-packages-installed } 當你安裝 Python 時,它會在你的電腦中建立一些目錄並放置一些檔案。 @@ -552,7 +572,7 @@ $ pip install "fastapi[standard]" 預設情況下,這些下載和解壓的檔案會放置於隨 Python 安裝的目錄中,即**全域環境**。 -## 什麼是虛擬環境 +## 什麼是虛擬環境 { #what-are-virtual-environments } 解決套件都安裝在全域環境中的問題方法是為你所做的每個專案使用一個**虛擬環境**。 @@ -577,7 +597,7 @@ flowchart TB stone-project ~~~ azkaban-project ``` -## 啟用虛擬環境意味著什麼 +## 啟用虛擬環境意味著什麼 { #what-does-activating-a-virtual-environment-mean } 當你啟用了虛擬環境,例如: @@ -714,7 +734,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python 啟用虛擬環境還會改變其他一些內容,但這是它所做的最重要的事情之一。 -## 檢查虛擬環境 +## 檢查虛擬環境 { #checking-a-virtual-environment } 當你檢查虛擬環境是否啟動時,例如: @@ -766,7 +786,7 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python /// -## 為什麼要停用虛擬環境 +## 為什麼要停用虛擬環境 { #why-deactivate-a-virtual-environment } 例如,你可能正在一個專案 `philosophers-stone` 上工作,**啟動了該虛擬環境**,安裝了套件並使用了該環境, @@ -820,7 +840,7 @@ I solemnly swear 🐺 -## 替代方案 +## 替代方案 { #alternatives } 這是一個簡單的指南,幫助你入門並教會你如何理解一切**底層**的原理。 @@ -837,7 +857,7 @@ I solemnly swear 🐺 * 確保你有一個**精確**的套件和版本集合來安裝,包括它們的依賴項,這樣你可以確保專案在生產環境中運行的狀態與開發時在你的電腦上運行的狀態完全相同,這被稱為**鎖定** * 還有很多其他功能 -## 結論 +## 結論 { #conclusion } 如果你讀過並理解了所有這些,現在**你對虛擬環境的了解已超過許多開發者**。🤓 From f23ea3bd957df9add546af378407b058473b453b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 15:33:24 +0000 Subject: [PATCH 191/243] =?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 f52be20d3..9f70eccb5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Update translations for zh-hant (update-outdated). PR [#14825](https://github.com/fastapi/fastapi/pull/14825) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for uk (update-outdated). PR [#14822](https://github.com/fastapi/fastapi/pull/14822) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update docs and translations scripts, enable Turkish. PR [#14824](https://github.com/fastapi/fastapi/pull/14824) by [@tiangolo](https://github.com/tiangolo). From 6ff8ff5b57bdbfb93030899091fe8219a722a74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 07:34:19 -0800 Subject: [PATCH 192/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20fr=20(update-outdated)=20(#14826)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/fr/docs/advanced/additional-responses.md | 38 +- .../docs/advanced/additional-status-codes.md | 20 +- docs/fr/docs/advanced/index.md | 18 +- .../path-operation-advanced-configuration.md | 114 ++-- docs/fr/docs/advanced/response-directly.md | 30 +- docs/fr/docs/benchmarks.md | 36 +- docs/fr/docs/deployment/docker.md | 599 +++++++++++++++--- docs/fr/docs/deployment/https.md | 247 ++++++-- docs/fr/docs/deployment/index.md | 8 +- docs/fr/docs/deployment/versions.md | 76 +-- docs/fr/docs/index.md | 399 +++++++----- docs/fr/docs/learn/index.md | 4 +- docs/fr/docs/project-generation.md | 102 +-- docs/fr/docs/python-types.md | 413 ++++++++---- docs/fr/docs/tutorial/background-tasks.md | 50 +- docs/fr/docs/tutorial/body-multiple-params.md | 88 +-- docs/fr/docs/tutorial/body.md | 80 +-- docs/fr/docs/tutorial/debugging.md | 24 +- docs/fr/docs/tutorial/first-steps.md | 335 +++++----- docs/fr/docs/tutorial/index.md | 78 ++- .../path-params-numeric-validations.md | 49 +- docs/fr/docs/tutorial/path-params.md | 225 ++++--- .../tutorial/query-params-str-validations.md | 439 +++++++++---- docs/fr/docs/tutorial/query-params.md | 133 ++-- 24 files changed, 2303 insertions(+), 1302 deletions(-) diff --git a/docs/fr/docs/advanced/additional-responses.md b/docs/fr/docs/advanced/additional-responses.md index 38527aad3..dabcded52 100644 --- a/docs/fr/docs/advanced/additional-responses.md +++ b/docs/fr/docs/advanced/additional-responses.md @@ -1,6 +1,6 @@ -# Réponses supplémentaires dans OpenAPI +# Réponses supplémentaires dans OpenAPI { #additional-responses-in-openapi } -/// warning | Attention +/// warning | Alertes Ceci concerne un sujet plutôt avancé. @@ -14,9 +14,9 @@ Ces réponses supplémentaires seront incluses dans le schéma OpenAPI, elles ap Mais pour ces réponses supplémentaires, vous devez vous assurer de renvoyer directement une `Response` comme `JSONResponse`, avec votre code HTTP et votre contenu. -## Réponse supplémentaire avec `model` +## Réponse supplémentaire avec `model` { #additional-response-with-model } -Vous pouvez ajouter à votre décorateur de *paramètre de chemin* un paramètre `responses`. +Vous pouvez passer à vos décorateurs de *chemin d'accès* un paramètre `responses`. Il prend comme valeur un `dict` dont les clés sont des codes HTTP pour chaque réponse, comme `200`, et la valeur de ces clés sont d'autres `dict` avec des informations pour chacun d'eux. @@ -26,7 +26,7 @@ Chacun de ces `dict` de réponse peut avoir une clé `model`, contenant un modè Par exemple, pour déclarer une autre réponse avec un code HTTP `404` et un modèle Pydantic `Message`, vous pouvez écrire : -{* ../../docs_src/additional_responses/tutorial001.py hl[18,22] *} +{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *} /// note | Remarque @@ -49,7 +49,7 @@ Le bon endroit est : /// -Les réponses générées au format OpenAPI pour cette *opération de chemin* seront : +Les réponses générées au format OpenAPI pour ce *chemin d'accès* seront : ```JSON hl_lines="3-12" { @@ -169,13 +169,13 @@ Les schémas sont référencés à un autre endroit du modèle OpenAPI : } ``` -## Types de médias supplémentaires pour la réponse principale +## Types de médias supplémentaires pour la réponse principale { #additional-media-types-for-the-main-response } Vous pouvez utiliser ce même paramètre `responses` pour ajouter différents types de médias pour la même réponse principale. -Par exemple, vous pouvez ajouter un type de média supplémentaire `image/png`, en déclarant que votre *opération de chemin* peut renvoyer un objet JSON (avec le type de média `application/json`) ou une image PNG : +Par exemple, vous pouvez ajouter un type de média supplémentaire `image/png`, en déclarant que votre *chemin d'accès* peut renvoyer un objet JSON (avec le type de média `application/json`) ou une image PNG : -{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *} +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} /// note | Remarque @@ -191,7 +191,7 @@ Mais si vous avez spécifié une classe de réponse personnalisée avec `None` c /// -## Combinaison d'informations +## Combiner les informations { #combining-information } Vous pouvez également combiner des informations de réponse provenant de plusieurs endroits, y compris les paramètres `response_model`, `status_code` et `responses`. @@ -203,17 +203,17 @@ Par exemple, vous pouvez déclarer une réponse avec un code HTTP `404` qui util Et une réponse avec un code HTTP `200` qui utilise votre `response_model`, mais inclut un `example` personnalisé : -{* ../../docs_src/additional_responses/tutorial003.py hl[20:31] *} +{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *} Tout sera combiné et inclus dans votre OpenAPI, et affiché dans la documentation de l'API : -## Combinez les réponses prédéfinies et les réponses personnalisées +## Combinez les réponses prédéfinies et les réponses personnalisées { #combine-predefined-responses-and-custom-ones } -Vous voulez peut-être avoir des réponses prédéfinies qui s'appliquent à de nombreux *paramètre de chemin*, mais vous souhaitez les combiner avec des réponses personnalisées nécessaires à chaque *opération de chemin*. +Vous voulez peut-être avoir des réponses prédéfinies qui s'appliquent à de nombreux *chemins d'accès*, mais vous souhaitez les combiner avec des réponses personnalisées nécessaires à chaque *chemin d'accès*. -Dans ces cas, vous pouvez utiliser la technique Python "d'affection par décomposition" (appelé _unpacking_ en anglais) d'un `dict` avec `**dict_to_unpack` : +Dans ces cas, vous pouvez utiliser la technique Python « unpacking » d'un `dict` avec `**dict_to_unpack` : ```Python old_dict = { @@ -233,15 +233,15 @@ Ici, `new_dict` contiendra toutes les paires clé-valeur de `old_dict` plus la n } ``` -Vous pouvez utiliser cette technique pour réutiliser certaines réponses prédéfinies dans vos *paramètres de chemin* et les combiner avec des réponses personnalisées supplémentaires. +Vous pouvez utiliser cette technique pour réutiliser certaines réponses prédéfinies dans vos *chemins d'accès* et les combiner avec des réponses personnalisées supplémentaires. Par exemple: -{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *} +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} -## Plus d'informations sur les réponses OpenAPI +## Plus d'informations sur les réponses OpenAPI { #more-information-about-openapi-responses } Pour voir exactement ce que vous pouvez inclure dans les réponses, vous pouvez consulter ces sections dans la spécification OpenAPI : -* Objet Responses de OpenAPI , il inclut le `Response Object`. -* Objet Response de OpenAPI , vous pouvez inclure n'importe quoi directement dans chaque réponse à l'intérieur de votre paramètre `responses`. Y compris `description`, `headers`, `content` (à l'intérieur de cela, vous déclarez différents types de médias et schémas JSON) et `links`. +* Objet Responses de OpenAPI, il inclut le `Response Object`. +* Objet Response de OpenAPI, vous pouvez inclure n'importe quoi directement dans chaque réponse à l'intérieur de votre paramètre `responses`. Y compris `description`, `headers`, `content` (à l'intérieur de cela, vous déclarez différents types de médias et schémas JSON) et `links`. diff --git a/docs/fr/docs/advanced/additional-status-codes.md b/docs/fr/docs/advanced/additional-status-codes.md index dde6b9a63..b2befffa8 100644 --- a/docs/fr/docs/advanced/additional-status-codes.md +++ b/docs/fr/docs/advanced/additional-status-codes.md @@ -1,28 +1,28 @@ -# Codes HTTP supplémentaires +# Codes HTTP supplémentaires { #additional-status-codes } Par défaut, **FastAPI** renverra les réponses à l'aide d'une structure de données `JSONResponse`, en plaçant la réponse de votre *chemin d'accès* à l'intérieur de cette `JSONResponse`. Il utilisera le code HTTP par défaut ou celui que vous avez défini dans votre *chemin d'accès*. -## Codes HTTP supplémentaires +## Codes HTTP supplémentaires { #additional-status-codes_1 } Si vous souhaitez renvoyer des codes HTTP supplémentaires en plus du code principal, vous pouvez le faire en renvoyant directement une `Response`, comme une `JSONResponse`, et en définissant directement le code HTTP supplémentaire. -Par exemple, disons que vous voulez avoir un *chemin d'accès* qui permet de mettre à jour les éléments et renvoie les codes HTTP 200 "OK" en cas de succès. +Par exemple, disons que vous voulez avoir un *chemin d'accès* qui permet de mettre à jour les éléments et renvoie les codes HTTP 200 « OK » en cas de succès. -Mais vous voulez aussi qu'il accepte de nouveaux éléments. Et lorsque les éléments n'existaient pas auparavant, il les crée et renvoie un code HTTP de 201 "Créé". +Mais vous voulez aussi qu'il accepte de nouveaux éléments. Et lorsque les éléments n'existaient pas auparavant, il les crée et renvoie un code HTTP de 201 « Créé ». Pour y parvenir, importez `JSONResponse` et renvoyez-y directement votre contenu, en définissant le `status_code` que vous souhaitez : -{* ../../docs_src/additional_status_codes/tutorial001.py hl[4,25] *} +{* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *} -/// warning | Attention +/// warning | Alertes Lorsque vous renvoyez une `Response` directement, comme dans l'exemple ci-dessus, elle sera renvoyée directement. Elle ne sera pas sérialisée avec un modèle. -Assurez-vous qu'il contient les données souhaitées et que les valeurs soient dans un format JSON valides (si vous utilisez une `JSONResponse`). +Assurez-vous qu'il contient les données souhaitées et que les valeurs sont dans un format JSON valide (si vous utilisez une `JSONResponse`). /// @@ -30,12 +30,12 @@ Assurez-vous qu'il contient les données souhaitées et que les valeurs soient d Vous pouvez également utiliser `from starlette.responses import JSONResponse`. -Pour plus de commodités, **FastAPI** fournit les objets `starlette.responses` sous forme d'un alias accessible par `fastapi.responses`. Mais la plupart des réponses disponibles proviennent directement de Starlette. Il en est de même avec l'objet `statut`. +Pour plus de commodités, **FastAPI** fournit les objets `starlette.responses` sous forme d'un alias accessible par `fastapi.responses`. Mais la plupart des réponses disponibles proviennent directement de Starlette. Il en est de même avec `status`. /// -## Documents OpenAPI et API +## Documents OpenAPI et API { #openapi-and-api-docs } Si vous renvoyez directement des codes HTTP et des réponses supplémentaires, ils ne seront pas inclus dans le schéma OpenAPI (la documentation de l'API), car FastAPI n'a aucun moyen de savoir à l'avance ce que vous allez renvoyer. -Mais vous pouvez documenter cela dans votre code, en utilisant : [Réponses supplémentaires dans OpenAPI](additional-responses.md){.internal-link target=_blank}. +Mais vous pouvez documenter cela dans votre code, en utilisant : [Réponses supplémentaires](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/fr/docs/advanced/index.md b/docs/fr/docs/advanced/index.md index d9d8ad8e6..a2f9d3b1b 100644 --- a/docs/fr/docs/advanced/index.md +++ b/docs/fr/docs/advanced/index.md @@ -1,27 +1,21 @@ -# Guide de l'utilisateur avancé +# Guide de l'utilisateur avancé { #advanced-user-guide } -## Caractéristiques supplémentaires +## Caractéristiques supplémentaires { #additional-features } Le [Tutoriel - Guide de l'utilisateur](../tutorial/index.md){.internal-link target=_blank} devrait suffire à vous faire découvrir toutes les fonctionnalités principales de **FastAPI**. Dans les sections suivantes, vous verrez des options, configurations et fonctionnalités supplémentaires. -/// note | Remarque +/// tip | Astuce -Les sections de ce chapitre ne sont **pas nécessairement "avancées"**. +Les sections suivantes ne sont **pas nécessairement « avancées »**. -Et il est possible que pour votre cas d'utilisation, la solution se trouve dans l'un d'entre eux. +Et il est possible que, pour votre cas d'utilisation, la solution se trouve dans l'une d'entre elles. /// -## Lisez d'abord le didacticiel +## Lire d'abord le tutoriel { #read-the-tutorial-first } Vous pouvez utiliser la plupart des fonctionnalités de **FastAPI** grâce aux connaissances du [Tutoriel - Guide de l'utilisateur](../tutorial/index.md){.internal-link target=_blank}. Et les sections suivantes supposent que vous l'avez lu et que vous en connaissez les idées principales. - -## Cours TestDriven.io - -Si vous souhaitez suivre un cours pour débutants avancés pour compléter cette section de la documentation, vous pouvez consulter : Développement piloté par les tests avec FastAPI et Docker par **TestDriven.io**. - -10 % de tous les bénéfices de ce cours sont reversés au développement de **FastAPI**. 🎉 😄 diff --git a/docs/fr/docs/advanced/path-operation-advanced-configuration.md b/docs/fr/docs/advanced/path-operation-advanced-configuration.md index 7daf0fc65..fc88f3363 100644 --- a/docs/fr/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/fr/docs/advanced/path-operation-advanced-configuration.md @@ -1,106 +1,108 @@ -# Configuration avancée des paramètres de chemin +# Configuration avancée des chemins d'accès { #path-operation-advanced-configuration } -## ID d'opération OpenAPI +## ID d’opération OpenAPI { #openapi-operationid } -/// warning | Attention +/// warning | Alertes -Si vous n'êtes pas un "expert" en OpenAPI, vous n'en avez probablement pas besoin. +Si vous n’êtes pas un « expert » d’OpenAPI, vous n’en avez probablement pas besoin. /// -Dans OpenAPI, les chemins sont des ressources, tels que /users/ ou /items/, exposées par votre API, et les opérations sont les méthodes HTTP utilisées pour manipuler ces chemins, telles que GET, POST ou DELETE. Les operationId sont des chaînes uniques facultatives utilisées pour identifier une opération d'un chemin. Vous pouvez définir l'OpenAPI `operationId` à utiliser dans votre *opération de chemin* avec le paramètre `operation_id`. +Vous pouvez définir l’OpenAPI `operationId` à utiliser dans votre chemin d’accès avec le paramètre `operation_id`. -Vous devez vous assurer qu'il est unique pour chaque opération. +Vous devez vous assurer qu’il est unique pour chaque opération. -{* ../../docs_src/path_operation_advanced_configuration/tutorial001.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} -### Utilisation du nom *path operation function* comme operationId +### Utiliser le nom de la fonction de chemin d’accès comme operationId { #using-the-path-operation-function-name-as-the-operationid } -Si vous souhaitez utiliser les noms de fonction de vos API comme `operationId`, vous pouvez les parcourir tous et remplacer chaque `operation_id` de l'*opération de chemin* en utilisant leur `APIRoute.name`. +Si vous souhaitez utiliser les noms de fonction de vos API comme `operationId`, vous pouvez les parcourir tous et remplacer l’`operation_id` de chaque chemin d’accès en utilisant leur `APIRoute.name`. -Vous devriez le faire après avoir ajouté toutes vos *paramètres de chemin*. +Vous devez le faire après avoir ajouté tous vos chemins d’accès. -{* ../../docs_src/path_operation_advanced_configuration/tutorial002.py hl[2,12:21,24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *} /// tip | Astuce -Si vous appelez manuellement `app.openapi()`, vous devez mettre à jour les `operationId` avant. +Si vous appelez manuellement `app.openapi()`, vous devez mettre à jour les `operationId` avant cela. /// -/// warning | Attention +/// warning | Alertes -Pour faire cela, vous devez vous assurer que chacun de vos *chemin* ait un nom unique. +Si vous faites cela, vous devez vous assurer que chacune de vos fonctions de chemin d’accès a un nom unique. -Même s'ils se trouvent dans des modules différents (fichiers Python). +Même si elles se trouvent dans des modules différents (fichiers Python). /// -## Exclusion d'OpenAPI +## Exclusion d’OpenAPI { #exclude-from-openapi } -Pour exclure un *chemin* du schéma OpenAPI généré (et donc des systèmes de documentation automatiques), utilisez le paramètre `include_in_schema` et assignez-lui la valeur `False` : +Pour exclure un chemin d’accès du schéma OpenAPI généré (et donc des systèmes de documentation automatiques), utilisez le paramètre `include_in_schema` et définissez-le à `False` : -{* ../../docs_src/path_operation_advanced_configuration/tutorial003.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *} -## Description avancée de docstring +## Description avancée depuis la docstring { #advanced-description-from-docstring } -Vous pouvez limiter le texte utilisé de la docstring d'une *fonction de chemin* qui sera affiché sur OpenAPI. +Vous pouvez limiter les lignes utilisées de la docstring d’une fonction de chemin d’accès pour OpenAPI. -L'ajout d'un `\f` (un caractère d'échappement "form feed") va permettre à **FastAPI** de tronquer la sortie utilisée pour OpenAPI à ce stade. +L’ajout d’un `\f` (un caractère « saut de page » échappé) amène **FastAPI** à tronquer la sortie utilisée pour OpenAPI à cet endroit. -Il n'apparaîtra pas dans la documentation, mais d'autres outils (tel que Sphinx) pourront utiliser le reste. +Cela n’apparaîtra pas dans la documentation, mais d’autres outils (comme Sphinx) pourront utiliser le reste. -{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} -## Réponses supplémentaires +## Réponses supplémentaires { #additional-responses } -Vous avez probablement vu comment déclarer le `response_model` et le `status_code` pour une *opération de chemin*. +Vous avez probablement vu comment déclarer le `response_model` et le `status_code` pour un chemin d’accès. -Cela définit les métadonnées sur la réponse principale d'une *opération de chemin*. +Cela définit les métadonnées sur la réponse principale d’un chemin d’accès. Vous pouvez également déclarer des réponses supplémentaires avec leurs modèles, codes de statut, etc. -Il y a un chapitre entier ici dans la documentation à ce sujet, vous pouvez le lire sur [Réponses supplémentaires dans OpenAPI](additional-responses.md){.internal-link target=_blank}. +Il y a un chapitre entier dans la documentation à ce sujet, vous pouvez le lire dans [Réponses supplémentaires dans OpenAPI](additional-responses.md){.internal-link target=_blank}. -## OpenAPI supplémentaire +## OpenAPI supplémentaire { #openapi-extra } -Lorsque vous déclarez un *chemin* dans votre application, **FastAPI** génère automatiquement les métadonnées concernant ce *chemin* à inclure dans le schéma OpenAPI. +Lorsque vous déclarez un chemin d’accès dans votre application, **FastAPI** génère automatiquement les métadonnées pertinentes à propos de ce chemin d’accès à inclure dans le schéma OpenAPI. /// note | Détails techniques -La spécification OpenAPI appelle ces métadonnées des Objets d'opération. +Dans la spécification OpenAPI, cela s’appelle l’objet Operation. /// -Il contient toutes les informations sur le *chemin* et est utilisé pour générer automatiquement la documentation. +Il contient toutes les informations sur le chemin d’accès et est utilisé pour générer la documentation automatique. Il inclut les `tags`, `parameters`, `requestBody`, `responses`, etc. -Ce schéma OpenAPI spécifique aux *operations* est normalement généré automatiquement par **FastAPI**, mais vous pouvez également l'étendre. +Ce schéma OpenAPI spécifique à un chemin d’accès est normalement généré automatiquement par **FastAPI**, mais vous pouvez également l’étendre. /// tip | Astuce -Si vous avez seulement besoin de déclarer des réponses supplémentaires, un moyen plus pratique de le faire est d'utiliser les [réponses supplémentaires dans OpenAPI](additional-responses.md){.internal-link target=_blank}. +Ceci est un point d’extension de bas niveau. + +Si vous avez seulement besoin de déclarer des réponses supplémentaires, un moyen plus pratique de le faire est d’utiliser [Réponses supplémentaires dans OpenAPI](additional-responses.md){.internal-link target=_blank}. /// -Vous pouvez étendre le schéma OpenAPI pour une *opération de chemin* en utilisant le paramètre `openapi_extra`. +Vous pouvez étendre le schéma OpenAPI pour un chemin d’accès en utilisant le paramètre `openapi_extra`. -### Extensions OpenAPI +### Extensions OpenAPI { #openapi-extensions } -Cet `openapi_extra` peut être utile, par exemple, pour déclarer [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions) : +Cet `openapi_extra` peut être utile, par exemple, pour déclarer des [Extensions OpenAPI](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions) : -{* ../../docs_src/path_operation_advanced_configuration/tutorial005.py hl[6] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *} -Si vous ouvrez la documentation automatique de l'API, votre extension apparaîtra au bas du *chemin* spécifique. +Si vous ouvrez la documentation automatique de l’API, votre extension apparaîtra en bas du chemin d’accès spécifique. -Et dans le fichier openapi généré (`/openapi.json`), vous verrez également votre extension dans le cadre du *chemin* spécifique : +Et si vous consultez l’OpenAPI résultant (à `/openapi.json` dans votre API), vous verrez également votre extension comme partie du chemin d’accès spécifique : ```JSON hl_lines="22" { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" @@ -127,44 +129,44 @@ Et dans le fichier openapi généré (`/openapi.json`), vous verrez également v } ``` -### Personnalisation du Schéma OpenAPI pour un chemin +### Personnaliser le schéma OpenAPI d’un chemin d’accès { #custom-openapi-path-operation-schema } -Le dictionnaire contenu dans la variable `openapi_extra` sera fusionné avec le schéma OpenAPI généré automatiquement pour l'*opération de chemin*. +Le dictionnaire dans `openapi_extra` sera fusionné en profondeur avec le schéma OpenAPI généré automatiquement pour le chemin d’accès. Ainsi, vous pouvez ajouter des données supplémentaires au schéma généré automatiquement. -Par exemple, vous pouvez décider de lire et de valider la requête avec votre propre code, sans utiliser les fonctionnalités automatiques de validation proposée par Pydantic, mais vous pouvez toujours définir la requête dans le schéma OpenAPI. +Par exemple, vous pourriez décider de lire et de valider la requête avec votre propre code, sans utiliser les fonctionnalités automatiques de FastAPI avec Pydantic, mais vous pourriez tout de même vouloir définir la requête dans le schéma OpenAPI. -Vous pouvez le faire avec `openapi_extra` : +Vous pourriez le faire avec `openapi_extra` : -{* ../../docs_src/path_operation_advanced_configuration/tutorial006.py hl[20:37,39:40] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} -Dans cet exemple, nous n'avons déclaré aucun modèle Pydantic. En fait, le corps de la requête n'est même pas parsé en tant que JSON, il est lu directement en tant que `bytes`, et la fonction `magic_data_reader()` serait chargé de l'analyser d'une manière ou d'une autre. +Dans cet exemple, nous n’avons déclaré aucun modèle Pydantic. En fait, le corps de la requête n’est même pas parsé en JSON, il est lu directement en tant que `bytes`, et la fonction `magic_data_reader()` serait chargée de l’analyser d’une manière ou d’une autre. Néanmoins, nous pouvons déclarer le schéma attendu pour le corps de la requête. -### Type de contenu OpenAPI personnalisé +### Type de contenu OpenAPI personnalisé { #custom-openapi-content-type } -En utilisant cette même astuce, vous pouvez utiliser un modèle Pydantic pour définir le schéma JSON qui est ensuite inclus dans la section de schéma OpenAPI personnalisée pour le *chemin* concerné. +En utilisant cette même astuce, vous pourriez utiliser un modèle Pydantic pour définir le JSON Schema qui est ensuite inclus dans la section de schéma OpenAPI personnalisée pour le chemin d’accès. -Et vous pouvez le faire même si le type de données dans la requête n'est pas au format JSON. +Et vous pourriez le faire même si le type de données dans la requête n’est pas du JSON. -Dans cet exemple, nous n'utilisons pas les fonctionnalités de FastAPI pour extraire le schéma JSON des modèles Pydantic ni la validation automatique pour JSON. En fait, nous déclarons le type de contenu de la requête en tant que YAML, et non JSON : +Par exemple, dans cette application nous n’utilisons pas la fonctionnalité intégrée de FastAPI pour extraire le JSON Schema des modèles Pydantic ni la validation automatique pour le JSON. En fait, nous déclarons le type de contenu de la requête comme YAML, pas JSON : -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22,24] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} -Néanmoins, bien que nous n'utilisions pas la fonctionnalité par défaut, nous utilisons toujours un modèle Pydantic pour générer manuellement le schéma JSON pour les données que nous souhaitons recevoir en YAML. +Néanmoins, bien que nous n’utilisions pas la fonctionnalité intégrée par défaut, nous utilisons toujours un modèle Pydantic pour générer manuellement le JSON Schema pour les données que nous souhaitons recevoir en YAML. -Ensuite, nous utilisons directement la requête et extrayons son contenu en tant qu'octets. Cela signifie que FastAPI n'essaiera même pas d'analyser le payload de la requête en tant que JSON. +Ensuite, nous utilisons directement la requête et extrayons le corps en tant que `bytes`. Cela signifie que FastAPI n’essaiera même pas d’analyser le payload de la requête en JSON. -Et nous analysons directement ce contenu YAML, puis nous utilisons à nouveau le même modèle Pydantic pour valider le contenu YAML : +Ensuite, dans notre code, nous analysons directement ce contenu YAML, puis nous utilisons à nouveau le même modèle Pydantic pour valider le contenu YAML : -{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *} +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} /// tip | Astuce Ici, nous réutilisons le même modèle Pydantic. -Mais nous aurions pu tout aussi bien pu le valider d'une autre manière. +Mais de la même manière, nous aurions pu le valider autrement. /// diff --git a/docs/fr/docs/advanced/response-directly.md b/docs/fr/docs/advanced/response-directly.md index 4ff883c77..f35c39c06 100644 --- a/docs/fr/docs/advanced/response-directly.md +++ b/docs/fr/docs/advanced/response-directly.md @@ -1,20 +1,20 @@ -# Renvoyer directement une réponse +# Renvoyer directement une réponse { #return-a-response-directly } -Lorsque vous créez une *opération de chemins* **FastAPI**, vous pouvez normalement retourner n'importe quelle donnée : un `dict`, une `list`, un modèle Pydantic, un modèle de base de données, etc. +Lorsque vous créez un *chemin d'accès* **FastAPI**, vous pouvez normalement retourner n'importe quelle donnée : un `dict`, une `list`, un modèle Pydantic, un modèle de base de données, etc. -Par défaut, **FastAPI** convertirait automatiquement cette valeur de retour en JSON en utilisant le `jsonable_encoder` expliqué dans [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}. +Par défaut, **FastAPI** convertirait automatiquement cette valeur de retour en JSON en utilisant le `jsonable_encoder` expliqué dans [Encodeur compatible JSON](../tutorial/encoder.md){.internal-link target=_blank}. Ensuite, en arrière-plan, il mettra ces données JSON-compatible (par exemple un `dict`) à l'intérieur d'un `JSONResponse` qui sera utilisé pour envoyer la réponse au client. -Mais vous pouvez retourner une `JSONResponse` directement à partir de vos *opérations de chemin*. +Mais vous pouvez retourner une `JSONResponse` directement à partir de vos *chemins d'accès*. Cela peut être utile, par exemple, pour retourner des en-têtes personnalisés ou des cookies. -## Renvoyer une `Response` +## Renvoyer une `Response` { #return-a-response } En fait, vous pouvez retourner n'importe quelle `Response` ou n'importe quelle sous-classe de celle-ci. -/// note | Remarque +/// tip | Astuce `JSONResponse` est elle-même une sous-classe de `Response`. @@ -24,27 +24,27 @@ Et quand vous retournez une `Response`, **FastAPI** la transmet directement. Elle ne fera aucune conversion de données avec les modèles Pydantic, elle ne convertira pas le contenu en un type quelconque. -Cela vous donne beaucoup de flexibilité. Vous pouvez retourner n'importe quel type de données, surcharger n'importe quelle déclaration ou validation de données. +Cela vous donne beaucoup de flexibilité. Vous pouvez retourner n'importe quel type de données, surcharger n'importe quelle déclaration ou validation de données, etc. -## Utiliser le `jsonable_encoder` dans une `Response` +## Utiliser le `jsonable_encoder` dans une `Response` { #using-the-jsonable-encoder-in-a-response } -Parce que **FastAPI** n'apporte aucune modification à une `Response` que vous retournez, vous devez vous assurer que son contenu est prêt à être utilisé (sérialisable). +Parce que **FastAPI** n'apporte aucune modification à une `Response` que vous retournez, vous devez vous assurer que son contenu est prêt pour cela. Par exemple, vous ne pouvez pas mettre un modèle Pydantic dans une `JSONResponse` sans d'abord le convertir en un `dict` avec tous les types de données (comme `datetime`, `UUID`, etc.) convertis en types compatibles avec JSON. -Pour ces cas, vous pouvez spécifier un appel à `jsonable_encoder` pour convertir vos données avant de les passer à une réponse : +Pour ces cas, vous pouvez utiliser le `jsonable_encoder` pour convertir vos données avant de les passer à une réponse : -{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *} +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} /// note | Détails techniques Vous pouvez aussi utiliser `from starlette.responses import JSONResponse`. -**FastAPI** fournit le même objet `starlette.responses` que `fastapi.responses` juste par commodité pour le développeur. Mais la plupart des réponses disponibles proviennent directement de Starlette. +**FastAPI** fournit le même `starlette.responses` que `fastapi.responses` juste par commodité pour vous, le développeur. Mais la plupart des réponses disponibles proviennent directement de Starlette. /// -## Renvoyer une `Response` personnalisée +## Renvoyer une `Response` personnalisée { #returning-a-custom-response } L'exemple ci-dessus montre toutes les parties dont vous avez besoin, mais il n'est pas encore très utile, car vous auriez pu retourner l'`item` directement, et **FastAPI** l'aurait mis dans une `JSONResponse` pour vous, en le convertissant en `dict`, etc. Tout cela par défaut. @@ -54,9 +54,9 @@ Disons que vous voulez retourner une réponse étant l'un des frameworks Python les plus rapides disponibles, seulement inférieur à Starlette et Uvicorn (tous deux utilisés au cœur de FastAPI). (*) +Les benchmarks indépendants de TechEmpower montrent que les applications **FastAPI** s’exécutant avec Uvicorn sont parmi les frameworks Python les plus rapides disponibles, seulement en dessous de Starlette et Uvicorn eux‑mêmes (tous deux utilisés en interne par FastAPI). -Mais en prêtant attention aux tests de performance et aux comparaisons, il faut tenir compte de ce qu'il suit. +Mais en prêtant attention aux tests de performance et aux comparaisons, vous devez tenir compte de ce qui suit. -## Tests de performance et rapidité +## Tests de performance et rapidité { #benchmarks-and-speed } Lorsque vous vérifiez les tests de performance, il est commun de voir plusieurs outils de différents types comparés comme équivalents. En particulier, on voit Uvicorn, Starlette et FastAPI comparés (parmi de nombreux autres outils). -Plus le problème résolu par un outil est simple, mieux seront les performances obtenues. Et la plupart des tests de performance ne prennent pas en compte les fonctionnalités additionnelles fournies par les outils. +Plus le problème résolu par un outil est simple, meilleures seront les performances obtenues. Et la plupart des tests de performance ne prennent pas en compte les fonctionnalités additionnelles fournies par les outils. La hiérarchie est la suivante : * **Uvicorn** : un serveur ASGI - * **Starlette** : (utilise Uvicorn) un micro-framework web - * **FastAPI**: (utilise Starlette) un micro-framework pour API disposant de fonctionnalités additionnelles pour la création d'API, avec la validation des données, etc. + * **Starlette** : (utilise Uvicorn) un microframework web + * **FastAPI**: (utilise Starlette) un microframework pour API disposant de fonctionnalités additionnelles pour la création d'API, avec la validation des données, etc. * **Uvicorn** : - * A les meilleures performances, étant donné qu'il n'a pas beaucoup de code mis-à-part le serveur en lui-même. - * On n'écrit pas une application avec uniquement Uvicorn. Cela signifie que le code devrait inclure plus ou moins, au minimum, tout le code offert par Starlette (ou **FastAPI**). Et si on fait cela, l'application finale apportera les mêmes complications que si on avait utilisé un framework et que l'on avait minimisé la quantité de code et de bugs. - * Si on compare Uvicorn, il faut le comparer à d'autre applications de serveurs comme Daphne, Hypercorn, uWSGI, etc. + * A les meilleures performances, étant donné qu'il n'a pas beaucoup de code mis à part le serveur en lui‑même. + * On n'écrit pas une application directement avec Uvicorn. Cela signifie que le code devrait inclure, au minimum, plus ou moins tout le code offert par Starlette (ou **FastAPI**). Et si on fait cela, l'application finale aura la même surcharge que si on avait utilisé un framework, tout en minimisant la quantité de code et les bugs. + * Si on compare Uvicorn, il faut le comparer à d'autres serveurs d'applications comme Daphne, Hypercorn, uWSGI, etc. * **Starlette** : - * A les seconde meilleures performances après Uvicorn. Starlette utilise en réalité Uvicorn. De ce fait, il ne peut qu’être plus "lent" qu'Uvicorn car il requiert l'exécution de plus de code. - * Cependant il nous apporte les outils pour construire une application web simple, avec un routage basé sur des chemins, etc. - * Si on compare Starlette, il faut le comparer à d'autres frameworks web (ou micorframework) comme Sanic, Flask, Django, etc. + * A les secondes meilleures performances après Uvicorn. En réalité, Starlette utilise Uvicorn. De ce fait, il ne peut qu’être plus « lent » qu'Uvicorn car il requiert l'exécution de plus de code. + * Cependant, il apporte les outils pour construire une application web simple, avec un routage basé sur des chemins, etc. + * Si on compare Starlette, il faut le comparer à d'autres frameworks web (ou microframeworks) comme Sanic, Flask, Django, etc. * **FastAPI** : - * Comme Starlette, FastAPI utilise Uvicorn et ne peut donc pas être plus rapide que ce dernier. - * FastAPI apporte des fonctionnalités supplémentaires à Starlette. Des fonctionnalités qui sont nécessaires presque systématiquement lors de la création d'une API, comme la validation des données, la sérialisation. En utilisant FastAPI, on obtient une documentation automatiquement (qui ne requiert aucune manipulation pour être mise en place). - * Si on n'utilisait pas FastAPI mais directement Starlette (ou un outil équivalent comme Sanic, Flask, Responder, etc) il faudrait implémenter la validation des données et la sérialisation par nous-même. Le résultat serait donc le même dans les deux cas mais du travail supplémentaire serait à réaliser avec Starlette, surtout en considérant que la validation des données et la sérialisation représentent la plus grande quantité de code à écrire dans une application. - * De ce fait, en utilisant FastAPI on minimise le temps de développement, les bugs, le nombre de lignes de code, et on obtient les mêmes performances (si ce n'est de meilleurs performances) que l'on aurait pu avoir sans ce framework (en ayant à implémenter de nombreuses fonctionnalités importantes par nous-mêmes). - * Si on compare FastAPI, il faut le comparer à d'autres frameworks web (ou ensemble d'outils) qui fournissent la validation des données, la sérialisation et la documentation, comme Flask-apispec, NestJS, Molten, etc. + * Comme Starlette utilise Uvicorn et ne peut donc pas être plus rapide que lui, **FastAPI** utilise Starlette et ne peut donc pas être plus rapide que lui. + * FastAPI apporte des fonctionnalités supplémentaires à Starlette. Des fonctionnalités dont vous avez presque toujours besoin lors de la création d'une API, comme la validation des données et la sérialisation. En l'utilisant, vous obtenez une documentation automatique « gratuitement » (la documentation automatique n'ajoute même pas de surcharge à l’exécution, elle est générée au démarrage). + * Si on n'utilisait pas FastAPI mais directement Starlette (ou un autre outil comme Sanic, Flask, Responder, etc.), il faudrait implémenter toute la validation des données et la sérialisation soi‑même. L'application finale aurait donc la même surcharge que si elle avait été construite avec FastAPI. Et dans de nombreux cas, cette validation des données et cette sérialisation représentent la plus grande quantité de code écrite dans les applications. + * De ce fait, en utilisant FastAPI on minimise le temps de développement, les bugs, le nombre de lignes de code, et on obtient probablement les mêmes performances (voire de meilleures performances) que l'on aurait pu avoir sans ce framework (car il aurait fallu tout implémenter dans votre code). + * Si on compare FastAPI, il faut le comparer à d'autres frameworks d’application web (ou ensembles d'outils) qui fournissent la validation des données, la sérialisation et la documentation, comme Flask-apispec, NestJS, Molten, etc. Des frameworks avec validation des données, sérialisation et documentation automatiques intégrées. diff --git a/docs/fr/docs/deployment/docker.md b/docs/fr/docs/deployment/docker.md index 05b597a2d..ec30f9607 100644 --- a/docs/fr/docs/deployment/docker.md +++ b/docs/fr/docs/deployment/docker.md @@ -1,75 +1,150 @@ -# Déployer avec Docker +# FastAPI dans des conteneurs - Docker { #fastapi-in-containers-docker } -Dans cette section, vous verrez des instructions et des liens vers des guides pour savoir comment : +Lors du déploiement d'applications FastAPI, une approche courante consiste à construire une **image de conteneur Linux**. C'est généralement fait avec **Docker**. Vous pouvez ensuite déployer cette image de conteneur de plusieurs façons possibles. -* Faire de votre application **FastAPI** une image/conteneur Docker avec une performance maximale. En environ **5 min**. -* (Optionnellement) comprendre ce que vous, en tant que développeur, devez savoir sur HTTPS. -* Configurer un cluster en mode Docker Swarm avec HTTPS automatique, même sur un simple serveur à 5 dollars US/mois. En environ **20 min**. -* Générer et déployer une application **FastAPI** complète, en utilisant votre cluster Docker Swarm, avec HTTPS, etc. En environ **10 min**. - -Vous pouvez utiliser **Docker** pour le déploiement. Il présente plusieurs avantages comme la sécurité, la réplicabilité, la simplicité de développement, etc. - -Si vous utilisez Docker, vous pouvez utiliser l'image Docker officielle : - -## tiangolo/uvicorn-gunicorn-fastapi - -Cette image est dotée d'un mécanisme d'"auto-tuning", de sorte qu'il vous suffit d'ajouter votre code pour obtenir automatiquement des performances très élevées. Et sans faire de sacrifices. - -Mais vous pouvez toujours changer et mettre à jour toutes les configurations avec des variables d'environnement ou des fichiers de configuration. +L'utilisation de conteneurs Linux présente plusieurs avantages, notamment la **sécurité**, la **réplicabilité**, la **simplicité**, entre autres. /// tip | Astuce -Pour voir toutes les configurations et options, rendez-vous sur la page de l'image Docker : tiangolo/uvicorn-gunicorn-fastapi. +Vous êtes pressé et vous connaissez déjà tout ça ? Allez directement au [`Dockerfile` ci-dessous 👇](#build-a-docker-image-for-fastapi). /// -## Créer un `Dockerfile` - -* Allez dans le répertoire de votre projet. -* Créez un `Dockerfile` avec : +
+Aperçu du Dockerfile 👀 ```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 +FROM python:3.9 -COPY ./app /app +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app + +CMD ["fastapi", "run", "app/main.py", "--port", "80"] + +# Si vous exécutez derrière un proxy comme Nginx ou Traefik, ajoutez --proxy-headers +# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"] ``` -### Applications plus larges +
-Si vous avez suivi la section sur la création d' [Applications avec plusieurs fichiers](../tutorial/bigger-applications.md){.internal-link target=_blank}, votre `Dockerfile` pourrait ressembler à ceci : +## Qu'est-ce qu'un conteneur { #what-is-a-container } -```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 +Les conteneurs (principalement les conteneurs Linux) sont un moyen très **léger** d'empaqueter des applications, y compris toutes leurs dépendances et les fichiers nécessaires, tout en les isolant des autres conteneurs (autres applications ou composants) dans le même système. -COPY ./app /app/app +Les conteneurs Linux s'exécutent en utilisant le même noyau Linux que l'hôte (machine, machine virtuelle, serveur cloud, etc.). Cela signifie simplement qu'ils sont très légers (comparés à des machines virtuelles complètes émulant un système d'exploitation entier). + +Ainsi, les conteneurs consomment **peu de ressources**, une quantité comparable à l'exécution directe des processus (alors qu'une machine virtuelle consommerait beaucoup plus). + +Les conteneurs ont également leurs propres processus d'exécution **isolés** (généralement un seul processus), leur système de fichiers et leur réseau, ce qui simplifie le déploiement, la sécurité, le développement, etc. + +## Qu'est-ce qu'une image de conteneur { #what-is-a-container-image } + +Un **conteneur** s'exécute à partir d'une **image de conteneur**. + +Une image de conteneur est une version **statique** de tous les fichiers, des variables d'environnement et de la commande/le programme par défaut devant être présents dans un conteneur. Ici, **statique** signifie que l'**image** du conteneur ne s'exécute pas, elle n'est pas en cours d'exécution, ce ne sont que les fichiers et métadonnées empaquetés. + +Par opposition à une « **image de conteneur** » qui correspond aux contenus statiques stockés, un « **conteneur** » fait normalement référence à l'instance en cours d'exécution, la chose qui est **exécutée**. + +Lorsque le **conteneur** est démarré et en cours d'exécution (démarré à partir d'une **image de conteneur**), il peut créer ou modifier des fichiers, des variables d'environnement, etc. Ces changements n'existeront que dans ce conteneur, mais ne persisteront pas dans l'image de conteneur sous-jacente (ils ne seront pas enregistrés sur le disque). + +Une image de conteneur est comparable au **programme** et à ses contenus, par exemple `python` et un fichier `main.py`. + +Et le **conteneur** lui-même (par opposition à l'**image de conteneur**) est l'instance en cours d'exécution réelle de l'image, comparable à un **processus**. En fait, un conteneur ne fonctionne que lorsqu'il a un **processus en cours d'exécution** (et normalement, il s'agit d'un seul processus). Le conteneur s'arrête lorsqu'aucun processus n'y est en cours d'exécution. + +## Images de conteneur { #container-images } + +Docker a été l'un des principaux outils pour créer et gérer des **images de conteneur** et des **conteneurs**. + +Et il existe un Docker Hub public avec des **images de conteneur officielles** pré-construites pour de nombreux outils, environnements, bases de données et applications. + +Par exemple, il existe une image Python officielle. + +Et il existe beaucoup d'autres images pour différentes choses comme des bases de données, par exemple : + +* PostgreSQL +* MySQL +* MongoDB +* Redis, etc. + +En utilisant une image de conteneur pré-construite, il est très facile de **combiner** et d'utiliser différents outils. Par exemple, pour essayer une nouvelle base de données. Dans la plupart des cas, vous pouvez utiliser les **images officielles** et simplement les configurer avec des variables d'environnement. + +Ainsi, dans de nombreux cas, vous pouvez apprendre les conteneurs et Docker et réutiliser ces connaissances avec de nombreux outils et composants différents. + +Vous exécuteriez donc **plusieurs conteneurs** avec des éléments différents, comme une base de données, une application Python, un serveur web avec une application frontend React, et les connecter entre eux via leur réseau interne. + +Tous les systèmes de gestion de conteneurs (comme Docker ou Kubernetes) disposent de ces fonctionnalités réseau intégrées. + +## Conteneurs et processus { #containers-and-processes } + +Une **image de conteneur** inclut normalement dans ses métadonnées le programme/la commande par défaut à exécuter lorsque le **conteneur** est démarré et les paramètres à transmettre à ce programme. Très similaire à ce que vous utiliseriez en ligne de commande. + +Lorsqu'un **conteneur** est démarré, il exécutera cette commande/ce programme (bien que vous puissiez la/le remplacer et faire exécuter une autre commande/un autre programme). + +Un conteneur fonctionne tant que le **processus principal** (commande ou programme) est en cours d'exécution. + +Un conteneur a normalement un **seul processus**, mais il est aussi possible de démarrer des sous-processus à partir du processus principal, et ainsi vous aurez **plusieurs processus** dans le même conteneur. + +Mais il n'est pas possible d'avoir un conteneur en cours d'exécution sans **au moins un processus en cours**. Si le processus principal s'arrête, le conteneur s'arrête. + +## Construire une image Docker pour FastAPI { #build-a-docker-image-for-fastapi } + +Très bien, construisons quelque chose maintenant ! 🚀 + +Je vais vous montrer comment construire une **image Docker** pour FastAPI **à partir de zéro**, basée sur l'image **officielle Python**. + +C'est ce que vous voudrez faire dans **la plupart des cas**, par exemple : + +* Utiliser **Kubernetes** ou des outils similaires +* Exécuter sur un **Raspberry Pi** +* Utiliser un service cloud qui exécuterait une image de conteneur pour vous, etc. + +### Dépendances des paquets { #package-requirements } + +Vous aurez normalement les **dépendances des paquets** de votre application dans un fichier. + +Cela dépendra principalement de l'outil que vous utilisez pour **installer** ces dépendances. + +La manière la plus courante consiste à avoir un fichier `requirements.txt` avec les noms des paquets et leurs versions, un par ligne. + +Vous utiliserez bien sûr les mêmes idées que vous avez lues dans [À propos des versions de FastAPI](versions.md){.internal-link target=_blank} pour définir les plages de versions. + +Par exemple, votre `requirements.txt` pourrait ressembler à : + +``` +fastapi[standard]>=0.113.0,<0.114.0 +pydantic>=2.7.0,<3.0.0 ``` -### Raspberry Pi et autres architectures +Et vous installerez normalement ces dépendances de paquets avec `pip`, par exemple : -Si vous utilisez Docker sur un Raspberry Pi (qui a un processeur ARM) ou toute autre architecture, vous pouvez créer un `Dockerfile` à partir de zéro, basé sur une image de base Python (qui est multi-architecture) et utiliser Uvicorn seul. +
-Dans ce cas, votre `Dockerfile` pourrait ressembler à ceci : - -```Dockerfile -FROM python:3.7 - -RUN pip install fastapi uvicorn - -EXPOSE 80 - -COPY ./app /app - -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic ``` -## Créer le code **FastAPI**. +
-* Créer un répertoire `app` et y entrer. -* Créez un fichier `main.py` avec : +/// info + +Il existe d'autres formats et outils pour définir et installer des dépendances de paquets. + +/// + +### Créer le code **FastAPI** { #create-the-fastapi-code } + +* Créez un répertoire `app` et entrez dedans. +* Créez un fichier vide `__init__.py`. +* Créez un fichier `main.py` avec : ```Python -from typing import Optional - from fastapi import FastAPI app = FastAPI() @@ -81,22 +156,168 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` -* Vous devriez maintenant avoir une structure de répertoire telle que : +### Dockerfile { #dockerfile } + +Maintenant, dans le même répertoire de projet, créez un fichier `Dockerfile` avec : + +```{ .dockerfile .annotate } +# (1)! +FROM python:3.9 + +# (2)! +WORKDIR /code + +# (3)! +COPY ./requirements.txt /code/requirements.txt + +# (4)! +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (5)! +COPY ./app /code/app + +# (6)! +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +1. Démarrer à partir de l'image de base Python officielle. + +2. Définir le répertoire de travail courant sur `/code`. + + C'est là que nous placerons le fichier `requirements.txt` et le répertoire `app`. + +3. Copier le fichier des dépendances vers le répertoire `/code`. + + Copier **uniquement** le fichier des dépendances en premier, pas le reste du code. + + Comme ce fichier **ne change pas souvent**, Docker le détectera et utilisera le **cache** pour cette étape, ce qui activera le cache pour l'étape suivante aussi. + +4. Installer les dépendances listées dans le fichier des dépendances. + + L'option `--no-cache-dir` indique à `pip` de ne pas enregistrer localement les paquets téléchargés, car cela ne sert que si `pip` devait être relancé pour installer les mêmes paquets, mais ce n'est pas le cas lorsque l'on travaille avec des conteneurs. + + /// note | Remarque + + Le `--no-cache-dir` concerne uniquement `pip`, cela n'a rien à voir avec Docker ou les conteneurs. + + /// + + L'option `--upgrade` indique à `pip` de mettre à niveau les paquets s'ils sont déjà installés. + + Comme l'étape précédente de copie du fichier peut être détectée par le **cache Docker**, cette étape **utilisera également le cache Docker** lorsqu'il est disponible. + + L'utilisation du cache à cette étape vous **fera gagner** beaucoup de **temps** lors de la reconstruction de l'image encore et encore pendant le développement, au lieu de **télécharger et installer** toutes les dépendances **à chaque fois**. + +5. Copier le répertoire `./app` dans le répertoire `/code`. + + Comme cela contient tout le code qui est ce qui **change le plus fréquemment**, le **cache** Docker ne sera pas facilement utilisé pour cette étape ou pour les **étapes suivantes**. + + Il est donc important de placer cela **vers la fin** du `Dockerfile`, pour optimiser les temps de construction de l'image de conteneur. + +6. Définir la **commande** pour utiliser `fastapi run`, qui utilise Uvicorn sous le capot. + + `CMD` prend une liste de chaînes, chacune de ces chaînes correspond à ce que vous taperiez en ligne de commande séparé par des espaces. + + Cette commande sera exécutée à partir du **répertoire de travail courant**, le même répertoire `/code` que vous avez défini plus haut avec `WORKDIR /code`. + +/// tip | Astuce + +Passez en revue ce que fait chaque ligne en cliquant sur chaque bulle numérotée dans le code. 👆 + +/// + +/// warning | Alertes + +Vous devez vous assurer d'utiliser **toujours** la **forme exec** de l'instruction `CMD`, comme expliqué ci-dessous. + +/// + +#### Utiliser `CMD` - Forme Exec { #use-cmd-exec-form } + +L'instruction Docker `CMD` peut être écrite sous deux formes : + +✅ Forme **Exec** : + +```Dockerfile +# ✅ À faire +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +⛔️ Forme **Shell** : + +```Dockerfile +# ⛔️ À ne pas faire +CMD fastapi run app/main.py --port 80 +``` + +Assurez-vous d'utiliser toujours la forme **exec** pour garantir que FastAPI peut s'arrêter proprement et que les [événements de cycle de vie](../advanced/events.md){.internal-link target=_blank} sont déclenchés. + +Vous pouvez en lire davantage dans la documentation Docker sur les formes shell et exec. + +Cela peut être très visible lors de l'utilisation de `docker compose`. Voir cette section de la FAQ Docker Compose pour plus de détails techniques : Pourquoi mes services mettent-ils 10 secondes à se recréer ou à s'arrêter ?. + +#### Structure du répertoire { #directory-structure } + +Vous devriez maintenant avoir une structure de répertoire comme : ``` . ├── app +│   ├── __init__.py │ └── main.py -└── Dockerfile +├── Dockerfile +└── requirements.txt ``` -## Construire l'image Docker +#### Derrière un proxy de terminaison TLS { #behind-a-tls-termination-proxy } -* Allez dans le répertoire du projet (dans lequel se trouve votre `Dockerfile`, contenant votre répertoire `app`). +Si vous exécutez votre conteneur derrière un proxy de terminaison TLS (load balancer) comme Nginx ou Traefik, ajoutez l'option `--proxy-headers`, cela indiquera à Uvicorn (via la CLI FastAPI) de faire confiance aux en-têtes envoyés par ce proxy lui indiquant que l'application s'exécute derrière HTTPS, etc. + +```Dockerfile +CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] +``` + +#### Cache Docker { #docker-cache } + +Il y a une astuce importante dans ce `Dockerfile`, nous copions d'abord **le fichier des dépendances seul**, pas le reste du code. Laissez-moi vous expliquer pourquoi. + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +Docker et d'autres outils **construisent** ces images de conteneur **de manière incrémentale**, en ajoutant **une couche au-dessus de l'autre**, en commençant par le haut du `Dockerfile` et en ajoutant tous les fichiers créés par chacune des instructions du `Dockerfile`. + +Docker et des outils similaires utilisent également un **cache interne** lors de la construction de l'image : si un fichier n'a pas changé depuis la dernière construction de l'image de conteneur, alors il va **réutiliser la même couche** créée la dernière fois, au lieu de recopier le fichier et créer une nouvelle couche à partir de zéro. + +Éviter simplement la copie des fichiers n'améliore pas nécessairement les choses de manière significative, mais comme il a utilisé le cache pour cette étape, il peut **utiliser le cache pour l'étape suivante**. Par exemple, il peut utiliser le cache pour l'instruction qui installe les dépendances avec : + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +Le fichier des dépendances **ne changera pas fréquemment**. Ainsi, en copiant uniquement ce fichier, Docker pourra **utiliser le cache** pour cette étape. + +Et ensuite, Docker pourra **utiliser le cache pour l'étape suivante** qui télécharge et installe ces dépendances. Et c'est là que nous **gagnons beaucoup de temps**. ✨ ... et évitons l'ennui en attendant. 😪😆 + +Télécharger et installer les dépendances de paquets **peut prendre des minutes**, mais utiliser le **cache** ne **prendra que quelques secondes** au plus. + +Et comme vous reconstruirez l'image de conteneur encore et encore pendant le développement pour vérifier que vos modifications de code fonctionnent, cela vous fera gagner beaucoup de temps cumulé. + +Ensuite, vers la fin du `Dockerfile`, nous copions tout le code. Comme c'est ce qui **change le plus fréquemment**, nous le plaçons vers la fin, car presque toujours, tout ce qui suit cette étape ne pourra pas utiliser le cache. + +```Dockerfile +COPY ./app /code/app +``` + +### Construire l'image Docker { #build-the-docker-image } + +Maintenant que tous les fichiers sont en place, construisons l'image de conteneur. + +* Allez dans le répertoire du projet (là où se trouve votre `Dockerfile`, contenant votre répertoire `app`). * Construisez votre image FastAPI :
@@ -109,9 +330,17 @@ $ docker build -t myimage .
-## Démarrer le conteneur Docker +/// tip | Astuce -* Exécutez un conteneur basé sur votre image : +Remarquez le `.` à la fin, équivalent à `./`, il indique à Docker le répertoire à utiliser pour construire l'image de conteneur. + +Dans ce cas, c'est le même répertoire courant (`.`). + +/// + +### Démarrer le conteneur Docker { #start-the-docker-container } + +* Exécutez un conteneur basé sur votre image :
@@ -121,65 +350,269 @@ $ docker run -d --name mycontainer -p 80:80 myimage
-Vous disposez maintenant d'un serveur FastAPI optimisé dans un conteneur Docker. Configuré automatiquement pour votre -serveur actuel (et le nombre de cœurs du CPU). +## Vérifier { #check-it } -## Vérifier +Vous devriez pouvoir le vérifier via l'URL de votre conteneur Docker, par exemple : http://192.168.99.100/items/5?q=somequery ou http://127.0.0.1/items/5?q=somequery (ou équivalent, en utilisant votre hôte Docker). -Vous devriez pouvoir accéder à votre application via l'URL de votre conteneur Docker, par exemple : http://192.168.99.100/items/5?q=somequery ou http://127.0.0.1/items/5?q=somequery (ou équivalent, en utilisant votre hôte Docker). - -Vous verrez quelque chose comme : +Vous verrez quelque chose comme : ```JSON {"item_id": 5, "q": "somequery"} ``` -## Documentation interactive de l'API +## Documentation interactive de l'API { #interactive-api-docs } -Vous pouvez maintenant visiter http://192.168.99.100/docs ou http://127.0.0.1/docs (ou équivalent, en utilisant votre hôte Docker). +Vous pouvez maintenant aller sur http://192.168.99.100/docs ou http://127.0.0.1/docs (ou équivalent, en utilisant votre hôte Docker). -Vous verrez la documentation interactive automatique de l'API (fournie par Swagger UI) : +Vous verrez la documentation interactive automatique de l'API (fournie par Swagger UI) : ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -## Documentation de l'API alternative +## Documentation alternative de l'API { #alternative-api-docs } -Et vous pouvez également aller sur http://192.168.99.100/redoc ou http://127.0.0.1/redoc (ou équivalent, en utilisant votre hôte Docker). +Et vous pouvez aussi aller sur http://192.168.99.100/redoc ou http://127.0.0.1/redoc (ou équivalent, en utilisant votre hôte Docker). -Vous verrez la documentation automatique alternative (fournie par ReDoc) : +Vous verrez la documentation automatique alternative (fournie par ReDoc) : ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Traefik +## Construire une image Docker avec un FastAPI mono-fichier { #build-a-docker-image-with-a-single-file-fastapi } -Traefik est un reverse proxy/load balancer -haute performance. Il peut faire office de "Proxy de terminaison TLS" (entre autres fonctionnalités). +Si votre FastAPI est un seul fichier, par exemple `main.py` sans répertoire `./app`, votre structure de fichiers pourrait ressembler à ceci : -Il est intégré à Let's Encrypt. Ainsi, il peut gérer toutes les parties HTTPS, y compris l'acquisition et le renouvellement des certificats. +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` -Il est également intégré à Docker. Ainsi, vous pouvez déclarer vos domaines dans les configurations de chaque application et faire en sorte qu'elles lisent ces configurations, génèrent les certificats HTTPS et servent via HTTPS à votre application automatiquement, sans nécessiter aucune modification de leurs configurations. +Vous n'auriez alors qu'à changer les chemins correspondants pour copier le fichier dans le `Dockerfile` : + +```{ .dockerfile .annotate hl_lines="10 13" } +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (1)! +COPY ./main.py /code/ + +# (2)! +CMD ["fastapi", "run", "main.py", "--port", "80"] +``` + +1. Copier le fichier `main.py` directement dans le répertoire `/code` (sans répertoire `./app`). + +2. Utiliser `fastapi run` pour servir votre application dans le fichier unique `main.py`. + +Lorsque vous passez le fichier à `fastapi run`, il détectera automatiquement qu'il s'agit d'un fichier unique et non d'un package et saura comment l'importer et servir votre application FastAPI. 😎 + +## Concepts de déploiement { #deployment-concepts } + +Parlons à nouveau de certains des mêmes [Concepts de déploiement](concepts.md){.internal-link target=_blank} en termes de conteneurs. + +Les conteneurs sont principalement un outil pour simplifier le processus de **construction et de déploiement** d'une application, mais ils n'imposent pas une approche particulière pour gérer ces **concepts de déploiement**, et il existe plusieurs stratégies possibles. + +La **bonne nouvelle**, c'est qu'avec chaque stratégie différente, il existe un moyen de couvrir tous les concepts de déploiement. 🎉 + +Passons en revue ces **concepts de déploiement** en termes de conteneurs : + +* HTTPS +* Exécution au démarrage +* Redémarrages +* Réplication (le nombre de processus en cours d'exécution) +* Mémoire +* Étapes préalables au démarrage + +## HTTPS { #https } + +Si l'on se concentre uniquement sur l'**image de conteneur** pour une application FastAPI (et plus tard sur le **conteneur** en cours d'exécution), HTTPS serait normalement géré **à l'extérieur** par un autre outil. + +Cela pourrait être un autre conteneur, par exemple avec Traefik, gérant **HTTPS** et l'acquisition **automatique** des **certificats**. + +/// tip | Astuce + +Traefik s'intègre avec Docker, Kubernetes, et d'autres, donc il est très facile de configurer HTTPS pour vos conteneurs avec lui. + +/// + +Alternativement, HTTPS pourrait être géré par un fournisseur cloud comme l'un de leurs services (tout en exécutant l'application dans un conteneur). + +## Exécution au démarrage et redémarrages { #running-on-startup-and-restarts } + +Il y a normalement un autre outil chargé de **démarrer et exécuter** votre conteneur. + +Cela pourrait être **Docker** directement, **Docker Compose**, **Kubernetes**, un **service cloud**, etc. + +Dans la plupart (ou toutes) des situations, il existe une option simple pour activer l'exécution du conteneur au démarrage et activer les redémarrages en cas d'échec. Par exemple, dans Docker, c'est l'option de ligne de commande `--restart`. + +Sans utiliser de conteneurs, faire en sorte que les applications s'exécutent au démarrage et avec redémarrages peut être fastidieux et difficile. Mais en **travaillant avec des conteneurs**, dans la plupart des cas, cette fonctionnalité est incluse par défaut. ✨ + +## Réplication - Nombre de processus { #replication-number-of-processes } + +Si vous avez un cluster de machines avec **Kubernetes**, Docker Swarm Mode, Nomad, ou un autre système complexe similaire pour gérer des conteneurs distribués sur plusieurs machines, alors vous voudrez probablement **gérer la réplication** au **niveau du cluster** plutôt que d'utiliser un **gestionnaire de processus** (comme Uvicorn avec workers) dans chaque conteneur. + +L'un de ces systèmes de gestion de conteneurs distribués comme Kubernetes dispose normalement d'une manière intégrée de gérer la **réplication des conteneurs** tout en supportant l'**équilibrage de charge** des requêtes entrantes. Le tout au **niveau du cluster**. + +Dans ces cas, vous voudrez probablement construire une **image Docker à partir de zéro** comme [expliqué ci-dessus](#dockerfile), en installant vos dépendances et en exécutant **un seul processus Uvicorn** au lieu d'utiliser plusieurs workers Uvicorn. + +### Équilibreur de charge { #load-balancer } + +Lors de l'utilisation de conteneurs, vous aurez normalement un composant **à l'écoute sur le port principal**. Cela pourrait être un autre conteneur qui est également un **proxy de terminaison TLS** pour gérer **HTTPS** ou un outil similaire. + +Comme ce composant prend la **charge** des requêtes et la distribue entre les workers de façon (espérons-le) **équilibrée**, on l'appelle également communément un **équilibreur de charge**. + +/// tip | Astuce + +Le même composant de **proxy de terminaison TLS** utilisé pour HTTPS sera probablement aussi un **équilibreur de charge**. + +/// + +Et en travaillant avec des conteneurs, le même système que vous utilisez pour les démarrer et les gérer dispose déjà d'outils internes pour transmettre la **communication réseau** (par ex. les requêtes HTTP) depuis cet **équilibreur de charge** (qui peut aussi être un **proxy de terminaison TLS**) vers le ou les conteneurs avec votre application. + +### Un équilibreur de charge - Plusieurs conteneurs worker { #one-load-balancer-multiple-worker-containers } + +Lorsque vous travaillez avec **Kubernetes** ou des systèmes de gestion de conteneurs distribués similaires, l'utilisation de leurs mécanismes réseau internes permet au **seul équilibreur de charge** à l'écoute sur le **port** principal de transmettre la communication (les requêtes) vers potentiellement **plusieurs conteneurs** exécutant votre application. + +Chacun de ces conteneurs exécutant votre application aura normalement **un seul processus** (par ex. un processus Uvicorn exécutant votre application FastAPI). Ils seront tous des **conteneurs identiques**, exécutant la même chose, mais chacun avec son propre processus, sa mémoire, etc. De cette façon, vous profiterez de la **parallélisation** sur **différents cœurs** du CPU, voire sur **différentes machines**. + +Et le système de conteneurs distribués avec l'**équilibreur de charge** **distribuera les requêtes** à chacun des conteneurs exécutant votre application **à tour de rôle**. Ainsi, chaque requête pourrait être traitée par l'un des multiples **conteneurs répliqués** exécutant votre application. + +Et normalement cet **équilibreur de charge** pourra gérer des requêtes qui vont vers *d'autres* applications dans votre cluster (par ex. vers un autre domaine, ou sous un autre préfixe de chemin d'URL), et transmettra cette communication aux bons conteneurs pour *cette autre* application s'exécutant dans votre cluster. + +### Un processus par conteneur { #one-process-per-container } + +Dans ce type de scénario, vous voudrez probablement avoir **un seul processus (Uvicorn) par conteneur**, puisque vous gérez déjà la réplication au niveau du cluster. + +Donc, dans ce cas, vous **ne voudrez pas** avoir plusieurs workers dans le conteneur, par exemple avec l'option de ligne de commande `--workers`. Vous voudrez avoir **un seul processus Uvicorn** par conteneur (mais probablement plusieurs conteneurs). + +Avoir un autre gestionnaire de processus à l'intérieur du conteneur (comme ce serait le cas avec plusieurs workers) n'ajouterait que de la **complexité inutile** que vous gérez très probablement déjà avec votre système de cluster. + +### Conteneurs avec plusieurs processus et cas particuliers { #containers-with-multiple-processes-and-special-cases } + +Bien sûr, il existe des **cas particuliers** où vous pourriez vouloir avoir **un conteneur** avec plusieurs **processus worker Uvicorn** à l'intérieur. + +Dans ces cas, vous pouvez utiliser l'option de ligne de commande `--workers` pour définir le nombre de workers que vous souhaitez exécuter : + +```{ .dockerfile .annotate } +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app + +# (1)! +CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"] +``` + +1. Ici, nous utilisons l'option de ligne de commande `--workers` pour définir le nombre de workers à 4. + +Voici quelques exemples où cela pourrait avoir du sens : + +#### Une application simple { #a-simple-app } + +Vous pourriez vouloir un gestionnaire de processus dans le conteneur si votre application est **suffisamment simple** pour s'exécuter sur un **seul serveur**, pas un cluster. + +#### Docker Compose { #docker-compose } + +Vous pourriez déployer sur un **seul serveur** (pas un cluster) avec **Docker Compose**, donc vous n'auriez pas un moyen simple de gérer la réplication des conteneurs (avec Docker Compose) tout en préservant le réseau partagé et l'**équilibrage de charge**. + +Vous pourriez alors vouloir avoir **un seul conteneur** avec un **gestionnaire de processus** qui démarre **plusieurs processus worker** à l'intérieur. --- -Avec ces informations et ces outils, passez à la section suivante pour tout combiner. +L'idée principale est que **rien** de tout cela ne sont des **règles gravées dans la pierre** que vous devez suivre aveuglément. Vous pouvez utiliser ces idées pour **évaluer votre propre cas d'usage** et décider de la meilleure approche pour votre système, en vérifiant comment gérer les concepts suivants : -## Cluster en mode Docker Swarm avec Traefik et HTTPS +* Sécurité - HTTPS +* Exécution au démarrage +* Redémarrages +* Réplication (le nombre de processus en cours d'exécution) +* Mémoire +* Étapes préalables au démarrage -Vous pouvez avoir un cluster en mode Docker Swarm configuré en quelques minutes (environ 20 min) avec un processus Traefik principal gérant HTTPS (y compris l'acquisition et le renouvellement des certificats). +## Mémoire { #memory } -En utilisant le mode Docker Swarm, vous pouvez commencer par un "cluster" d'une seule machine (il peut même s'agir -d'un serveur à 5 USD/mois) et ensuite vous pouvez vous développer autant que vous le souhaitez en ajoutant d'autres serveurs. +Si vous exécutez **un seul processus par conteneur**, vous aurez une quantité de mémoire consommée plus ou moins bien définie, stable et limitée par chacun de ces conteneurs (plus d'un s'ils sont répliqués). -Pour configurer un cluster en mode Docker Swarm avec Traefik et la gestion de HTTPS, suivez ce guide : +Vous pouvez alors définir ces mêmes limites et exigences de mémoire dans vos configurations pour votre système de gestion de conteneurs (par exemple dans **Kubernetes**). De cette façon, il pourra **répliquer les conteneurs** sur les **machines disponibles** en tenant compte de la quantité de mémoire dont ils ont besoin et de la quantité disponible sur les machines du cluster. -### Docker Swarm Mode et Traefik pour un cluster HTTPS +Si votre application est **simple**, cela ne sera probablement **pas un problème**, et vous n'aurez peut-être pas besoin de spécifier des limites de mémoire strictes. Mais si vous **utilisez beaucoup de mémoire** (par exemple avec des modèles de **machine learning**), vous devez vérifier combien de mémoire vous consommez et ajuster le **nombre de conteneurs** qui s'exécutent sur **chaque machine** (et peut-être ajouter plus de machines à votre cluster). -### Déployer une application FastAPI +Si vous exécutez **plusieurs processus par conteneur**, vous devez vous assurer que le nombre de processus démarrés ne **consomme pas plus de mémoire** que ce qui est disponible. -La façon la plus simple de tout mettre en place, serait d'utiliser les [**Générateurs de projet FastAPI**](../project-generation.md){.internal-link target=_blank}. +## Étapes préalables au démarrage et conteneurs { #previous-steps-before-starting-and-containers } -Le génerateur de projet adéquat est conçu pour être intégré à ce cluster Docker Swarm avec Traefik et HTTPS décrit ci-dessus. +Si vous utilisez des conteneurs (par ex. Docker, Kubernetes), alors il existe deux approches principales que vous pouvez utiliser. -Vous pouvez générer un projet en 2 min environ. +### Plusieurs conteneurs { #multiple-containers } -Le projet généré a des instructions pour le déployer et le faire prend 2 min de plus. +Si vous avez **plusieurs conteneurs**, probablement chacun exécutant un **seul processus** (par exemple, dans un cluster **Kubernetes**), alors vous voudrez probablement avoir un **conteneur séparé** effectuant le travail des **étapes préalables** dans un seul conteneur, exécutant un seul processus, **avant** d'exécuter les conteneurs worker répliqués. + +/// info + +Si vous utilisez Kubernetes, ce sera probablement un Init Container. + +/// + +Si, dans votre cas d'usage, il n'y a pas de problème à exécuter ces étapes préalables **plusieurs fois en parallèle** (par exemple si vous n'exécutez pas de migrations de base de données, mais vérifiez simplement si la base de données est prête), alors vous pourriez aussi simplement les mettre dans chaque conteneur juste avant de démarrer le processus principal. + +### Un seul conteneur { #single-container } + +Si vous avez une configuration simple, avec **un seul conteneur** qui démarre ensuite plusieurs **processus worker** (ou un seul processus aussi), vous pouvez alors exécuter ces étapes préalables dans le même conteneur, juste avant de démarrer le processus avec l'application. + +### Image Docker de base { #base-docker-image } + +Il existait une image Docker officielle FastAPI : tiangolo/uvicorn-gunicorn-fastapi. Mais elle est désormais dépréciée. ⛔️ + +Vous ne devriez probablement **pas** utiliser cette image Docker de base (ni aucune autre similaire). + +Si vous utilisez **Kubernetes** (ou autres) et que vous définissez déjà la **réplication** au niveau du cluster, avec plusieurs **conteneurs**. Dans ces cas, il est préférable de **construire une image à partir de zéro** comme décrit ci-dessus : [Construire une image Docker pour FastAPI](#build-a-docker-image-for-fastapi). + +Et si vous devez avoir plusieurs workers, vous pouvez simplement utiliser l'option de ligne de commande `--workers`. + +/// note | Détails techniques + +L'image Docker a été créée à une époque où Uvicorn ne supportait pas la gestion et le redémarrage des workers morts, il fallait donc utiliser Gunicorn avec Uvicorn, ce qui ajoutait pas mal de complexité, uniquement pour que Gunicorn gère et redémarre les processus worker Uvicorn. + +Mais maintenant qu'Uvicorn (et la commande `fastapi`) supporte l'usage de `--workers`, il n'y a plus de raison d'utiliser une image Docker de base au lieu de construire la vôtre (c'est à peu près la même quantité de code 😅). + +/// + +## Déployer l'image de conteneur { #deploy-the-container-image } + +Après avoir une image de conteneur (Docker), il existe plusieurs façons de la déployer. + +Par exemple : + +* Avec **Docker Compose** sur un seul serveur +* Avec un cluster **Kubernetes** +* Avec un cluster Docker Swarm Mode +* Avec un autre outil comme Nomad +* Avec un service cloud qui prend votre image de conteneur et la déploie + +## Image Docker avec `uv` { #docker-image-with-uv } + +Si vous utilisez uv pour installer et gérer votre projet, vous pouvez suivre leur guide Docker pour uv. + +## Récapitulatif { #recap } + +Avec les systèmes de conteneurs (par ex. avec **Docker** et **Kubernetes**), il devient assez simple de gérer tous les **concepts de déploiement** : + +* HTTPS +* Exécution au démarrage +* Redémarrages +* Réplication (le nombre de processus en cours d'exécution) +* Mémoire +* Étapes préalables au démarrage + +Dans la plupart des cas, vous ne voudrez probablement pas utiliser d'image de base, et au contraire **construire une image de conteneur à partir de zéro** basée sur l'image Docker Python officielle. + +En prenant soin de l'**ordre** des instructions dans le `Dockerfile` et du **cache Docker**, vous pouvez **minimiser les temps de construction**, maximiser votre productivité (et éviter l'ennui). 😎 diff --git a/docs/fr/docs/deployment/https.md b/docs/fr/docs/deployment/https.md index 3f7068ff0..74d38cdb9 100644 --- a/docs/fr/docs/deployment/https.md +++ b/docs/fr/docs/deployment/https.md @@ -1,56 +1,231 @@ -# À propos de HTTPS +# À propos de HTTPS { #about-https } -Il est facile de penser que HTTPS peut simplement être "activé" ou non. +Il est facile de supposer que HTTPS est quelque chose qui est simplement « activé » ou non. Mais c'est beaucoup plus complexe que cela. -/// tip +/// tip | Astuce -Si vous êtes pressé ou si cela ne vous intéresse pas, passez aux sections suivantes pour obtenir des instructions étape par étape afin de tout configurer avec différentes techniques. +Si vous êtes pressé ou si cela ne vous intéresse pas, continuez avec les sections suivantes pour obtenir des instructions étape par étape afin de tout configurer avec différentes techniques. /// -Pour apprendre les bases du HTTPS, du point de vue d'un utilisateur, consultez https://howhttps.works/. +Pour apprendre les bases du HTTPS, du point de vue d'un utilisateur, consultez https://howhttps.works/. Maintenant, du point de vue d'un développeur, voici plusieurs choses à avoir en tête en pensant au HTTPS : -* Pour le HTTPS, le serveur a besoin de "certificats" générés par une tierce partie. - * Ces certificats sont en fait acquis auprès de la tierce partie, et non "générés". -* Les certificats ont une durée de vie. - * Ils expirent. - * Puis ils doivent être renouvelés et acquis à nouveau auprès de la tierce partie. -* Le cryptage de la connexion se fait au niveau du protocole TCP. - * C'est une couche en dessous de HTTP. - * Donc, le certificat et le traitement du cryptage sont faits avant HTTP. -* TCP ne connaît pas les "domaines", seulement les adresses IP. - * L'information sur le domaine spécifique demandé se trouve dans les données HTTP. -* Les certificats HTTPS "certifient" un certain domaine, mais le protocole et le cryptage se font au niveau TCP, avant de savoir quel domaine est traité. -* Par défaut, cela signifie que vous ne pouvez avoir qu'un seul certificat HTTPS par adresse IP. - * Quelle que soit la taille de votre serveur ou la taille de chacune des applications qu'il contient. - * Il existe cependant une solution à ce problème. -* Il existe une extension du protocole TLS (celui qui gère le cryptage au niveau TCP, avant HTTP) appelée SNI (indication du nom du serveur). - * Cette extension SNI permet à un seul serveur (avec une seule adresse IP) d'avoir plusieurs certificats HTTPS et de servir plusieurs domaines/applications HTTPS. - * Pour que cela fonctionne, un seul composant (programme) fonctionnant sur le serveur, écoutant sur l'adresse IP publique, doit avoir tous les certificats HTTPS du serveur. -* Après avoir obtenu une connexion sécurisée, le protocole de communication est toujours HTTP. - * Le contenu est crypté, même s'il est envoyé avec le protocole HTTP. +* Pour le HTTPS, **le serveur** doit **disposer de « certificats »** générés par une **tierce partie**. + * Ces certificats sont en réalité **acquis** auprès de la tierce partie, et non « générés ». +* Les certificats ont une **durée de vie**. + * Ils **expirent**. + * Puis ils doivent être **renouvelés**, **acquis à nouveau** auprès de la tierce partie. +* Le cryptage de la connexion se fait au **niveau TCP**. + * C'est une couche **en dessous de HTTP**. + * Donc, la gestion du **certificat et du cryptage** est effectuée **avant HTTP**. +* **TCP ne connaît pas les « domaines »**. Il ne connaît que les adresses IP. + * L'information sur le **domaine spécifique** demandé se trouve dans les **données HTTP**. +* Les **certificats HTTPS** « certifient » un **certain domaine**, mais le protocole et le cryptage se font au niveau TCP, **avant de savoir** quel domaine est traité. +* **Par défaut**, cela signifie que vous ne pouvez avoir qu'**un seul certificat HTTPS par adresse IP**. + * Quelle que soit la taille de votre serveur ou la petitesse de chacune des applications qu'il contient. + * Il existe cependant une **solution** à ce problème. +* Il existe une **extension** du protocole **TLS** (celui qui gère le cryptage au niveau TCP, avant HTTP) appelée **SNI**. + * Cette extension SNI permet à un seul serveur (avec une **seule adresse IP**) d'avoir **plusieurs certificats HTTPS** et de servir **plusieurs domaines/applications HTTPS**. + * Pour que cela fonctionne, un **seul** composant (programme) fonctionnant sur le serveur, écoutant sur l'**adresse IP publique**, doit avoir **tous les certificats HTTPS** du serveur. +* **Après** l'établissement d'une connexion sécurisée, le protocole de communication est **toujours HTTP**. + * Le contenu est **crypté**, même s'il est envoyé avec le **protocole HTTP**. -Il est courant d'avoir un seul programme/serveur HTTP fonctionnant sur le serveur (la machine, l'hôte, etc.) et -gérant toutes les parties HTTPS : envoyer les requêtes HTTP décryptées à l'application HTTP réelle fonctionnant sur -le même serveur (dans ce cas, l'application **FastAPI**), prendre la réponse HTTP de l'application, la crypter en utilisant le certificat approprié et la renvoyer au client en utilisant HTTPS. Ce serveur est souvent appelé un Proxy de terminaison TLS. +Il est courant d'avoir **un seul programme/serveur HTTP** fonctionnant sur le serveur (la machine, l'hôte, etc.) et **gérant toutes les parties HTTPS** : recevoir les **requêtes HTTPS chiffrées**, envoyer les **requêtes HTTP déchiffrées** à l'application HTTP réelle fonctionnant sur le même serveur (l'application **FastAPI**, dans ce cas), prendre la **réponse HTTP** de l'application, la **chiffrer** en utilisant le **certificat HTTPS** approprié et la renvoyer au client en utilisant **HTTPS**. Ce serveur est souvent appelé un **Proxy de terminaison TLS**. -## Let's Encrypt +Parmi les options que vous pourriez utiliser comme Proxy de terminaison TLS : -Avant Let's Encrypt, ces certificats HTTPS étaient vendus par des tiers de confiance. +* Traefik (qui peut également gérer les renouvellements de certificats) +* Caddy (qui peut également gérer les renouvellements de certificats) +* Nginx +* HAProxy -Le processus d'acquisition d'un de ces certificats était auparavant lourd, nécessitait pas mal de paperasses et les certificats étaient assez chers. +## Let's Encrypt { #lets-encrypt } -Mais ensuite, Let's Encrypt a été créé. +Avant Let's Encrypt, ces **certificats HTTPS** étaient vendus par des tiers de confiance. -Il s'agit d'un projet de la Fondation Linux. Il fournit des certificats HTTPS gratuitement. De manière automatisée. Ces certificats utilisent toutes les sécurités cryptographiques standard et ont une durée de vie courte (environ 3 mois), de sorte que la sécurité est en fait meilleure en raison de leur durée de vie réduite. +Le processus d'acquisition de l'un de ces certificats était auparavant lourd, nécessitait pas mal de paperasses et les certificats étaient assez chers. + +Mais ensuite, **Let's Encrypt** a été créé. + +Il s'agit d'un projet de la Fondation Linux. Il fournit **des certificats HTTPS gratuitement**, de manière automatisée. Ces certificats utilisent toutes les sécurités cryptographiques standard et ont une durée de vie courte (environ 3 mois), de sorte que la **sécurité est en fait meilleure** en raison de leur durée de vie réduite. Les domaines sont vérifiés de manière sécurisée et les certificats sont générés automatiquement. Cela permet également d'automatiser le renouvellement de ces certificats. -L'idée est d'automatiser l'acquisition et le renouvellement de ces certificats, afin que vous puissiez disposer d'un HTTPS sécurisé, gratuitement et pour toujours. +L'idée est d'automatiser l'acquisition et le renouvellement de ces certificats, afin que vous puissiez disposer d'un **HTTPS sécurisé, gratuitement et pour toujours**. + +## HTTPS pour les développeurs { #https-for-developers } + +Voici un exemple de ce à quoi pourrait ressembler une API HTTPS, étape par étape, en portant principalement attention aux idées importantes pour les développeurs. + +### Nom de domaine { #domain-name } + +Tout commencerait probablement par le fait que vous **acquériez** un **nom de domaine**. Ensuite, vous le configureriez dans un serveur DNS (possiblement le même que votre fournisseur cloud). + +Vous obtiendriez probablement un serveur cloud (une machine virtuelle) ou quelque chose de similaire, et il aurait une adresse IP **publique** fixe. + +Dans le ou les serveurs DNS, vous configureriez un enregistrement (un « `A record` ») pour faire pointer **votre domaine** vers l'**adresse IP publique de votre serveur**. + +Vous feriez probablement cela une seule fois, la première fois, lors de la mise en place de l'ensemble. + +/// tip | Astuce + +Cette partie relative au nom de domaine intervient bien avant HTTPS, mais comme tout dépend du domaine et de l'adresse IP, il vaut la peine de la mentionner ici. + +/// + +### DNS { #dns } + +Concentrons-nous maintenant sur toutes les parties réellement liées à HTTPS. + +D'abord, le navigateur vérifierait auprès des **serveurs DNS** quelle est l'**IP du domaine**, dans ce cas, `someapp.example.com`. + +Les serveurs DNS indiqueraient au navigateur d'utiliser une **adresse IP** spécifique. Ce serait l'adresse IP publique utilisée par votre serveur, celle que vous avez configurée dans les serveurs DNS. + + + +### Début de la négociation TLS (Handshake) { #tls-handshake-start } + +Le navigateur communiquerait ensuite avec cette adresse IP sur le **port 443** (le port HTTPS). + +La première partie de la communication consiste simplement à établir la connexion entre le client et le serveur et à décider des clés cryptographiques qu'ils utiliseront, etc. + + + +Cette interaction entre le client et le serveur pour établir la connexion TLS s'appelle la **négociation TLS (TLS handshake)**. + +### TLS avec l'extension SNI { #tls-with-sni-extension } + +**Un seul processus** sur le serveur peut écouter sur un **port** spécifique d'une **adresse IP** spécifique. Il pourrait y avoir d'autres processus écoutant sur d'autres ports de la même adresse IP, mais un seul pour chaque combinaison d'adresse IP et de port. + +TLS (HTTPS) utilise par défaut le port spécifique `443`. C'est donc le port dont nous aurions besoin. + +Comme un seul processus peut écouter sur ce port, le processus qui le ferait serait le **Proxy de terminaison TLS**. + +Le Proxy de terminaison TLS aurait accès à un ou plusieurs **certificats TLS** (certificats HTTPS). + +En utilisant l'**extension SNI** mentionnée plus haut, le Proxy de terminaison TLS vérifierait lequel des certificats TLS (HTTPS) disponibles il devrait utiliser pour cette connexion, en choisissant celui qui correspond au domaine attendu par le client. + +Dans ce cas, il utiliserait le certificat pour `someapp.example.com`. + + + +Le client **fait déjà confiance** à l'entité qui a généré ce certificat TLS (dans ce cas Let's Encrypt, mais nous y reviendrons plus tard), il peut donc **vérifier** que le certificat est valide. + +Ensuite, en utilisant le certificat, le client et le Proxy de terminaison TLS **décident comment chiffrer** le reste de la **communication TCP**. Cela termine la partie **négociation TLS**. + +Après cela, le client et le serveur disposent d'une **connexion TCP chiffrée**, c'est ce que fournit TLS. Ils peuvent alors utiliser cette connexion pour démarrer la **communication HTTP** proprement dite. + +Et c'est ce qu'est **HTTPS** : c'est simplement du **HTTP** à l'intérieur d'une **connexion TLS sécurisée** au lieu d'une connexion TCP pure (non chiffrée). + +/// tip | Astuce + +Remarquez que le cryptage de la communication se produit au **niveau TCP**, pas au niveau HTTP. + +/// + +### Requête HTTPS { #https-request } + +Maintenant que le client et le serveur (spécifiquement le navigateur et le Proxy de terminaison TLS) ont une **connexion TCP chiffrée**, ils peuvent démarrer la **communication HTTP**. + +Ainsi, le client envoie une **requête HTTPS**. Ce n'est qu'une requête HTTP à travers une connexion TLS chiffrée. + + + +### Déchiffrer la requête { #decrypt-the-request } + +Le Proxy de terminaison TLS utiliserait le chiffrement convenu pour **déchiffrer la requête**, et transmettrait la **requête HTTP en clair (déchiffrée)** au processus exécutant l'application (par exemple un processus avec Uvicorn exécutant l'application FastAPI). + + + +### Réponse HTTP { #http-response } + +L'application traiterait la requête et enverrait une **réponse HTTP en clair (non chiffrée)** au Proxy de terminaison TLS. + + + +### Réponse HTTPS { #https-response } + +Le Proxy de terminaison TLS **chiffrerait ensuite la réponse** en utilisant la cryptographie convenue auparavant (qui a commencé avec le certificat pour `someapp.example.com`), et la renverrait au navigateur. + +Ensuite, le navigateur vérifierait que la réponse est valide et chiffrée avec la bonne clé cryptographique, etc. Il **déchiffrerait la réponse** et la traiterait. + + + +Le client (navigateur) saura que la réponse provient du bon serveur parce qu'elle utilise la cryptographie convenue auparavant à l'aide du **certificat HTTPS**. + +### Applications multiples { #multiple-applications } + +Sur le même serveur (ou les mêmes serveurs), il pourrait y avoir **plusieurs applications**, par exemple d'autres programmes d'API ou une base de données. + +Un seul processus peut gérer l'adresse IP et le port spécifiques (le Proxy de terminaison TLS dans notre exemple), mais les autres applications/processus peuvent également s'exécuter sur le ou les serveurs, tant qu'ils n'essaient pas d'utiliser la même **combinaison d'adresse IP publique et de port**. + + + +De cette façon, le Proxy de terminaison TLS pourrait gérer HTTPS et les certificats pour **plusieurs domaines**, pour plusieurs applications, puis transmettre les requêtes à la bonne application dans chaque cas. + +### Renouvellement des certificats { #certificate-renewal } + +À un moment donné dans le futur, chaque certificat **expirerait** (environ 3 mois après son acquisition). + +Ensuite, il y aurait un autre programme (dans certains cas c'est un autre programme, dans d'autres cas cela pourrait être le même Proxy de terminaison TLS) qui communiquerait avec Let's Encrypt et renouvellerait le ou les certificats. + + + +Les **certificats TLS** sont **associés à un nom de domaine**, pas à une adresse IP. + +Ainsi, pour renouveler les certificats, le programme de renouvellement doit **prouver** à l'autorité (Let's Encrypt) qu'il **« possède » et contrôle ce domaine**. + +Pour ce faire, et pour s'adapter aux différents besoins des applications, il existe plusieurs façons de procéder. Parmi les plus courantes : + +* **Modifier certains enregistrements DNS**. + * Pour cela, le programme de renouvellement doit prendre en charge les API du fournisseur DNS ; ainsi, selon le fournisseur DNS que vous utilisez, cela peut être ou non une option. +* **S'exécuter comme un serveur** (au moins pendant le processus d'acquisition du certificat) sur l'adresse IP publique associée au domaine. + * Comme nous l'avons dit plus haut, un seul processus peut écouter sur une adresse IP et un port spécifiques. + * C'est l'une des raisons pour lesquelles il est très utile que le même Proxy de terminaison TLS prenne également en charge le processus de renouvellement des certificats. + * Sinon, vous pourriez avoir à arrêter le Proxy de terminaison TLS momentanément, démarrer le programme de renouvellement pour acquérir les certificats, puis les configurer avec le Proxy de terminaison TLS, et ensuite redémarrer le Proxy de terminaison TLS. Ce n'est pas idéal, car votre/vos application(s) ne seront pas disponibles pendant le temps où le Proxy de terminaison TLS est arrêté. + +Tout ce processus de renouvellement, tout en continuant à servir l'application, est l'une des principales raisons pour lesquelles vous voudriez avoir un **système séparé pour gérer HTTPS** avec un Proxy de terminaison TLS, au lieu d'utiliser directement les certificats TLS avec le serveur d'application (par exemple Uvicorn). + +## En-têtes Proxy Forwarded { #proxy-forwarded-headers } + +Lorsque vous utilisez un proxy pour gérer HTTPS, votre **serveur d'application** (par exemple Uvicorn via FastAPI CLI) ne connaît rien du processus HTTPS, il communique en HTTP en clair avec le **Proxy de terminaison TLS**. + +Ce **proxy** définirait normalement certains en-têtes HTTP à la volée avant de transmettre la requête au **serveur d'application**, pour informer le serveur d'application que la requête est **transmise** par le proxy. + +/// note | Détails techniques + +Les en-têtes du proxy sont : + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +Néanmoins, comme le **serveur d'application** ne sait pas qu'il se trouve derrière un **proxy** de confiance, par défaut, il ne ferait pas confiance à ces en-têtes. + +Mais vous pouvez configurer le **serveur d'application** pour qu'il fasse confiance aux en-têtes transmis (*forwarded*) envoyés par le **proxy**. Si vous utilisez FastAPI CLI, vous pouvez utiliser l'*option CLI* `--forwarded-allow-ips` pour lui indiquer à partir de quelles IP il doit faire confiance à ces en-têtes transmis. + +Par exemple, si le **serveur d'application** ne reçoit des communications que du **proxy** de confiance, vous pouvez définir `--forwarded-allow-ips="*"` pour lui faire faire confiance à toutes les IP entrantes, puisqu'il ne recevra des requêtes que depuis l'IP utilisée par le **proxy**. + +De cette façon, l'application sera en mesure de savoir quelle est sa propre URL publique, si elle utilise HTTPS, le domaine, etc. + +Cela serait utile, par exemple, pour gérer correctement les redirections. + +/// tip | Astuce + +Vous pouvez en savoir plus dans la documentation [Derrière un proxy - Activer les en-têtes transmis par le proxy](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} + +/// + +## Récapitulatif { #recap } + +Disposer de **HTTPS** est très important, et assez **critique** dans la plupart des cas. La majeure partie de l'effort que vous, en tant que développeur, devez fournir autour de HTTPS consiste simplement à **comprendre ces concepts** et leur fonctionnement. + +Mais une fois que vous connaissez les informations de base sur **HTTPS pour les développeurs**, vous pouvez facilement combiner et configurer différents outils pour vous aider à tout gérer simplement. + +Dans certains des prochains chapitres, je vous montrerai plusieurs exemples concrets de configuration de **HTTPS** pour des applications **FastAPI**. 🔒 diff --git a/docs/fr/docs/deployment/index.md b/docs/fr/docs/deployment/index.md index e2014afe9..3b08e9af7 100644 --- a/docs/fr/docs/deployment/index.md +++ b/docs/fr/docs/deployment/index.md @@ -1,8 +1,8 @@ -# Déploiement +# Déploiement { #deployment } Le déploiement d'une application **FastAPI** est relativement simple. -## Que signifie le déploiement +## Que signifie le déploiement { #what-does-deployment-mean } **Déployer** une application signifie effectuer les étapes nécessaires pour la rendre **disponible pour les utilisateurs**. @@ -14,7 +14,7 @@ l'application efficacement et sans interruption ni problème. Ceci contraste avec les étapes de **développement**, où vous êtes constamment en train de modifier le code, de le casser et de le réparer, d'arrêter et de redémarrer le serveur de développement, _etc._ -## Stratégies de déploiement +## Stratégies de déploiement { #deployment-strategies } Il existe plusieurs façons de procéder, en fonction de votre cas d'utilisation spécifique et des outils que vous utilisez. @@ -22,6 +22,8 @@ utilisez. Vous pouvez **déployer un serveur** vous-même en utilisant une combinaison d'outils, vous pouvez utiliser un **service cloud** qui fait une partie du travail pour vous, ou encore d'autres options possibles. +Par exemple, nous, l'équipe derrière FastAPI, avons créé **FastAPI Cloud**, pour rendre le déploiement d'applications FastAPI dans le cloud aussi fluide que possible, avec la même expérience développeur que lorsque vous travaillez avec FastAPI. + Je vais vous montrer certains des principaux concepts que vous devriez probablement avoir à l'esprit lors du déploiement d'une application **FastAPI** (bien que la plupart de ces concepts s'appliquent à tout autre type d'application web). diff --git a/docs/fr/docs/deployment/versions.md b/docs/fr/docs/deployment/versions.md index 9d84274e2..81794428f 100644 --- a/docs/fr/docs/deployment/versions.md +++ b/docs/fr/docs/deployment/versions.md @@ -1,101 +1,93 @@ -# À propos des versions de FastAPI +# À propos des versions de FastAPI { #about-fastapi-versions } -**FastAPI** est déjà utilisé en production dans de nombreuses applications et systèmes. Et la couverture de test est maintenue à 100 %. Mais son développement est toujours aussi rapide. +**FastAPI** est déjà utilisé en production dans de nombreuses applications et de nombreux systèmes. Et la couverture de tests est maintenue à 100 %. Mais son développement avance toujours rapidement. -De nouvelles fonctionnalités sont ajoutées fréquemment, des bogues sont corrigés régulièrement et le code est -amélioré continuellement. +De nouvelles fonctionnalités sont ajoutées fréquemment, des bogues sont corrigés régulièrement et le code s'améliore continuellement. -C'est pourquoi les versions actuelles sont toujours `0.x.x`, cela reflète que chaque version peut potentiellement -recevoir des changements non rétrocompatibles. Cela suit les conventions de versionnage sémantique. +C'est pourquoi les versions actuelles sont toujours `0.x.x`, cela reflète que chaque version pourrait potentiellement comporter des changements non rétrocompatibles. Cela suit les conventions de versionnage sémantique. Vous pouvez créer des applications de production avec **FastAPI** dès maintenant (et vous le faites probablement depuis un certain temps), vous devez juste vous assurer que vous utilisez une version qui fonctionne correctement avec le reste de votre code. -## Épinglez votre version de `fastapi` +## Épingler votre version de `fastapi` { #pin-your-fastapi-version } -Tout d'abord il faut "épingler" la version de **FastAPI** que vous utilisez à la dernière version dont vous savez -qu'elle fonctionne correctement pour votre application. +La première chose que vous devez faire est « épingler » la version de **FastAPI** que vous utilisez à la dernière version spécifique dont vous savez qu’elle fonctionne correctement pour votre application. -Par exemple, disons que vous utilisez la version `0.45.0` dans votre application. +Par exemple, disons que vous utilisez la version `0.112.0` dans votre application. -Si vous utilisez un fichier `requirements.txt`, vous pouvez spécifier la version avec : +Si vous utilisez un fichier `requirements.txt`, vous pouvez spécifier la version avec : ```txt -fastapi==0.45.0 +fastapi[standard]==0.112.0 ``` -ce qui signifierait que vous utiliseriez exactement la version `0.45.0`. +ce qui signifierait que vous utiliseriez exactement la version `0.112.0`. -Ou vous pourriez aussi l'épingler avec : +Ou vous pourriez aussi l'épingler avec : ```txt -fastapi>=0.45.0,<0.46.0 +fastapi[standard]>=0.112.0,<0.113.0 ``` -cela signifierait que vous utiliseriez les versions `0.45.0` ou supérieures, mais inférieures à `0.46.0`, par exemple, une version `0.45.2` serait toujours acceptée. +cela signifierait que vous utiliseriez les versions `0.112.0` ou supérieures, mais inférieures à `0.113.0`, par exemple, une version `0.112.2` serait toujours acceptée. -Si vous utilisez un autre outil pour gérer vos installations, comme Poetry, Pipenv, ou autres, ils ont tous un moyen que vous pouvez utiliser pour définir des versions spécifiques pour vos paquets. +Si vous utilisez un autre outil pour gérer vos installations, comme `uv`, Poetry, Pipenv, ou autres, ils ont tous un moyen que vous pouvez utiliser pour définir des versions spécifiques pour vos paquets. -## Versions disponibles +## Versions disponibles { #available-versions } Vous pouvez consulter les versions disponibles (par exemple, pour vérifier quelle est la dernière version en date) dans les [Notes de version](../release-notes.md){.internal-link target=_blank}. -## À propos des versions +## À propos des versions { #about-versions } -Suivant les conventions de versionnage sémantique, toute version inférieure à `1.0.0` peut potentiellement ajouter -des changements non rétrocompatibles. +Suivant les conventions de versionnage sémantique, toute version inférieure à `1.0.0` peut potentiellement ajouter des changements non rétrocompatibles. -FastAPI suit également la convention que tout changement de version "PATCH" est pour des corrections de bogues et -des changements rétrocompatibles. +FastAPI suit également la convention selon laquelle tout changement de version « PATCH » concerne des corrections de bogues et des changements rétrocompatibles. /// tip | Astuce -Le "PATCH" est le dernier chiffre, par exemple, dans `0.2.3`, la version PATCH est `3`. +Le « PATCH » est le dernier chiffre, par exemple, dans `0.2.3`, la version PATCH est `3`. /// -Donc, vous devriez être capable d'épingler une version comme suit : +Donc, vous devriez être en mesure d'épingler une version comme suit : ```txt fastapi>=0.45.0,<0.46.0 ``` -Les changements non rétrocompatibles et les nouvelles fonctionnalités sont ajoutés dans les versions "MINOR". +Les changements non rétrocompatibles et les nouvelles fonctionnalités sont ajoutés dans les versions « MINOR ». /// tip | Astuce -Le "MINOR" est le numéro au milieu, par exemple, dans `0.2.3`, la version MINOR est `2`. +Le « MINOR » est le numéro au milieu, par exemple, dans `0.2.3`, la version MINOR est `2`. /// -## Mise à jour des versions FastAPI +## Mettre à niveau les versions de FastAPI { #upgrading-the-fastapi-versions } -Vous devriez tester votre application. +Vous devez ajouter des tests pour votre application. -Avec **FastAPI** c'est très facile (merci à Starlette), consultez la documentation : [Testing](../tutorial/testing.md){.internal-link target=_blank} +Avec **FastAPI** c'est très facile (merci à Starlette), consultez les documents : [Tests](../tutorial/testing.md){.internal-link target=_blank} -Après avoir effectué des tests, vous pouvez mettre à jour la version **FastAPI** vers une version plus récente, et vous assurer que tout votre code fonctionne correctement en exécutant vos tests. +Après avoir des tests, vous pouvez mettre à niveau la version de **FastAPI** vers une version plus récente et vous assurer que tout votre code fonctionne correctement en exécutant vos tests. -Si tout fonctionne, ou après avoir fait les changements nécessaires, et que tous vos tests passent, vous pouvez -épingler votre version de `fastapi` à cette nouvelle version récente. +Si tout fonctionne, ou après avoir effectué les changements nécessaires, et que tous vos tests passent, vous pouvez alors épingler votre `fastapi` à cette nouvelle version récente. -## À propos de Starlette +## À propos de Starlette { #about-starlette } -Vous ne devriez pas épingler la version de `starlette`. +Vous ne devez pas épingler la version de `starlette`. Différentes versions de **FastAPI** utiliseront une version spécifique plus récente de Starlette. Ainsi, vous pouvez simplement laisser **FastAPI** utiliser la bonne version de Starlette. -## À propos de Pydantic +## À propos de Pydantic { #about-pydantic } -Pydantic inclut des tests pour **FastAPI** avec ses propres tests, ainsi les nouvelles versions de Pydantic (au-dessus -de `1.0.0`) sont toujours compatibles avec **FastAPI**. +Pydantic inclut les tests pour **FastAPI** avec ses propres tests, ainsi les nouvelles versions de Pydantic (au-dessus de `1.0.0`) sont toujours compatibles avec FastAPI. -Vous pouvez épingler Pydantic à toute version supérieure à `1.0.0` qui fonctionne pour vous et inférieure à `2.0.0`. +Vous pouvez épingler Pydantic à toute version supérieure à `1.0.0` qui fonctionne pour vous. -Par exemple : +Par exemple : ```txt -pydantic>=1.2.0,<2.0.0 +pydantic>=2.7.0,<3.0.0 ``` diff --git a/docs/fr/docs/index.md b/docs/fr/docs/index.md index 99ea8dda1..2aeaa1c69 100644 --- a/docs/fr/docs/index.md +++ b/docs/fr/docs/index.md @@ -1,11 +1,11 @@ -# FastAPI +# FastAPI { #fastapi }

- FastAPI + FastAPI

Framework FastAPI, haute performance, facile à apprendre, rapide à coder, prêt pour la production @@ -27,7 +27,7 @@ --- -**Documentation** : https://fastapi.tiangolo.com +**Documentation** : https://fastapi.tiangolo.com/fr **Code Source** : https://github.com/fastapi/fastapi @@ -37,128 +37,130 @@ FastAPI est un framework web moderne et rapide (haute performance) pour la créa Les principales fonctionnalités sont : -* **Rapidité** : De très hautes performances, au niveau de **NodeJS** et **Go** (grâce à Starlette et Pydantic). [L'un des frameworks Python les plus rapides](#performance). -* **Rapide à coder** : Augmente la vitesse de développement des fonctionnalités d'environ 200 % à 300 %. * -* **Moins de bugs** : Réduit d'environ 40 % les erreurs induites par le développeur. * -* **Intuitif** : Excellente compatibilité avec les IDE. Complétion complète. Moins de temps passé à déboguer. -* **Facile** : Conçu pour être facile à utiliser et à apprendre. Moins de temps passé à lire la documentation. -* **Concis** : Diminue la duplication de code. De nombreuses fonctionnalités liées à la déclaration de chaque paramètre. Moins de bugs. -* **Robuste** : Obtenez un code prêt pour la production. Avec une documentation interactive automatique. -* **Basé sur des normes** : Basé sur (et entièrement compatible avec) les standards ouverts pour les APIs : OpenAPI (précédemment connu sous le nom de Swagger) et JSON Schema. +* **Rapide** : très hautes performances, au niveau de **NodeJS** et **Go** (grâce à Starlette et Pydantic). [L'un des frameworks Python les plus rapides](#performance). +* **Rapide à coder** : augmente la vitesse de développement des fonctionnalités d'environ 200 % à 300 %. * +* **Moins de bugs** : réduit d'environ 40 % les erreurs induites par le développeur. * +* **Intuitif** : excellente compatibilité avec les éditeurs. Autocomplétion partout. Moins de temps passé à déboguer. +* **Facile** : conçu pour être facile à utiliser et à apprendre. Moins de temps passé à lire les documents. +* **Concis** : diminue la duplication de code. Plusieurs fonctionnalités à partir de chaque déclaration de paramètre. Moins de bugs. +* **Robuste** : obtenez un code prêt pour la production. Avec une documentation interactive automatique. +* **Basé sur des normes** : basé sur (et entièrement compatible avec) les standards ouverts pour les APIs : OpenAPI (précédemment connu sous le nom de Swagger) et JSON Schema. * estimation basée sur des tests d'une équipe de développement interne, construisant des applications de production. -## Sponsors +## Sponsors { #sponsors } -{% if sponsors %} +### Sponsor clé de voûte { #keystone-sponsor } + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Sponsors Or et Argent { #gold-and-silver-sponsors } + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} -Other sponsors +Autres sponsors -## Opinions +## Opinions { #opinions } -"_[...] J'utilise beaucoup **FastAPI** ces derniers temps. [...] Je prévois de l'utiliser dans mon équipe pour tous les **services de ML chez Microsoft**. Certains d'entre eux seront intégrés dans le coeur de **Windows** et dans certains produits **Office**._" +« _[...] J'utilise beaucoup **FastAPI** ces derniers temps. [...] Je prévois de l'utiliser dans mon équipe pour tous les **services de ML chez Microsoft**. Certains d'entre eux sont intégrés au cœur de **Windows** et à certains produits **Office**._ »

Kabir Khan - Microsoft (ref)
--- -"_Nous avons adopté la bibliothèque **FastAPI** pour créer un serveur **REST** qui peut être interrogé pour obtenir des **prédictions**. [pour Ludwig]_" +« _Nous avons adopté la bibliothèque **FastAPI** pour créer un serveur **REST** qui peut être interrogé pour obtenir des **prédictions**. [pour Ludwig]_ » -
Piero Molino, Yaroslav Dudin et Sai Sumanth Miryala - Uber (ref)
+
Piero Molino, Yaroslav Dudin, et Sai Sumanth Miryala - Uber (ref)
--- -"_**Netflix** a le plaisir d'annoncer la sortie en open-source de notre framework d'orchestration de **gestion de crise** : **Dispatch** ! [construit avec **FastAPI**]_" +« _**Netflix** est heureux d'annoncer la publication en open source de notre framework d'orchestration de **gestion de crise** : **Dispatch** ! [construit avec **FastAPI**]_ »
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
--- -"_Je suis très enthousiaste à propos de **FastAPI**. C'est un bonheur !_" +« _Je suis plus qu'enthousiaste à propos de **FastAPI**. C'est tellement fun !_ » -
Brian Okken - Auteur du podcast Python Bytes (ref)
+
Brian Okken - Animateur du podcast Python Bytes (ref)
--- -"_Honnêtement, ce que vous avez construit a l'air super solide et élégant. A bien des égards, c'est comme ça que je voulais que **Hug** soit - c'est vraiment inspirant de voir quelqu'un construire ça._" +« _Honnêtement, ce que vous avez construit a l'air super solide et soigné. À bien des égards, c'est ce que je voulais que **Hug** soit — c'est vraiment inspirant de voir quelqu'un construire ça._ » -
Timothy Crosley - Créateur de Hug (ref)
+
Timothy Crosley - Créateur de Hug (ref)
--- -"_Si vous cherchez à apprendre un **framework moderne** pour créer des APIs REST, regardez **FastAPI** [...] C'est rapide, facile à utiliser et à apprendre [...]_" +« _Si vous cherchez à apprendre un **framework moderne** pour créer des APIs REST, regardez **FastAPI** [...] C'est rapide, facile à utiliser et facile à apprendre [...]_ » -"_Nous sommes passés à **FastAPI** pour nos **APIs** [...] Je pense que vous l'aimerez [...]_" +« _Nous sommes passés à **FastAPI** pour nos **APIs** [...] Je pense que vous l'aimerez [...]_ »
Ines Montani - Matthew Honnibal - Fondateurs de Explosion AI - Créateurs de spaCy (ref) - (ref)
--- -"_Si quelqu'un cherche à construire une API Python de production, je recommande vivement **FastAPI**. Il est **bien conçu**, **simple à utiliser** et **très évolutif**. Il est devenu un **composant clé** dans notre stratégie de développement API first et il est à l'origine de nombreux automatismes et services tels que notre ingénieur virtuel TAC._" +« _Si quelqu'un cherche à construire une API Python de production, je recommande vivement **FastAPI**. Il est **magnifiquement conçu**, **simple à utiliser** et **hautement scalable**. Il est devenu un **composant clé** de notre stratégie de développement API-first et alimente de nombreuses automatisations et services tels que notre ingénieur TAC virtuel._ »
Deon Pillsbury - Cisco (ref)
--- -## **Typer**, le FastAPI des CLI +## Mini documentaire FastAPI { #fastapi-mini-documentary } + +Un mini documentaire FastAPI est sorti fin 2025, vous pouvez le regarder en ligne : + +FastAPI Mini Documentary + +## **Typer**, le FastAPI des CLIs { #typer-the-fastapi-of-clis } -Si vous souhaitez construire une application CLI utilisable dans un terminal au lieu d'une API web, regardez **Typer**. +Si vous construisez une application CLI à utiliser dans un terminal au lieu d'une API web, regardez **Typer**. -**Typer** est le petit frère de FastAPI. Et il est destiné à être le **FastAPI des CLI**. ⌨️ 🚀 +**Typer** est le petit frère de FastAPI. Et il est destiné à être le **FastAPI des CLIs**. ⌨️ 🚀 -## Prérequis +## Prérequis { #requirements } FastAPI repose sur les épaules de géants : * Starlette pour les parties web. * Pydantic pour les parties données. -## Installation +## Installation { #installation } + +Créez et activez un environnement virtuel puis installez FastAPI :
```console -$ pip install fastapi +$ pip install "fastapi[standard]" ---> 100% ```
-Vous aurez également besoin d'un serveur ASGI pour la production tel que Uvicorn ou Hypercorn. +**Remarque** : Vous devez vous assurer de mettre « fastapi[standard] » entre guillemets pour garantir que cela fonctionne dans tous les terminaux. -
+## Exemple { #example } -```console -$ pip install "uvicorn[standard]" +### Créer { #create-it } ----> 100% -``` - -
- -## Exemple - -### Créez - -* Créez un fichier `main.py` avec : +Créez un fichier `main.py` avec : ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -170,18 +172,16 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ```
-Ou utilisez async def ... +Ou utilisez async def... Si votre code utilise `async` / `await`, utilisez `async def` : -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -193,28 +193,41 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` -**Note** +**Remarque** : -Si vous n'êtes pas familier avec cette notion, consultez la section _"Vous êtes pressés ?"_ à propos de `async` et `await` dans la documentation. +Si vous ne savez pas, consultez la section « Vous êtes pressés ? » à propos de `async` et `await` dans la documentation.
-### Lancez +### Lancer { #run-it } Lancez le serveur avec :
```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. ``` @@ -222,34 +235,34 @@ INFO: Application startup complete.
-À propos de la commande uvicorn main:app --reload ... +À propos de la commande fastapi dev main.py... -La commande `uvicorn main:app` fait référence à : +La commande `fastapi dev` lit votre fichier `main.py`, détecte l'application **FastAPI** qu'il contient et lance un serveur avec Uvicorn. -* `main` : le fichier `main.py` (le "module" Python). -* `app` : l'objet créé à l'intérieur de `main.py` avec la ligne `app = FastAPI()`. -* `--reload` : fait redémarrer le serveur après des changements de code. À n'utiliser que pour le développement. +Par défaut, `fastapi dev` démarre avec le rechargement automatique activé pour le développement local. + +Vous pouvez en savoir plus dans la documentation de la CLI FastAPI.
-### Vérifiez +### Vérifier { #check-it } Ouvrez votre navigateur à l'adresse http://127.0.0.1:8000/items/5?q=somequery. -Vous obtenez alors cette réponse JSON : +Vous verrez la réponse JSON : ```JSON {"item_id": 5, "q": "somequery"} ``` -Vous venez de créer une API qui : +Vous avez déjà créé une API qui : -* Reçoit les requêtes HTTP pour les _chemins_ `/` et `/items/{item_id}`. -* Les deux _chemins_ acceptent des opérations `GET` (également connu sous le nom de _méthodes_ HTTP). -* Le _chemin_ `/items/{item_id}` a un _paramètre_ `item_id` qui doit être un `int`. -* Le _chemin_ `/items/{item_id}` a un _paramètre de requête_ optionnel `q` de type `str`. +* Reçoit des requêtes HTTP sur les _chemins_ `/` et `/items/{item_id}`. +* Les deux _chemins_ acceptent des opérations `GET` (également connues sous le nom de _méthodes_ HTTP). +* Le _chemin_ `/items/{item_id}` a un _paramètre de chemin_ `item_id` qui doit être un `int`. +* Le _chemin_ `/items/{item_id}` a un _paramètre de requête_ optionnel `q` de type `str`. -### Documentation API interactive +### Documentation API interactive { #interactive-api-docs } Maintenant, rendez-vous sur http://127.0.0.1:8000/docs. @@ -257,23 +270,21 @@ Vous verrez la documentation interactive automatique de l'API (fournie par http://127.0.0.1:8000/redoc. -Vous verrez la documentation interactive automatique de l'API (fournie par ReDoc) : +Vous verrez la documentation alternative automatique (fournie par ReDoc) : ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Exemple plus poussé +## Mettre à niveau l'exemple { #example-upgrade } -Maintenant, modifiez le fichier `main.py` pour recevoir le corps d'une requête `PUT`. +Modifiez maintenant le fichier `main.py` pour recevoir un corps depuis une requête `PUT`. -Déclarez ce corps en utilisant les types Python standards, grâce à Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union +Déclarez le corps en utilisant les types Python standard, grâce à Pydantic. +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -283,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -292,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -301,35 +312,35 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -Le serveur se recharge normalement automatiquement (car vous avez pensé à `--reload` dans la commande `uvicorn` ci-dessus). +Le serveur `fastapi dev` devrait se recharger automatiquement. -### Plus loin avec la documentation API interactive +### Mettre à niveau la documentation API interactive { #interactive-api-docs-upgrade } Maintenant, rendez-vous sur http://127.0.0.1:8000/docs. -* La documentation interactive de l'API sera automatiquement mise à jour, y compris le nouveau corps de la requête : +* La documentation interactive de l'API sera automatiquement mise à jour, y compris le nouveau corps : ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* Cliquez sur le bouton "Try it out", il vous permet de renseigner les paramètres et d'interagir directement avec l'API : +* Cliquez sur le bouton « Try it out », il vous permet de renseigner les paramètres et d'interagir directement avec l'API : ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Cliquez ensuite sur le bouton "Execute", l'interface utilisateur communiquera avec votre API, enverra les paramètres, obtiendra les résultats et les affichera à l'écran : +* Cliquez ensuite sur le bouton « Execute », l'interface utilisateur communiquera avec votre API, enverra les paramètres, obtiendra les résultats et les affichera à l'écran : ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Plus loin avec la documentation API alternative +### Mettre à niveau la documentation API alternative { #alternative-api-docs-upgrade } Et maintenant, rendez-vous sur http://127.0.0.1:8000/redoc. -* La documentation alternative reflétera également le nouveau paramètre de requête et le nouveau corps : +* La documentation alternative reflètera également le nouveau paramètre de requête et le nouveau corps : ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### En résumé +### En résumé { #recap } -En résumé, vous déclarez **une fois** les types de paramètres, le corps de la requête, etc. en tant que paramètres de fonction. +En résumé, vous déclarez **une fois** les types de paramètres, le corps, etc. en tant que paramètres de fonction. Vous faites cela avec les types Python standard modernes. @@ -337,7 +348,7 @@ Vous n'avez pas à apprendre une nouvelle syntaxe, les méthodes ou les classes Juste du **Python** standard. -Par exemple, pour un `int`: +Par exemple, pour un `int` : ```Python item_id: int @@ -351,54 +362,54 @@ item: Item ... et avec cette déclaration unique, vous obtenez : -* Une assistance dans votre IDE, notamment : - * la complétion. +* Une assistance dans l'éditeur, notamment : + * l'autocomplétion. * la vérification des types. * La validation des données : * des erreurs automatiques et claires lorsque les données ne sont pas valides. - * une validation même pour les objets JSON profondément imbriqués. -* Une conversion des données d'entrée : venant du réseau et allant vers les données et types de Python, permettant de lire : - * le JSON. - * les paramètres du chemin. - * les paramètres de la requête. - * les cookies. - * les en-têtes. - * les formulaires. - * les fichiers. -* La conversion des données de sortie : conversion des données et types Python en données réseau (au format JSON), permettant de convertir : - * les types Python (`str`, `int`, `float`, `bool`, `list`, etc). - * les objets `datetime`. - * les objets `UUID`. - * les modèles de base de données. - * ... et beaucoup plus. -* La documentation API interactive automatique, avec 2 interfaces utilisateur au choix : + * une validation même pour les objets JSON profondément imbriqués. +* Conversion des données d'entrée : venant du réseau vers les données et types Python. Lecture depuis : + * JSON. + * Paramètres de chemin. + * Paramètres de requête. + * Cookies. + * En-têtes. + * Formulaires. + * Fichiers. +* Conversion des données de sortie : conversion des données et types Python en données réseau (au format JSON) : + * Conversion des types Python (`str`, `int`, `float`, `bool`, `list`, etc). + * Objets `datetime`. + * Objets `UUID`. + * Modèles de base de données. + * ... et bien plus. +* Documentation API interactive automatique, avec 2 interfaces utilisateur au choix : * Swagger UI. * ReDoc. --- -Pour revenir à l'exemple de code précédent, **FastAPI** permet de : +Pour revenir à l'exemple de code précédent, **FastAPI** va : -* Valider que `item_id` existe dans le chemin des requêtes `GET` et `PUT`. +* Valider la présence d'un `item_id` dans le chemin pour les requêtes `GET` et `PUT`. * Valider que `item_id` est de type `int` pour les requêtes `GET` et `PUT`. - * Si ce n'est pas le cas, le client voit une erreur utile et claire. -* Vérifier qu'il existe un paramètre de requête facultatif nommé `q` (comme dans `http://127.0.0.1:8000/items/foo?q=somequery`) pour les requêtes `GET`. - * Puisque le paramètre `q` est déclaré avec `= None`, il est facultatif. - * Sans le `None`, il serait nécessaire (comme l'est le corps de la requête dans le cas du `PUT`). -* Pour les requêtes `PUT` vers `/items/{item_id}`, de lire le corps en JSON : - * Vérifier qu'il a un attribut obligatoire `name` qui devrait être un `str`. - * Vérifier qu'il a un attribut obligatoire `prix` qui doit être un `float`. - * Vérifier qu'il a un attribut facultatif `is_offer`, qui devrait être un `bool`, s'il est présent. - * Tout cela fonctionnerait également pour les objets JSON profondément imbriqués. -* Convertir de et vers JSON automatiquement. -* Documenter tout avec OpenAPI, qui peut être utilisé par : - * Les systèmes de documentation interactifs. - * Les systèmes de génération automatique de code client, pour de nombreuses langues. + * Si ce n'est pas le cas, le client verra une erreur utile et claire. +* Vérifier s'il existe un paramètre de requête optionnel nommé `q` (comme dans `http://127.0.0.1:8000/items/foo?q=somequery`) pour les requêtes `GET`. + * Comme le paramètre `q` est déclaré avec `= None`, il est optionnel. + * Sans le `None`, il serait requis (comme l'est le corps dans le cas de `PUT`). +* Pour les requêtes `PUT` vers `/items/{item_id}`, lire le corps au format JSON : + * Vérifier qu'il a un attribut obligatoire `name` qui doit être un `str`. + * Vérifier qu'il a un attribut obligatoire `price` qui doit être un `float`. + * Vérifier qu'il a un attribut optionnel `is_offer`, qui doit être un `bool`, s'il est présent. + * Tout cela fonctionne également pour les objets JSON profondément imbriqués. +* Convertir automatiquement depuis et vers JSON. +* Tout documenter avec OpenAPI, qui peut être utilisé par : + * des systèmes de documentation interactive. + * des systèmes de génération automatique de clients, pour de nombreux langages. * Fournir directement 2 interfaces web de documentation interactive. --- -Nous n'avons fait qu'effleurer la surface, mais vous avez déjà une idée de la façon dont tout cela fonctionne. +Nous n'avons fait qu'effleurer la surface, mais vous avez déjà une idée de la façon dont tout fonctionne. Essayez de changer la ligne contenant : @@ -412,61 +423,137 @@ Essayez de changer la ligne contenant : ... "item_name": item.name ... ``` -... vers : +... à : ```Python ... "item_price": item.price ... ``` -... et voyez comment votre éditeur complétera automatiquement les attributs et connaîtra leurs types : +... et voyez comment votre éditeur complète automatiquement les attributs et connaît leurs types : -![compatibilité IDE](https://fastapi.tiangolo.com/img/vscode-completion.png) +![compatibilité éditeur](https://fastapi.tiangolo.com/img/vscode-completion.png) Pour un exemple plus complet comprenant plus de fonctionnalités, voir le Tutoriel - Guide utilisateur. -**Spoiler alert** : le tutoriel - guide utilisateur inclut : +**Alerte spoiler** : le tutoriel - guide utilisateur inclut : -* Déclaration de **paramètres** provenant d'autres endroits différents comme : **en-têtes.**, **cookies**, **champs de formulaire** et **fichiers**. -* L'utilisation de **contraintes de validation** comme `maximum_length` ou `regex`. -* Un **système d'injection de dépendance ** très puissant et facile à utiliser . -* Sécurité et authentification, y compris la prise en charge de **OAuth2** avec les **jetons JWT** et l'authentification **HTTP Basic**. -* Des techniques plus avancées (mais tout aussi faciles) pour déclarer les **modèles JSON profondément imbriqués** (grâce à Pydantic). -* Intégration de **GraphQL** avec Strawberry et d'autres bibliothèques. -* D'obtenir de nombreuses fonctionnalités supplémentaires (grâce à Starlette) comme : +* Déclaration de **paramètres** provenant d'autres emplacements comme : **en-têtes**, **cookies**, **champs de formulaire** et **fichiers**. +* Comment définir des **contraintes de validation** comme `maximum_length` ou `regex`. +* Un système **d'injection de dépendances** très puissant et facile à utiliser. +* Sécurité et authentification, y compris la prise en charge de **OAuth2** avec des **JWT tokens** et l'authentification **HTTP Basic**. +* Des techniques plus avancées (mais tout aussi faciles) pour déclarer des **modèles JSON profondément imbriqués** (grâce à Pydantic). +* Intégration **GraphQL** avec Strawberry et d'autres bibliothèques. +* De nombreuses fonctionnalités supplémentaires (grâce à Starlette) comme : * **WebSockets** - * de tester le code très facilement avec `requests` et `pytest` - * **CORS** + * des tests extrêmement faciles basés sur HTTPX et `pytest` + * **CORS** * **Cookie Sessions** * ... et plus encore. -## Performance +### Déployer votre application (optionnel) { #deploy-your-app-optional } -Les benchmarks TechEmpower indépendants montrent que les applications **FastAPI** s'exécutant sous Uvicorn sont parmi les frameworks existants en Python les plus rapides , juste derrière Starlette et Uvicorn (utilisés en interne par FastAPI). (*) +Vous pouvez, si vous le souhaitez, déployer votre application FastAPI sur FastAPI Cloud, allez vous inscrire sur la liste d'attente si ce n'est pas déjà fait. 🚀 + +Si vous avez déjà un compte **FastAPI Cloud** (nous vous avons invité depuis la liste d'attente 😉), vous pouvez déployer votre application avec une seule commande. + +Avant de déployer, assurez-vous d'être connecté : + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Puis déployez votre application : + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +C'est tout ! Vous pouvez maintenant accéder à votre application à cette URL. ✨ + +#### À propos de FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** est construit par le même auteur et la même équipe derrière **FastAPI**. + +Il simplifie le processus de **construction**, de **déploiement** et **d'accès** à une API avec un effort minimal. + +Il apporte la même **expérience développeur** de la création d'applications avec FastAPI au **déploiement** dans le cloud. 🎉 + +FastAPI Cloud est le principal sponsor et financeur des projets open source *FastAPI and friends*. ✨ + +#### Déployer sur d'autres fournisseurs cloud { #deploy-to-other-cloud-providers } + +FastAPI est open source et basé sur des standards. Vous pouvez déployer des applications FastAPI sur n'importe quel fournisseur cloud de votre choix. + +Suivez les guides de votre fournisseur cloud pour y déployer des applications FastAPI. 🤓 + +## Performance { #performance } + +Les benchmarks TechEmpower indépendants montrent que les applications **FastAPI** s'exécutant sous Uvicorn sont parmi les frameworks Python les plus rapides, juste derrière Starlette et Uvicorn eux-mêmes (utilisés en interne par FastAPI). (*) Pour en savoir plus, consultez la section Benchmarks. -## Dépendances facultatives +## Dépendances { #dependencies } -Utilisées par Pydantic: +FastAPI dépend de Pydantic et Starlette. -* email-validator - pour la validation des adresses email. +### Dépendances `standard` { #standard-dependencies } + +Lorsque vous installez FastAPI avec `pip install "fastapi[standard]"`, il inclut le groupe `standard` de dépendances optionnelles : + +Utilisées par Pydantic : + +* email-validator - pour la validation des adresses e-mail. Utilisées par Starlette : -* requests - Obligatoire si vous souhaitez utiliser `TestClient`. +* httpx - Obligatoire si vous souhaitez utiliser le `TestClient`. * jinja2 - Obligatoire si vous souhaitez utiliser la configuration de template par défaut. -* python-multipart - Obligatoire si vous souhaitez supporter le "décodage" de formulaire avec `request.form()`. -* itsdangerous - Obligatoire pour la prise en charge de `SessionMiddleware`. -* pyyaml - Obligatoire pour le support `SchemaGenerator` de Starlette (vous n'en avez probablement pas besoin avec FastAPI). +* python-multipart - Obligatoire si vous souhaitez prendre en charge l’« parsing » de formulaires avec `request.form()`. -Utilisées par FastAPI / Starlette : +Utilisées par FastAPI : -* uvicorn - Pour le serveur qui charge et sert votre application. -* orjson - Obligatoire si vous voulez utiliser `ORJSONResponse`. +* uvicorn - pour le serveur qui charge et sert votre application. Cela inclut `uvicorn[standard]`, qui comprend certaines dépendances (par ex. `uvloop`) nécessaires pour une haute performance. +* `fastapi-cli[standard]` - pour fournir la commande `fastapi`. + * Cela inclut `fastapi-cloud-cli`, qui vous permet de déployer votre application FastAPI sur FastAPI Cloud. + +### Sans les dépendances `standard` { #without-standard-dependencies } + +Si vous ne souhaitez pas inclure les dépendances optionnelles `standard`, vous pouvez installer avec `pip install fastapi` au lieu de `pip install "fastapi[standard]"`. + +### Sans `fastapi-cloud-cli` { #without-fastapi-cloud-cli } + +Si vous souhaitez installer FastAPI avec les dépendances standard mais sans `fastapi-cloud-cli`, vous pouvez installer avec `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. + +### Dépendances optionnelles supplémentaires { #additional-optional-dependencies } + +Il existe des dépendances supplémentaires que vous pourriez vouloir installer. + +Dépendances optionnelles supplémentaires pour Pydantic : + +* pydantic-settings - pour la gestion des paramètres. +* pydantic-extra-types - pour des types supplémentaires à utiliser avec Pydantic. + +Dépendances optionnelles supplémentaires pour FastAPI : + +* orjson - Obligatoire si vous souhaitez utiliser `ORJSONResponse`. * ujson - Obligatoire si vous souhaitez utiliser `UJSONResponse`. -Vous pouvez tout installer avec `pip install fastapi[all]`. - -## Licence +## Licence { #license } Ce projet est soumis aux termes de la licence MIT. diff --git a/docs/fr/docs/learn/index.md b/docs/fr/docs/learn/index.md index 46fc095dc..552687703 100644 --- a/docs/fr/docs/learn/index.md +++ b/docs/fr/docs/learn/index.md @@ -1,5 +1,5 @@ -# Apprendre +# Apprendre { #learn } Voici les sections introductives et les tutoriels pour apprendre **FastAPI**. -Vous pouvez considérer ceci comme un **manuel**, un **cours**, la **méthode officielle** et recommandée pour appréhender FastAPI. 😎 +Vous pouvez considérer ceci comme un **livre**, un **cours**, la **méthode officielle** et recommandée pour apprendre FastAPI. 😎 diff --git a/docs/fr/docs/project-generation.md b/docs/fr/docs/project-generation.md index 4c04dc167..f062ffecf 100644 --- a/docs/fr/docs/project-generation.md +++ b/docs/fr/docs/project-generation.md @@ -1,84 +1,28 @@ -# Génération de projets - Modèle +# Modèle Full Stack FastAPI { #full-stack-fastapi-template } -Vous pouvez utiliser un générateur de projet pour commencer, qui réalisera pour vous la mise en place de bases côté architecture globale, sécurité, base de données et premières routes d'API. +Les modèles, bien qu'ils soient généralement livrés avec une configuration spécifique, sont conçus pour être flexibles et personnalisables. Cela vous permet de les modifier et de les adapter aux exigences de votre projet, ce qui en fait un excellent point de départ. 🏁 -Un générateur de projet fera toujours une mise en place très subjective que vous devriez modifier et adapter suivant vos besoins, mais cela reste un bon point de départ pour vos projets. +Vous pouvez utiliser ce modèle pour démarrer, car il inclut une grande partie de la configuration initiale, la sécurité, la base de données et quelques endpoints d'API déjà prêts pour vous. -## Full Stack FastAPI PostgreSQL +Dépôt GitHub : Modèle Full Stack FastAPI -GitHub : https://github.com/tiangolo/full-stack-fastapi-postgresql +## Modèle Full Stack FastAPI - Pile technologique et fonctionnalités { #full-stack-fastapi-template-technology-stack-and-features } -### Full Stack FastAPI PostgreSQL - Fonctionnalités - -* Intégration **Docker** complète (basée sur Docker). -* Déploiement Docker en mode Swarm -* Intégration **Docker Compose** et optimisation pour développement local. -* Serveur web Python **prêt au déploiement** utilisant Uvicorn et Gunicorn. -* Backend Python **FastAPI** : - * **Rapide** : Très hautes performances, comparables à **NodeJS** ou **Go** (grâce à Starlette et Pydantic). - * **Intuitif** : Excellent support des éditeurs. Complétion partout. Moins de temps passé à déboguer. - * **Facile** : Fait pour être facile à utiliser et apprendre. Moins de temps passé à lire de la documentation. - * **Concis** : Minimise la duplication de code. Plusieurs fonctionnalités à chaque déclaration de paramètre. - * **Robuste** : Obtenez du code prêt pour être utilisé en production. Avec de la documentation automatique interactive. - * **Basé sur des normes** : Basé sur (et totalement compatible avec) les normes ouvertes pour les APIs : OpenAPI et JSON Schema. - * **Et bien d'autres fonctionnalités** comme la validation automatique, la sérialisation, l'authentification avec OAuth2 JWT tokens, etc. -* Hashage de **mots de passe sécurisé** par défaut. -* Authentification par **jetons JWT**. -* Modèles **SQLAlchemy** (indépendants des extensions Flask, afin qu'ils puissent être utilisés directement avec des *workers* Celery). -* Modèle de démarrages basiques pour les utilisateurs (à modifier et supprimer au besoin). -* Migrations **Alembic**. -* **CORS** (partage des ressources entre origines multiples, ou *Cross Origin Resource Sharing*). -* *Worker* **Celery** pouvant importer et utiliser les modèles et le code du reste du backend. -* Tests du backend REST basés sur **Pytest**, intégrés dans Docker, pour que vous puissiez tester toutes les interactions de l'API indépendamment de la base de données. Étant exécutés dans Docker, les tests peuvent utiliser un nouvel entrepôt de données créé de zéro à chaque fois (vous pouvez donc utiliser ElasticSearch, MongoDB, CouchDB, etc. et juste tester que l'API fonctionne). -* Intégration Python facile avec **Jupyter Kernels** pour le développement à distance ou intra-Docker avec des extensions comme Atom Hydrogen ou Visual Studio Code Jupyter. -* Frontend **Vue** : - * Généré avec Vue CLI. - * Gestion de l'**Authentification JWT**. - * Page de connexion. - * Après la connexion, page de tableau de bord principal. - * Tableau de bord principal avec création et modification d'utilisateurs. - * Modification de ses propres caractéristiques utilisateur. - * **Vuex**. - * **Vue-router**. - * **Vuetify** pour de magnifiques composants *material design*. - * **TypeScript**. - * Serveur Docker basé sur **Nginx** (configuré pour être facilement manipulé avec Vue-router). - * Utilisation de *Docker multi-stage building*, pour ne pas avoir besoin de sauvegarder ou *commit* du code compilé. - * Tests frontend exécutés à la compilation (pouvant être désactivés). - * Fait aussi modulable que possible, pour pouvoir fonctionner comme tel, tout en pouvant être utilisé qu'en partie grâce à Vue CLI. -* **PGAdmin** pour les bases de données PostgreSQL, facilement modifiable pour utiliser PHPMYAdmin ou MySQL. -* **Flower** pour la surveillance de tâches Celery. -* Équilibrage de charge entre le frontend et le backend avec **Traefik**, afin de pouvoir avoir les deux sur le même domaine, séparés par chemins, mais servis par différents conteneurs. -* Intégration Traefik, comprenant la génération automatique de certificat **HTTPS** Let's Encrypt. -* GitLab **CI** (intégration continue), comprenant des tests pour le frontend et le backend. - -## Full Stack FastAPI Couchbase - -GitHub : https://github.com/tiangolo/full-stack-fastapi-couchbase - -⚠️ **ATTENTION** ⚠️ - -Si vous démarrez un nouveau projet de zéro, allez voir les alternatives au début de cette page. - -Par exemple, le générateur de projet Full Stack FastAPI PostgreSQL peut être une meilleure alternative, étant activement maintenu et utilisé et comprenant toutes les nouvelles fonctionnalités et améliorations. - -Vous êtes toujours libre d'utiliser le générateur basé sur Couchbase si vous le voulez, cela devrait probablement fonctionner correctement, et si vous avez déjà un projet généré en utilisant ce dernier, cela devrait fonctionner aussi (et vous l'avez déjà probablement mis à jour suivant vos besoins). - -Vous pouvez en apprendre plus dans la documentation du dépôt GithHub. - -## Full Stack FastAPI MongoDB - -...viendra surement plus tard, suivant le temps que j'ai. 😅 🎉 - -## Modèles d'apprentissage automatique avec spaCy et FastAPI - -GitHub : https://github.com/microsoft/cookiecutter-spacy-fastapi - -## Modèles d'apprentissage automatique avec spaCy et FastAPI - Fonctionnalités - -* Intégration d'un modèle NER **spaCy**. -* Formatage de requête pour **Azure Cognitive Search**. -* Serveur Python web **prêt à utiliser en production** utilisant Uvicorn et Gunicorn. -* Déploiement CI/CD Kubernetes pour **Azure DevOps** (AKS). -* **Multilangues**. Choisissez facilement l'une des langues intégrées à spaCy durant la mise en place du projet. -* **Facilement généralisable** à d'autres bibliothèques similaires (Pytorch, Tensorflow), et non juste spaCy. +- ⚡ [**FastAPI**](https://fastapi.tiangolo.com/fr) pour l'API backend Python. + - 🧰 [SQLModel](https://sqlmodel.tiangolo.com) pour les interactions avec la base de données SQL en Python (ORM). + - 🔍 [Pydantic](https://docs.pydantic.dev), utilisé par FastAPI, pour la validation des données et la gestion des paramètres. + - 💾 [PostgreSQL](https://www.postgresql.org) comme base de données SQL. +- 🚀 [React](https://react.dev) pour le frontend. + - 💃 Utilisation de TypeScript, des hooks, de Vite et d'autres éléments d'un stack frontend moderne. + - 🎨 [Tailwind CSS](https://tailwindcss.com) et [shadcn/ui](https://ui.shadcn.com) pour les composants frontend. + - 🤖 Un client frontend généré automatiquement. + - 🧪 [Playwright](https://playwright.dev) pour les tests de bout en bout. + - 🦇 Prise en charge du mode sombre. +- 🐋 [Docker Compose](https://www.docker.com) pour le développement et la production. +- 🔒 Hachage sécurisé des mots de passe par défaut. +- 🔑 Authentification JWT (JSON Web Token). +- 📫 Récupération de mot de passe par e-mail. +- ✅ Tests avec [Pytest](https://pytest.org). +- 📞 [Traefik](https://traefik.io) comme proxy inverse / répartiteur de charge. +- 🚢 Instructions de déploiement avec Docker Compose, y compris la configuration d'un proxy Traefik frontal pour gérer les certificats HTTPS automatiques. +- 🏭 CI (intégration continue) et CD (déploiement continu) basés sur GitHub Actions. diff --git a/docs/fr/docs/python-types.md b/docs/fr/docs/python-types.md index 99ca90827..f393b0f5c 100644 --- a/docs/fr/docs/python-types.md +++ b/docs/fr/docs/python-types.md @@ -1,70 +1,68 @@ -# Introduction aux Types Python +# Introduction aux types Python { #python-types-intro } -Python supporte des annotations de type (ou *type hints*) optionnelles. +Python prend en charge des « type hints » (aussi appelées « annotations de type ») facultatives. -Ces annotations de type constituent une syntaxe spéciale qui permet de déclarer le type d'une variable. +Ces « type hints » ou annotations sont une syntaxe spéciale qui permet de déclarer le type d'une variable. -En déclarant les types de vos variables, cela permet aux différents outils comme les éditeurs de texte d'offrir un meilleur support. +En déclarant les types de vos variables, les éditeurs et outils peuvent vous offrir un meilleur support. -Ce chapitre n'est qu'un **tutoriel rapide / rappel** sur les annotations de type Python. -Seulement le minimum nécessaire pour les utiliser avec **FastAPI** sera couvert... ce qui est en réalité très peu. +Ceci est un **tutoriel rapide / rappel** à propos des annotations de type Python. Il couvre uniquement le minimum nécessaire pour les utiliser avec **FastAPI** ... ce qui est en réalité très peu. -**FastAPI** est totalement basé sur ces annotations de type, qui lui donnent de nombreux avantages. +**FastAPI** est totalement basé sur ces annotations de type, elles lui donnent de nombreux avantages et bénéfices. -Mais même si vous n'utilisez pas ou n'utiliserez jamais **FastAPI**, vous pourriez bénéficier d'apprendre quelques choses sur ces dernières. +Mais même si vous n'utilisez jamais **FastAPI**, vous auriez intérêt à en apprendre un peu à leur sujet. -/// note +/// note | Remarque -Si vous êtes un expert Python, et que vous savez déjà **tout** sur les annotations de type, passez au chapitre suivant. +Si vous êtes un expert Python, et que vous savez déjà tout sur les annotations de type, passez au chapitre suivant. /// -## Motivations +## Motivation { #motivation } -Prenons un exemple simple : +Commençons par un exemple simple : -{*../../docs_src/python_types/tutorial001.py*} +{* ../../docs_src/python_types/tutorial001_py39.py *} -Exécuter ce programe affiche : +Exécuter ce programme affiche : ``` John Doe ``` -La fonction : +La fonction fait ce qui suit : * Prend un `first_name` et un `last_name`. -* Convertit la première lettre de chaque paramètre en majuscules grâce à `title()`. -* Concatène les résultats avec un espace entre les deux. +* Convertit la première lettre de chacun en majuscule avec `title()`. +* Concatène ces deux valeurs avec un espace au milieu. -{*../../docs_src/python_types/tutorial001.py hl[2] *} +{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *} -### Limitations +### Modifier le code { #edit-it } C'est un programme très simple. Mais maintenant imaginez que vous l'écriviez de zéro. -À un certain point vous auriez commencé la définition de la fonction, vous aviez les paramètres prêts. +À un certain moment, vous auriez commencé la définition de la fonction, vous aviez les paramètres prêts ... -Mais vous aviez besoin de "cette méthode qui convertit la première lettre en majuscule". +Mais ensuite vous devez appeler « cette méthode qui convertit la première lettre en majuscule ». -Était-ce `upper` ? `uppercase` ? `first_uppercase` ? `capitalize` ? +Était-ce `upper` ? Était-ce `uppercase` ? `first_uppercase` ? `capitalize` ? -Vous essayez donc d'utiliser le vieil ami du programmeur, l'auto-complétion de l'éditeur. +Vous essayez alors avec l'ami de toujours des programmeurs, l'autocomplétion de l'éditeur. -Vous écrivez le premier paramètre, `first_name`, puis un point (`.`) et appuyez sur `Ctrl+Espace` pour déclencher l'auto-complétion. +Vous tapez le premier paramètre de la fonction, `first_name`, puis un point (`.`) et appuyez sur `Ctrl+Espace` pour déclencher l'autocomplétion. -Mais malheureusement, rien d'utile n'en résulte : +Mais, malheureusement, vous n'obtenez rien d'utile : -### Ajouter des types +### Ajouter des types { #add-types } Modifions une seule ligne de la version précédente. -Nous allons changer seulement cet extrait, les paramètres de la fonction, de : - +Nous allons changer exactement ce fragment, les paramètres de la fonction, de : ```Python first_name, last_name @@ -78,222 +76,389 @@ Nous allons changer seulement cet extrait, les paramètres de la fonction, de : C'est tout. -Ce sont des annotations de types : +Ce sont les « annotations de type » : -{*../../docs_src/python_types/tutorial002.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} -À ne pas confondre avec la déclaration de valeurs par défaut comme ici : +Ce n'est pas la même chose que de déclarer des valeurs par défaut, ce qui serait : ```Python first_name="john", last_name="doe" ``` -C'est une chose différente. +C'est différent. -On utilise un deux-points (`:`), et pas un égal (`=`). +Nous utilisons des deux-points (`:`), pas des signes égal (`=`). -Et ajouter des annotations de types ne crée normalement pas de différence avec le comportement qui aurait eu lieu si elles n'étaient pas là. +Et ajouter des annotations de type ne change normalement pas ce qui se passe par rapport à ce qui se passerait sans elles. -Maintenant, imaginez que vous êtes en train de créer cette fonction, mais avec des annotations de type cette fois. +Mais maintenant, imaginez que vous êtes à nouveau en train de créer cette fonction, mais avec des annotations de type. -Au même moment que durant l'exemple précédent, vous essayez de déclencher l'auto-complétion et vous voyez : +Au même moment, vous essayez de déclencher l'autocomplétion avec `Ctrl+Espace` et vous voyez : -Vous pouvez donc dérouler les options jusqu'à trouver la méthode à laquelle vous pensiez. +Avec cela, vous pouvez faire défiler en voyant les options, jusqu'à trouver celle qui « vous dit quelque chose » : -## Plus de motivations +## Plus de motivation { #more-motivation } -Cette fonction possède déjà des annotations de type : +Regardez cette fonction, elle a déjà des annotations de type : -{*../../docs_src/python_types/tutorial003.py hl[1] *} +{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *} -Comme l'éditeur connaît le type des variables, vous n'avez pas seulement l'auto-complétion, mais aussi de la détection d'erreurs : +Comme l'éditeur connaît les types des variables, vous n'obtenez pas seulement l'autocomplétion, vous obtenez aussi des vérifications d'erreurs : -Maintenant que vous avez connaissance du problème, convertissez `age` en chaîne de caractères grâce à `str(age)` : +Vous savez maintenant qu'il faut corriger, convertir `age` en chaîne avec `str(age)` : -{*../../docs_src/python_types/tutorial004.py hl[2] *} +{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *} -## Déclarer des types +## Déclarer des types { #declaring-types } -Vous venez de voir là où les types sont généralement déclarés : dans les paramètres de fonctions. +Vous venez de voir l'endroit principal pour déclarer des annotations de type : dans les paramètres des fonctions. -C'est aussi ici que vous les utiliseriez avec **FastAPI**. +C'est aussi l'endroit principal où vous les utiliserez avec **FastAPI**. -### Types simples +### Types simples { #simple-types } -Vous pouvez déclarer tous les types de Python, pas seulement `str`. +Vous pouvez déclarer tous les types standards de Python, pas seulement `str`. -Comme par exemple : +Vous pouvez utiliser, par exemple : * `int` * `float` * `bool` * `bytes` -{*../../docs_src/python_types/tutorial005.py hl[1] *} +{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *} -### Types génériques avec des paramètres de types +### Types génériques avec paramètres de type { #generic-types-with-type-parameters } -Il existe certaines structures de données qui contiennent d'autres valeurs, comme `dict`, `list`, `set` et `tuple`. Et les valeurs internes peuvent elles aussi avoir leurs propres types. +Il existe certaines structures de données qui peuvent contenir d'autres valeurs, comme `dict`, `list`, `set` et `tuple`. Et les valeurs internes peuvent aussi avoir leur propre type. -Pour déclarer ces types et les types internes, on utilise le module standard de Python `typing`. +Ces types qui ont des types internes sont appelés types « génériques ». Et il est possible de les déclarer, même avec leurs types internes. -Il existe spécialement pour supporter ces annotations de types. +Pour déclarer ces types et les types internes, vous pouvez utiliser le module standard Python `typing`. Il existe spécifiquement pour prendre en charge ces annotations de type. -#### `List` +#### Versions plus récentes de Python { #newer-versions-of-python } -Par exemple, définissons une variable comme `list` de `str`. +La syntaxe utilisant `typing` est compatible avec toutes les versions, de Python 3.6 aux plus récentes, y compris Python 3.9, Python 3.10, etc. -Importez `List` (avec un `L` majuscule) depuis `typing`. +Au fur et à mesure que Python évolue, les versions plus récentes apportent un meilleur support pour ces annotations de type et dans de nombreux cas vous n'aurez même pas besoin d'importer et d'utiliser le module `typing` pour les déclarer. -{*../../docs_src/python_types/tutorial006.py hl[1] *} +Si vous pouvez choisir une version plus récente de Python pour votre projet, vous pourrez profiter de cette simplicité supplémentaire. -Déclarez la variable, en utilisant la syntaxe des deux-points (`:`). +Dans toute la documentation, il y a des exemples compatibles avec chaque version de Python (lorsqu'il y a une différence). -Et comme type, mettez `List`. +Par exemple « Python 3.6+ » signifie que c'est compatible avec Python 3.6 ou supérieur (y compris 3.7, 3.8, 3.9, 3.10, etc.). Et « Python 3.9+ » signifie que c'est compatible avec Python 3.9 ou supérieur (y compris 3.10, etc). -Les listes étant un type contenant des types internes, mettez ces derniers entre crochets (`[`, `]`) : +Si vous pouvez utiliser les dernières versions de Python, utilisez les exemples pour la dernière version, ils auront la meilleure et la plus simple syntaxe, par exemple, « Python 3.10+ ». -{*../../docs_src/python_types/tutorial006.py hl[4] *} +#### Liste { #list } -/// tip | Astuce +Par exemple, définissons une variable comme une `list` de `str`. -Ces types internes entre crochets sont appelés des "paramètres de type". +Déclarez la variable, en utilisant la même syntaxe avec deux-points (`:`). -Ici, `str` est un paramètre de type passé à `List`. +Comme type, mettez `list`. + +Comme la liste est un type qui contient des types internes, mettez-les entre crochets : + +{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *} + +/// info + +Ces types internes entre crochets sont appelés « paramètres de type ». + +Dans ce cas, `str` est le paramètre de type passé à `list`. /// -Ce qui signifie : "la variable `items` est une `list`, et chacun de ses éléments a pour type `str`. +Cela signifie : « la variable `items` est une `list`, et chacun des éléments de cette liste est un `str` ». -En faisant cela, votre éditeur pourra vous aider, même pendant que vous traitez des éléments de la liste. +En faisant cela, votre éditeur peut vous fournir de l'aide même pendant le traitement des éléments de la liste : Sans types, c'est presque impossible à réaliser. -Vous remarquerez que la variable `item` n'est qu'un des éléments de la list `items`. +Remarquez que la variable `item` est l'un des éléments de la liste `items`. -Et pourtant, l'éditeur sait qu'elle est de type `str` et pourra donc vous aider à l'utiliser. +Et pourtant, l'éditeur sait que c'est un `str` et fournit le support approprié. -#### `Tuple` et `Set` +#### Tuple et Set { #tuple-and-set } -C'est le même fonctionnement pour déclarer un `tuple` ou un `set` : +Vous feriez la même chose pour déclarer des `tuple` et des `set` : -{*../../docs_src/python_types/tutorial007.py hl[1,4] *} +{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *} -Dans cet exemple : +Cela signifie : -* La variable `items_t` est un `tuple` avec 3 éléments, un `int`, un deuxième `int`, et un `str`. +* La variable `items_t` est un `tuple` avec 3 éléments, un `int`, un autre `int`, et un `str`. * La variable `items_s` est un `set`, et chacun de ses éléments est de type `bytes`. -#### `Dict` +#### Dict { #dict } -Pour définir un `dict`, il faut lui passer 2 paramètres, séparés par une virgule (`,`). +Pour définir un `dict`, vous passez 2 paramètres de type, séparés par des virgules. -Le premier paramètre de type est pour les clés et le second pour les valeurs du dictionnaire (`dict`). +Le premier paramètre de type est pour les clés du `dict`. -{*../../docs_src/python_types/tutorial008.py hl[1,4] *} +Le second paramètre de type est pour les valeurs du `dict` : -Dans cet exemple : +{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *} -* La variable `prices` est de type `dict` : - * Les clés de ce dictionnaire sont de type `str`. - * Les valeurs de ce dictionnaire sont de type `float`. +Cela signifie : -#### `Optional` +* La variable `prices` est un `dict` : + * Les clés de ce `dict` sont de type `str` (disons, le nom de chaque article). + * Les valeurs de ce `dict` sont de type `float` (disons, le prix de chaque article). -Vous pouvez aussi utiliser `Optional` pour déclarer qu'une variable a un type, comme `str` mais qu'il est "optionnel" signifiant qu'il pourrait aussi être `None`. +#### Union { #union } -{*../../docs_src/python_types/tutorial009.py hl[1,4] *} +Vous pouvez déclarer qu'une variable peut être de plusieurs types, par exemple, un `int` ou un `str`. -Utiliser `Optional[str]` plutôt que `str` permettra à l'éditeur de vous aider à détecter les erreurs où vous supposeriez qu'une valeur est toujours de type `str`, alors qu'elle pourrait aussi être `None`. +Dans Python 3.6 et supérieur (y compris Python 3.10), vous pouvez utiliser le type `Union` de `typing` et mettre entre crochets les types possibles à accepter. -#### Types génériques +Dans Python 3.10, il existe aussi une nouvelle syntaxe où vous pouvez mettre les types possibles séparés par une barre verticale (`|`). -Les types qui peuvent contenir des paramètres de types entre crochets, comme : +//// tab | Python 3.10+ -* `List` -* `Tuple` -* `Set` -* `Dict` +```Python hl_lines="1" +{!> ../../docs_src/python_types/tutorial008b_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial008b_py39.py!} +``` + +//// + +Dans les deux cas, cela signifie que `item` peut être un `int` ou un `str`. + +#### Possiblement `None` { #possibly-none } + +Vous pouvez déclarer qu'une valeur peut avoir un type, comme `str`, mais qu'elle peut aussi être `None`. + +Dans Python 3.6 et supérieur (y compris Python 3.10), vous pouvez le déclarer en important et en utilisant `Optional` depuis le module `typing`. + +```Python hl_lines="1 4" +{!../../docs_src/python_types/tutorial009_py39.py!} +``` + +Utiliser `Optional[str]` au lieu de simplement `str` permettra à l'éditeur de vous aider à détecter des erreurs où vous supposeriez qu'une valeur est toujours un `str`, alors qu'elle pourrait en fait aussi être `None`. + +`Optional[Something]` est en réalité un raccourci pour `Union[Something, None]`, ils sont équivalents. + +Cela signifie aussi que dans Python 3.10, vous pouvez utiliser `Something | None` : + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../docs_src/python_types/tutorial009_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009_py39.py!} +``` + +//// + +//// tab | Python 3.9+ alternative + +```Python hl_lines="1 4" +{!> ../../docs_src/python_types/tutorial009b_py39.py!} +``` + +//// + +#### Utiliser `Union` ou `Optional` { #using-union-or-optional } + +Si vous utilisez une version de Python inférieure à 3.10, voici un conseil de mon point de vue très **subjectif** : + +* 🚨 Évitez d'utiliser `Optional[SomeType]` +* À la place ✨ **utilisez `Union[SomeType, None]`** ✨. + +Les deux sont équivalents et sous le capot ce sont les mêmes, mais je recommanderais `Union` plutôt que `Optional` parce que le mot « facultatif » semble impliquer que la valeur est optionnelle, alors que cela signifie en fait « elle peut être `None` », même si elle n'est pas facultative et est toujours requise. + +Je pense que `Union[SomeType, None]` est plus explicite sur ce que cela signifie. + +Il ne s'agit que des mots et des noms. Mais ces mots peuvent influencer la manière dont vous et vos coéquipiers pensez au code. + +Par exemple, prenons cette fonction : + +{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *} + +Le paramètre `name` est défini comme `Optional[str]`, mais il n'est pas facultatif, vous ne pouvez pas appeler la fonction sans le paramètre : + +```Python +say_hi() # Oh non, cela lève une erreur ! 😱 +``` + +Le paramètre `name` est toujours requis (pas « optionnel ») parce qu'il n'a pas de valeur par défaut. Néanmoins, `name` accepte `None` comme valeur : + +```Python +say_hi(name=None) # Cela fonctionne, None est valide 🎉 +``` + +La bonne nouvelle est que, dès que vous êtes sur Python 3.10, vous n'avez plus à vous en soucier, car vous pourrez simplement utiliser `|` pour définir des unions de types : + +{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *} + +Et alors vous n'aurez plus à vous soucier de noms comme `Optional` et `Union`. 😎 + +#### Types génériques { #generic-types } + +Ces types qui prennent des paramètres de type entre crochets sont appelés des **types génériques** ou **Generics**, par exemple : + +//// tab | Python 3.10+ + +Vous pouvez utiliser les mêmes types intégrés comme génériques (avec des crochets et des types à l'intérieur) : + +* `list` +* `tuple` +* `set` +* `dict` + +Et, comme avec les versions précédentes de Python, depuis le module `typing` : + +* `Union` * `Optional` -* ...et d'autres. +* ... et d'autres. -sont appelés des **types génériques** ou **Generics**. +Dans Python 3.10, comme alternative à l'utilisation des génériques `Union` et `Optional`, vous pouvez utiliser la barre verticale (`|`) pour déclarer des unions de types, c'est bien mieux et plus simple. -### Classes en tant que types +//// + +//// tab | Python 3.9+ + +Vous pouvez utiliser les mêmes types intégrés comme génériques (avec des crochets et des types à l'intérieur) : + +* `list` +* `tuple` +* `set` +* `dict` + +Et des génériques depuis le module `typing` : + +* `Union` +* `Optional` +* ... et d'autres. + +//// + +### Classes en tant que types { #classes-as-types } Vous pouvez aussi déclarer une classe comme type d'une variable. -Disons que vous avez une classe `Person`, avec une variable `name` : - -{*../../docs_src/python_types/tutorial010.py hl[1:3] *} +Disons que vous avez une classe `Person`, avec un nom : +{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *} Vous pouvez ensuite déclarer une variable de type `Person` : -{*../../docs_src/python_types/tutorial010.py hl[6] *} +{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *} -Et vous aurez accès, encore une fois, au support complet offert par l'éditeur : +Et là encore, vous obtenez tout le support de l'éditeur : -## Les modèles Pydantic +Remarquez que cela signifie « `one_person` est une instance de la classe `Person` ». + +Cela ne signifie pas « `one_person` est la classe appelée `Person` ». + +## Modèles Pydantic { #pydantic-models } Pydantic est une bibliothèque Python pour effectuer de la validation de données. -Vous déclarez la forme de la donnée avec des classes et des attributs. +Vous déclarez la « forme » de la donnée sous forme de classes avec des attributs. -Chaque attribut possède un type. +Et chaque attribut a un type. -Puis vous créez une instance de cette classe avec certaines valeurs et **Pydantic** validera les valeurs, les convertira dans le type adéquat (si c'est nécessaire et possible) et vous donnera un objet avec toute la donnée. +Ensuite, vous créez une instance de cette classe avec certaines valeurs et elle validera les valeurs, les convertira dans le type approprié (le cas échéant) et vous donnera un objet avec toutes les données. -Ainsi, votre éditeur vous offrira un support adapté pour l'objet résultant. +Et vous obtenez tout le support de l'éditeur avec cet objet résultant. -Extrait de la documentation officielle de **Pydantic** : +Un exemple tiré de la documentation officielle de Pydantic : -{*../../docs_src/python_types/tutorial011.py*} +{* ../../docs_src/python_types/tutorial011_py310.py *} /// info -Pour en savoir plus à propos de Pydantic, allez jeter un coup d'oeil à sa documentation. +Pour en savoir plus à propos de Pydantic, consultez sa documentation. /// -**FastAPI** est basé entièrement sur **Pydantic**. +**FastAPI** est entièrement basé sur Pydantic. -Vous verrez bien plus d'exemples de son utilisation dans [Tutoriel - Guide utilisateur](tutorial/index.md){.internal-link target=_blank}. +Vous verrez beaucoup plus de tout cela en pratique dans le [Tutoriel - Guide utilisateur](tutorial/index.md){.internal-link target=_blank}. -## Les annotations de type dans **FastAPI** +/// tip | Astuce -**FastAPI** utilise ces annotations pour faire différentes choses. +Pydantic a un comportement spécial lorsque vous utilisez `Optional` ou `Union[Something, None]` sans valeur par défaut, vous pouvez en lire davantage dans la documentation de Pydantic à propos des champs Optionals requis. -Avec **FastAPI**, vous déclarez des paramètres grâce aux annotations de types et vous obtenez : +/// -* **du support de l'éditeur** -* **de la vérification de types** +## Annotations de type avec métadonnées { #type-hints-with-metadata-annotations } -...et **FastAPI** utilise ces mêmes déclarations pour : +Python dispose également d'une fonctionnalité qui permet de mettre des **métadonnées supplémentaires** dans ces annotations de type en utilisant `Annotated`. -* **Définir les prérequis** : depuis les paramètres de chemins des requêtes, les entêtes, les corps, les dépendances, etc. -* **Convertir des données** : depuis la requête vers les types requis. -* **Valider des données** : venant de chaque requête : - * Générant automatiquement des **erreurs** renvoyées au client quand la donnée est invalide. +Depuis Python 3.9, `Annotated` fait partie de la bibliothèque standard, vous pouvez donc l'importer depuis `typing`. + +{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *} + +Python lui-même ne fait rien avec ce `Annotated`. Et pour les éditeurs et autres outils, le type est toujours `str`. + +Mais vous pouvez utiliser cet espace dans `Annotated` pour fournir à **FastAPI** des métadonnées supplémentaires sur la façon dont vous voulez que votre application se comporte. + +L'important à retenir est que le premier paramètre de type que vous passez à `Annotated` est le type réel. Le reste n'est que des métadonnées pour d'autres outils. + +Pour l'instant, vous avez juste besoin de savoir que `Annotated` existe, et que c'est du Python standard. 😎 + +Plus tard, vous verrez à quel point cela peut être puissant. + +/// tip | Astuce + +Le fait que ce soit du Python standard signifie que vous bénéficierez toujours de la meilleure expérience développeur possible dans votre éditeur, avec les outils que vous utilisez pour analyser et refactoriser votre code, etc. ✨ + +Et aussi que votre code sera très compatible avec de nombreux autres outils et bibliothèques Python. 🚀 + +/// + +## Annotations de type dans **FastAPI** { #type-hints-in-fastapi } + +**FastAPI** tire parti de ces annotations de type pour faire plusieurs choses. + +Avec **FastAPI**, vous déclarez des paramètres avec des annotations de type et vous obtenez : + +* **Du support de l'éditeur**. +* **Des vérifications de types**. + +... et **FastAPI** utilise les mêmes déclarations pour : + +* **Définir des prérequis** : à partir des paramètres de chemin de la requête, des paramètres de requête, des en-têtes, des corps, des dépendances, etc. +* **Convertir des données** : de la requête vers le type requis. +* **Valider des données** : provenant de chaque requête : + * En générant des **erreurs automatiques** renvoyées au client lorsque la donnée est invalide. * **Documenter** l'API avec OpenAPI : - * ce qui ensuite utilisé par les interfaces utilisateur automatiques de documentation interactive. + * ce qui est ensuite utilisé par les interfaces utilisateur de documentation interactive automatiques. -Tout cela peut paraître bien abstrait, mais ne vous inquiétez pas, vous verrez tout ça en pratique dans [Tutoriel - Guide utilisateur](tutorial/index.md){.internal-link target=_blank}. +Tout cela peut sembler abstrait. Ne vous inquiétez pas. Vous verrez tout cela en action dans le [Tutoriel - Guide utilisateur](tutorial/index.md){.internal-link target=_blank}. -Ce qu'il faut retenir c'est qu'en utilisant les types standard de Python, à un seul endroit (plutôt que d'ajouter plus de classes, de décorateurs, etc.), **FastAPI** fera une grande partie du travail pour vous. +L'important est qu'en utilisant les types standards de Python, en un seul endroit (au lieu d'ajouter plus de classes, de décorateurs, etc.), **FastAPI** fera une grande partie du travail pour vous. /// info -Si vous avez déjà lu le tutoriel et êtes revenus ici pour voir plus sur les types, une bonne ressource est la "cheat sheet" de `mypy`. +Si vous avez déjà parcouru tout le tutoriel et êtes revenu pour en voir plus sur les types, une bonne ressource est l'« aide-mémoire » de `mypy`. /// diff --git a/docs/fr/docs/tutorial/background-tasks.md b/docs/fr/docs/tutorial/background-tasks.md index 6efd16e07..ed7494669 100644 --- a/docs/fr/docs/tutorial/background-tasks.md +++ b/docs/fr/docs/tutorial/background-tasks.md @@ -1,4 +1,4 @@ -# Tâches d'arrière-plan +# Tâches d'arrière-plan { #background-tasks } Vous pouvez définir des tâches d'arrière-plan qui seront exécutées après avoir retourné une réponse. @@ -7,20 +7,19 @@ Ceci est utile pour les opérations qui doivent avoir lieu après une requête, Cela comprend, par exemple : * Les notifications par email envoyées après l'exécution d'une action : - * Étant donné que se connecter à un serveur et envoyer un email a tendance à être «lent» (plusieurs secondes), vous pouvez retourner la réponse directement et envoyer la notification en arrière-plan. + * Étant donné que se connecter à un serveur et envoyer un email a tendance à être « lent » (plusieurs secondes), vous pouvez retourner la réponse directement et envoyer la notification en arrière-plan. * Traiter des données : - * Par exemple, si vous recevez un fichier qui doit passer par un traitement lent, vous pouvez retourner une réponse «Accepted» (HTTP 202) puis faire le traitement en arrière-plan. + * Par exemple, si vous recevez un fichier qui doit passer par un traitement lent, vous pouvez retourner une réponse « Accepted » (HTTP 202) puis faire le traitement en arrière-plan. +## Utiliser `BackgroundTasks` { #using-backgroundtasks } -## Utiliser `BackgroundTasks` +Pour commencer, importez `BackgroundTasks` et définissez un paramètre dans votre *fonction de chemin d'accès* avec `BackgroundTasks` comme type déclaré. -Pour commencer, importez `BackgroundTasks` et définissez un paramètre dans votre *fonction de chemin* avec `BackgroundTasks` comme type déclaré. - -{* ../../docs_src/background_tasks/tutorial001.py hl[1,13] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} **FastAPI** créera l'objet de type `BackgroundTasks` pour vous et le passera comme paramètre. -## Créer une fonction de tâche +## Créer une fonction de tâche { #create-a-task-function } Une fonction à exécuter comme tâche d'arrière-plan est juste une fonction standard qui peut recevoir des paramètres. @@ -30,14 +29,13 @@ Dans cet exemple, la fonction de tâche écrira dans un fichier (afin de simuler L'opération d'écriture n'utilisant ni `async` ni `await`, on définit la fonction avec un `def` normal. -{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} -## Ajouter une tâche d'arrière-plan +## Ajouter une tâche d'arrière-plan { #add-the-background-task } -Dans votre *fonction de chemin*, passez votre fonction de tâche à l'objet de type `BackgroundTasks` (`background_tasks` ici) grâce à la méthode `.add_task()` : +Dans votre *fonction de chemin d'accès*, passez votre fonction de tâche à l'objet de type `BackgroundTasks` (`background_tasks` ici) grâce à la méthode `.add_task()` : - -{* ../../docs_src/background_tasks/tutorial001.py hl[14] *} +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} `.add_task()` reçoit comme arguments : @@ -45,40 +43,40 @@ Dans votre *fonction de chemin*, passez votre fonction de tâche à l'objet de t * Les arguments positionnels à passer à la fonction de tâche dans l'ordre (`email`). * Les arguments nommés à passer à la fonction de tâche (`message="some notification"`). -## Injection de dépendances +## Injection de dépendances { #dependency-injection } -Utiliser `BackgroundTasks` fonctionne aussi avec le système d'injection de dépendances. Vous pouvez déclarer un paramètre de type `BackgroundTasks` à différents niveaux : dans une *fonction de chemin*, dans une dépendance, dans une sous-dépendance... +Utiliser `BackgroundTasks` fonctionne aussi avec le système d'injection de dépendances. Vous pouvez déclarer un paramètre de type `BackgroundTasks` à différents niveaux : dans une *fonction de chemin d'accès*, dans une dépendance (dependable), dans une sous-dépendance, etc. -**FastAPI** sait quoi faire dans chaque cas et comment réutiliser le même objet, afin que tous les paramètres de type `BackgroundTasks` soient fusionnés et que les tâches soient exécutées en arrière-plan : +**FastAPI** sait quoi faire dans chaque cas et comment réutiliser le même objet, afin que toutes les tâches d'arrière-plan soient fusionnées et que les tâches soient ensuite exécutées en arrière-plan : -{* ../../docs_src/background_tasks/tutorial002.py hl[13,15,22,25] *} +{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} Dans cet exemple, les messages seront écrits dans le fichier `log.txt` après que la réponse soit envoyée. -S'il y avait une `query` (paramètre nommé `q`) dans la requête, alors elle sera écrite dans `log.txt` via une tâche d'arrière-plan. +S'il y avait un paramètre de requête dans la requête, alors il sera écrit dans le journal via une tâche d'arrière-plan. -Et ensuite une autre tâche d'arrière-plan (générée dans les paramètres de la *la fonction de chemin*) écrira un message dans `log.txt` comprenant le paramètre de chemin `email`. +Et ensuite une autre tâche d'arrière-plan (générée dans la *fonction de chemin d'accès*) écrira un message comprenant le paramètre de chemin `email`. -## Détails techniques +## Détails techniques { #technical-details } La classe `BackgroundTasks` provient directement de `starlette.background`. Elle est importée/incluse directement dans **FastAPI** pour que vous puissiez l'importer depuis `fastapi` et éviter d'importer accidentellement `BackgroundTask` (sans `s` à la fin) depuis `starlette.background`. -En utilisant seulement `BackgroundTasks` (et non `BackgroundTask`), il est possible de l'utiliser en tant que paramètre de *fonction de chemin* et de laisser **FastAPI** gérer le reste pour vous, comme en utilisant l'objet `Request` directement. +En utilisant seulement `BackgroundTasks` (et non `BackgroundTask`), il est possible de l'utiliser en tant que paramètre de *fonction de chemin d'accès* et de laisser **FastAPI** gérer le reste pour vous, comme en utilisant l'objet `Request` directement. Il est tout de même possible d'utiliser `BackgroundTask` seul dans **FastAPI**, mais dans ce cas il faut créer l'objet dans le code et renvoyer une `Response` Starlette l'incluant. -Plus de détails sont disponibles dans la documentation officielle de Starlette sur les tâches d'arrière-plan (via leurs classes `BackgroundTasks`et `BackgroundTask`). +Plus de détails sont disponibles dans la documentation officielle de Starlette sur les tâches d'arrière-plan. -## Avertissement +## Avertissement { #caveat } Si vous avez besoin de réaliser des traitements lourds en tâche d'arrière-plan et que vous n'avez pas besoin que ces traitements aient lieu dans le même process (par exemple, pas besoin de partager la mémoire, les variables, etc.), il peut s'avérer profitable d'utiliser des outils plus importants tels que Celery. -Ces outils nécessitent généralement des configurations plus complexes ainsi qu'un gestionnaire de queue de message, comme RabbitMQ ou Redis, mais ils permettent d'exécuter des tâches d'arrière-plan dans différents process, et potentiellement, sur plusieurs serveurs. +Ces outils nécessitent généralement des configurations plus complexes ainsi qu'un gestionnaire de queue de message, comme RabbitMQ ou Redis, mais ils permettent d'exécuter des tâches d'arrière-plan dans différents process, et surtout, sur plusieurs serveurs. Mais si vous avez besoin d'accéder aux variables et objets de la même application **FastAPI**, ou si vous avez besoin d'effectuer de petites tâches d'arrière-plan (comme envoyer des notifications par email), vous pouvez simplement vous contenter d'utiliser `BackgroundTasks`. -## Résumé +## Résumé { #recap } -Importez et utilisez `BackgroundTasks` grâce aux paramètres de *fonction de chemin* et les dépendances pour ajouter des tâches d'arrière-plan. +Importez et utilisez `BackgroundTasks` grâce aux paramètres de *fonction de chemin d'accès* et les dépendances pour ajouter des tâches d'arrière-plan. diff --git a/docs/fr/docs/tutorial/body-multiple-params.md b/docs/fr/docs/tutorial/body-multiple-params.md index 0541acc74..9aed24e98 100644 --- a/docs/fr/docs/tutorial/body-multiple-params.md +++ b/docs/fr/docs/tutorial/body-multiple-params.md @@ -1,24 +1,24 @@ -# Body - Paramètres multiples +# Body - Paramètres multiples { #body-multiple-parameters } -Maintenant que nous avons vu comment manipuler `Path` et `Query`, voyons comment faire pour le corps d'une requête, communément désigné par le terme anglais "body". +Maintenant que nous avons vu comment utiliser `Path` et `Query`, voyons des usages plus avancés des déclarations de paramètres du corps de la requête. -## Mélanger les paramètres `Path`, `Query` et body +## Mélanger les paramètres `Path`, `Query` et body { #mix-path-query-and-body-parameters } -Tout d'abord, sachez que vous pouvez mélanger les déclarations des paramètres `Path`, `Query` et body, **FastAPI** saura quoi faire. +Tout d'abord, sachez que vous pouvez mélanger librement les déclarations des paramètres `Path`, `Query` et du body, **FastAPI** saura quoi faire. -Vous pouvez également déclarer des paramètres body comme étant optionnels, en leur assignant une valeur par défaut à `None` : +Et vous pouvez également déclarer des paramètres du body comme étant optionnels, en leur assignant une valeur par défaut à `None` : {* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} -/// note +/// note | Remarque -Notez que, dans ce cas, le paramètre `item` provenant du `Body` est optionnel (sa valeur par défaut est `None`). +Notez que, dans ce cas, l'élément `item` récupéré depuis le body est optionnel. Comme sa valeur par défaut est `None`. /// -## Paramètres multiples du body +## Paramètres multiples du body { #multiple-body-parameters } -Dans l'exemple précédent, les opérations de routage attendaient un body JSON avec les attributs d'un `Item`, par exemple : +Dans l'exemple précédent, les chemins d'accès attendraient un body JSON avec les attributs d'un `Item`, par exemple : ```JSON { @@ -29,13 +29,13 @@ Dans l'exemple précédent, les opérations de routage attendaient un body JSON } ``` -Mais vous pouvez également déclarer plusieurs paramètres provenant de body, par exemple `item` et `user` simultanément : +Mais vous pouvez également déclarer plusieurs paramètres provenant du body, par exemple `item` et `user` : {* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *} -Dans ce cas, **FastAPI** détectera qu'il y a plus d'un paramètre dans le body (chacun correspondant à un modèle Pydantic). +Dans ce cas, **FastAPI** détectera qu'il y a plus d'un paramètre du body dans la fonction (il y a deux paramètres qui sont des modèles Pydantic). -Il utilisera alors les noms des paramètres comme clés, et s'attendra à recevoir quelque chose de semblable à : +Il utilisera alors les noms des paramètres comme clés (noms de champs) dans le body, et s'attendra à recevoir un body semblable à : ```JSON { @@ -52,25 +52,25 @@ Il utilisera alors les noms des paramètres comme clés, et s'attendra à recevo } ``` -/// note +/// note | Remarque -"Notez que, bien que nous ayons déclaré le paramètre `item` de la même manière que précédemment, il est maintenant associé à la clé `item` dans le corps de la requête."`. +Notez que, bien que `item` ait été déclaré de la même manière qu'auparavant, il est désormais attendu à l'intérieur du body sous la clé `item`. /// -**FastAPI** effectue la conversion de la requête de façon transparente, de sorte que les objets `item` et `user` se trouvent correctement définis. +**FastAPI** effectuera la conversion automatique depuis la requête, de sorte que le paramètre `item` reçoive son contenu spécifique, et de même pour `user`. -Il effectue également la validation des données (même imbriquées les unes dans les autres), et permet de les documenter correctement (schéma OpenAPI et documentation auto-générée). +Il effectuera la validation des données composées, et les documentera ainsi pour le schéma OpenAPI et la documentation automatique. -## Valeurs scalaires dans le body +## Valeurs singulières dans le body { #singular-values-in-body } -De la même façon qu'il existe `Query` et `Path` pour définir des données supplémentaires pour les paramètres query et path, **FastAPI** fournit un équivalent `Body`. +De la même façon qu'il existe `Query` et `Path` pour définir des données supplémentaires pour les paramètres de requête et de chemin, **FastAPI** fournit un équivalent `Body`. -Par exemple, en étendant le modèle précédent, vous pouvez vouloir ajouter un paramètre `importance` dans le même body, en plus des paramètres `item` et `user`. +Par exemple, en étendant le modèle précédent, vous pourriez décider d'avoir une autre clé `importance` dans le même body, en plus de `item` et `user`. -Si vous le déclarez tel quel, comme c'est une valeur [scalaire](https://docs.github.com/fr/graphql/reference/scalars), **FastAPI** supposera qu'il s'agit d'un paramètre de requête (`Query`). +Si vous le déclarez tel quel, comme c'est une valeur singulière, **FastAPI** supposera qu'il s'agit d'un paramètre de requête. -Mais vous pouvez indiquer à **FastAPI** de la traiter comme une variable de body en utilisant `Body` : +Mais vous pouvez indiquer à **FastAPI** de la traiter comme une autre clé du body en utilisant `Body` : {* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} @@ -92,51 +92,51 @@ Dans ce cas, **FastAPI** s'attendra à un body semblable à : } ``` -Encore une fois, cela convertira les types de données, les validera, permettra de générer la documentation, etc... +Encore une fois, il convertira les types de données, validera, documentera, etc. -## Paramètres multiples body et query +## Paramètres multiples du body et paramètres de requête { #multiple-body-params-and-query } -Bien entendu, vous pouvez déclarer autant de paramètres que vous le souhaitez, en plus des paramètres body déjà déclarés. +Bien entendu, vous pouvez également déclarer des paramètres de requête supplémentaires quand vous en avez besoin, en plus de tout paramètre du body. -Par défaut, les valeurs [scalaires](https://docs.github.com/fr/graphql/reference/scalars) sont interprétées comme des paramètres query, donc inutile d'ajouter explicitement `Query`. Vous pouvez juste écrire : - -```Python -q: Union[str, None] = None -``` - -Ou bien, en Python 3.10 et supérieur : +Comme, par défaut, les valeurs singulières sont interprétées comme des paramètres de requête, vous n'avez pas besoin d'ajouter explicitement `Query`, vous pouvez simplement écrire : ```Python q: str | None = None ``` +Ou en Python 3.9 : + +```Python +q: Union[str, None] = None +``` + Par exemple : -{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[27] *} +{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} /// info -`Body` possède les mêmes paramètres de validation additionnels et de gestion des métadonnées que `Query` et `Path`, ainsi que d'autres que nous verrons plus tard. +`Body` possède également les mêmes paramètres supplémentaires de validation et de métadonnées que `Query`, `Path` et d'autres que vous verrez plus tard. /// -## Inclure un paramètre imbriqué dans le body +## Intégrer un seul paramètre du body { #embed-a-single-body-parameter } -Disons que vous avez seulement un paramètre `item` dans le body, correspondant à un modèle Pydantic `Item`. +Supposons que vous n'ayez qu'un seul paramètre `item` dans le body, provenant d'un modèle Pydantic `Item`. -Par défaut, **FastAPI** attendra sa déclaration directement dans le body. +Par défaut, **FastAPI** attendra alors son contenu directement. -Cependant, si vous souhaitez qu'il interprête correctement un JSON avec une clé `item` associée au contenu du modèle, comme cela serait le cas si vous déclariez des paramètres body additionnels, vous pouvez utiliser le paramètre spécial `embed` de `Body` : +Mais si vous voulez qu'il attende un JSON avec une clé `item` contenant le contenu du modèle, comme lorsqu'on déclare des paramètres supplémentaires du body, vous pouvez utiliser le paramètre spécial `embed` de `Body` : ```Python item: Item = Body(embed=True) ``` -Voici un exemple complet : +comme dans : {* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} -Dans ce cas **FastAPI** attendra un body semblable à : +Dans ce cas **FastAPI** s'attendra à un body semblable à : ```JSON hl_lines="2" { @@ -160,12 +160,12 @@ au lieu de : } ``` -## Pour résumer +## Récapitulatif { #recap } -Vous pouvez ajouter plusieurs paramètres body dans votre fonction de routage, même si une requête ne peut avoir qu'un seul body. +Vous pouvez ajouter plusieurs paramètres du body à votre fonction de chemin d'accès, même si une requête ne peut avoir qu'un seul body. -Cependant, **FastAPI** se chargera de faire opérer sa magie, afin de toujours fournir à votre fonction des données correctes, les validera et documentera le schéma associé. +Mais **FastAPI** s'en chargera, vous fournira les bonnes données dans votre fonction, et validera et documentera le schéma correct dans le chemin d'accès. -Vous pouvez également déclarer des valeurs [scalaires](https://docs.github.com/fr/graphql/reference/scalars) à recevoir dans le body. +Vous pouvez également déclarer des valeurs singulières à recevoir dans le body. -Et vous pouvez indiquer à **FastAPI** d'inclure le body dans une autre variable, même lorsqu'un seul paramètre est déclaré. +Et vous pouvez indiquer à **FastAPI** d'intégrer le body sous une clé même lorsqu'un seul paramètre est déclaré. diff --git a/docs/fr/docs/tutorial/body.md b/docs/fr/docs/tutorial/body.md index 760b6d80a..ca115fabc 100644 --- a/docs/fr/docs/tutorial/body.md +++ b/docs/fr/docs/tutorial/body.md @@ -1,10 +1,10 @@ -# Corps de la requête +# Corps de la requête { #request-body } Quand vous avez besoin d'envoyer de la donnée depuis un client (comme un navigateur) vers votre API, vous l'envoyez en tant que **corps de requête**. Le corps d'une **requête** est de la donnée envoyée par le client à votre API. Le corps d'une **réponse** est la donnée envoyée par votre API au client. -Votre API aura presque toujours à envoyer un corps de **réponse**. Mais un client n'a pas toujours à envoyer un corps de **requête**. +Votre API aura presque toujours à envoyer un corps de **réponse**. Mais un client n'a pas toujours à envoyer un **corps de requête** : parfois il demande seulement un chemin, peut-être avec quelques paramètres de requête, mais n'envoie pas de corps. Pour déclarer un corps de **requête**, on utilise les modèles de Pydantic en profitant de tous leurs avantages et fonctionnalités. @@ -18,23 +18,23 @@ Ceci étant découragé, la documentation interactive générée par Swagger UI /// -## Importez le `BaseModel` de Pydantic +## Importer le `BaseModel` de Pydantic { #import-pydantics-basemodel } Commencez par importer la classe `BaseModel` du module `pydantic` : -{* ../../docs_src/body/tutorial001.py hl[4] *} +{* ../../docs_src/body/tutorial001_py310.py hl[2] *} -## Créez votre modèle de données +## Créer votre modèle de données { #create-your-data-model } Déclarez ensuite votre modèle de données en tant que classe qui hérite de `BaseModel`. Utilisez les types Python standard pour tous les attributs : -{* ../../docs_src/body/tutorial001.py hl[7:11] *} +{* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} -Tout comme pour la déclaration de paramètres de requête, quand un attribut de modèle a une valeur par défaut, il n'est pas nécessaire. Sinon, cet attribut doit être renseigné dans le corps de la requête. Pour rendre ce champ optionnel simplement, utilisez `None` comme valeur par défaut. +Tout comme pour la déclaration de paramètres de requête, quand un attribut de modèle a une valeur par défaut, il n'est pas nécessaire. Sinon, cet attribut doit être renseigné dans le corps de la requête. Utilisez `None` pour le rendre simplement optionnel. -Par exemple, le modèle ci-dessus déclare un "objet" JSON (ou `dict` Python) tel que : +Par exemple, le modèle ci-dessus déclare un JSON « `object` » (ou `dict` Python) tel que : ```JSON { @@ -45,7 +45,7 @@ Par exemple, le modèle ci-dessus déclare un "objet" JSON (ou `dict` Python) te } ``` -...`description` et `tax` étant des attributs optionnels (avec `None` comme valeur par défaut), cet "objet" JSON serait aussi valide : +... `description` et `tax` étant des attributs optionnels (avec `None` comme valeur par défaut), ce JSON « `object` » serait aussi valide : ```JSON { @@ -54,28 +54,28 @@ Par exemple, le modèle ci-dessus déclare un "objet" JSON (ou `dict` Python) te } ``` -## Déclarez-le comme paramètre +## Le déclarer comme paramètre { #declare-it-as-a-parameter } Pour l'ajouter à votre *opération de chemin*, déclarez-le comme vous déclareriez des paramètres de chemin ou de requête : -{* ../../docs_src/body/tutorial001.py hl[18] *} +{* ../../docs_src/body/tutorial001_py310.py hl[16] *} -...et déclarez que son type est le modèle que vous avez créé : `Item`. +... et déclarez que son type est le modèle que vous avez créé : `Item`. -## Résultats +## Résultats { #results } En utilisant uniquement les déclarations de type Python, **FastAPI** réussit à : * Lire le contenu de la requête en tant que JSON. * Convertir les types correspondants (si nécessaire). * Valider la donnée. - * Si la donnée est invalide, une erreur propre et claire sera renvoyée, indiquant exactement où était la donnée incorrecte. + * Si la donnée est invalide, une erreur propre et claire sera renvoyée, indiquant exactement où et quelle était la donnée incorrecte. * Passer la donnée reçue dans le paramètre `item`. - * Ce paramètre ayant été déclaré dans la fonction comme étant de type `Item`, vous aurez aussi tout le support offert par l'éditeur (auto-complétion, etc.) pour tous les attributs de ce paramètre et les types de ces attributs. -* Générer des définitions JSON Schema pour votre modèle, qui peuvent être utilisées où vous en avez besoin dans votre projet ensuite. -* Ces schémas participeront à la constitution du schéma généré OpenAPI, et seront donc utilisés par les documentations automatiquement générées. + * Ce paramètre ayant été déclaré dans la fonction comme étant de type `Item`, vous aurez aussi tout le support offert par l'éditeur (autocomplétion, etc.) pour tous les attributs de ce paramètre et les types de ces attributs. +* Générer des définitions JSON Schema pour votre modèle ; vous pouvez également les utiliser partout ailleurs si cela a du sens pour votre projet. +* Ces schémas participeront à la constitution du schéma généré OpenAPI, et seront utilisés par les documentations automatiques UIs. -## Documentation automatique +## Documentation automatique { #automatic-docs } Les schémas JSON de vos modèles seront intégrés au schéma OpenAPI global de votre application, et seront donc affichés dans la documentation interactive de l'API : @@ -85,63 +85,63 @@ Et seront aussi utilisés dans chaque *opération de chemin* de la documentation -## Support de l'éditeur +## Support de l'éditeur { #editor-support } -Dans votre éditeur, vous aurez des annotations de types et de l'auto-complétion partout dans votre fonction (ce qui n'aurait pas été le cas si vous aviez utilisé un classique `dict` plutôt qu'un modèle Pydantic) : +Dans votre éditeur, vous aurez des annotations de type et de l'autocomplétion partout dans votre fonction (ce qui n'aurait pas été le cas si vous aviez reçu un `dict` plutôt qu'un modèle Pydantic) : -Et vous obtenez aussi de la vérification d'erreur pour les opérations incorrectes de types : +Et vous obtenez aussi des vérifications d'erreurs pour les opérations de types incorrectes : Ce n'est pas un hasard, ce framework entier a été bâti avec ce design comme objectif. -Et cela a été rigoureusement testé durant la phase de design, avant toute implémentation, pour s'assurer que cela fonctionnerait avec tous les éditeurs. +Et cela a été rigoureusement testé durant la phase de design, avant toute implémentation, pour vous assurer que cela fonctionnerait avec tous les éditeurs. Des changements sur Pydantic ont même été faits pour supporter cela. -Les captures d'écrans précédentes ont été prises sur Visual Studio Code. +Les captures d'écran précédentes ont été prises sur Visual Studio Code. -Mais vous auriez le même support de l'éditeur avec PyCharm et la majorité des autres éditeurs de code Python. +Mais vous auriez le même support de l'éditeur avec PyCharm et la majorité des autres éditeurs de code Python : /// tip | Astuce -Si vous utilisez PyCharm comme éditeur, vous pouvez utiliser le Plugin Pydantic PyCharm Plugin. +Si vous utilisez PyCharm comme éditeur, vous pouvez utiliser le plug-in Pydantic PyCharm Plugin. Ce qui améliore le support pour les modèles Pydantic avec : -* de l'auto-complétion +* de l'autocomplétion * des vérifications de type -* du "refactoring" (ou remaniement de code) +* du « refactoring » (ou remaniement de code) * de la recherche -* de l'inspection +* des inspections /// -## Utilisez le modèle +## Utiliser le modèle { #use-the-model } Dans la fonction, vous pouvez accéder à tous les attributs de l'objet du modèle directement : -{* ../../docs_src/body/tutorial002.py hl[21] *} +{* ../../docs_src/body/tutorial002_py310.py *} -## Corps de la requête + paramètres de chemin +## Corps de la requête + paramètres de chemin { #request-body-path-parameters } Vous pouvez déclarer des paramètres de chemin et un corps de requête pour la même *opération de chemin*. **FastAPI** est capable de reconnaître que les paramètres de la fonction qui correspondent aux paramètres de chemin doivent être **récupérés depuis le chemin**, et que les paramètres de fonctions déclarés comme modèles Pydantic devraient être **récupérés depuis le corps de la requête**. -{* ../../docs_src/body/tutorial003.py hl[17:18] *} +{* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} -## Corps de la requête + paramètres de chemin et de requête +## Corps de la requête + paramètres de chemin et de requête { #request-body-path-query-parameters } Vous pouvez aussi déclarer un **corps**, et des paramètres de **chemin** et de **requête** dans la même *opération de chemin*. **FastAPI** saura reconnaître chacun d'entre eux et récupérer la bonne donnée au bon endroit. -{* ../../docs_src/body/tutorial004.py hl[18] *} +{* ../../docs_src/body/tutorial004_py310.py hl[16] *} Les paramètres de la fonction seront reconnus comme tel : @@ -149,14 +149,16 @@ Les paramètres de la fonction seront reconnus comme tel : * Si le paramètre est d'un **type singulier** (comme `int`, `float`, `str`, `bool`, etc.), il sera interprété comme un paramètre de **requête**. * Si le paramètre est déclaré comme ayant pour type un **modèle Pydantic**, il sera interprété comme faisant partie du **corps** de la requête. -/// note +/// note | Remarque -**FastAPI** saura que la valeur de `q` n'est pas requise grâce à la valeur par défaut `=None`. +**FastAPI** saura que la valeur de `q` n'est pas requise grâce à la valeur par défaut `= None`. -Le type `Optional` dans `Optional[str]` n'est pas utilisé par **FastAPI**, mais sera utile à votre éditeur pour améliorer le support offert par ce dernier et détecter plus facilement des erreurs de type. +L'annotation de type `str | None` (Python 3.10+) ou `Union` dans `Union[str, None]` (Python 3.9+) n'est pas utilisée par **FastAPI** pour déterminer que la valeur n'est pas requise, il le saura parce qu'elle a une valeur par défaut `= None`. + +Mais ajouter ces annotations de type permettra à votre éditeur de vous offrir un meilleur support et de détecter des erreurs. /// -## Sans Pydantic +## Sans Pydantic { #without-pydantic } -Si vous ne voulez pas utiliser des modèles Pydantic, vous pouvez aussi utiliser des paramètres de **Corps**. Pour cela, allez voir la partie de la documentation sur [Corps de la requête - Paramètres multiples](body-multiple-params.md){.internal-link target=_blank}. +Si vous ne voulez pas utiliser des modèles Pydantic, vous pouvez aussi utiliser des paramètres de **Body**. Pour cela, allez voir la documentation sur [Corps de la requête - Paramètres multiples : Valeurs singulières dans le corps](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. diff --git a/docs/fr/docs/tutorial/debugging.md b/docs/fr/docs/tutorial/debugging.md index ab00fbdeb..a88fa2b23 100644 --- a/docs/fr/docs/tutorial/debugging.md +++ b/docs/fr/docs/tutorial/debugging.md @@ -1,14 +1,14 @@ -# Débogage +# Débogage { #debugging } Vous pouvez connecter le débogueur dans votre éditeur, par exemple avec Visual Studio Code ou PyCharm. -## Faites appel à `uvicorn` +## Appeler `uvicorn` { #call-uvicorn } Dans votre application FastAPI, importez et exécutez directement `uvicorn` : -{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} -### À propos de `__name__ == "__main__"` +### À propos de `__name__ == "__main__"` { #about-name-main } Le but principal de `__name__ == "__main__"` est d'avoir du code qui est exécuté lorsque votre fichier est appelé avec : @@ -26,7 +26,7 @@ mais qui n'est pas appelé lorsqu'un autre fichier l'importe, comme dans : from myapp import app ``` -#### Pour davantage de détails +#### Pour davantage de détails { #more-details } Imaginons que votre fichier s'appelle `myapp.py`. @@ -78,7 +78,7 @@ Pour plus d'informations, consultez débogueur +## Exécuter votre code avec votre débogueur { #run-your-code-with-your-debugger } Parce que vous exécutez le serveur Uvicorn directement depuis votre code, vous pouvez appeler votre programme Python (votre application FastAPI) directement depuis le débogueur. @@ -86,10 +86,10 @@ Parce que vous exécutez le serveur Uvicorn directement depuis votre code, vous Par exemple, dans Visual Studio Code, vous pouvez : -- Cliquer sur l'onglet "Debug" de la barre d'activités de Visual Studio Code. -- "Add configuration...". -- Sélectionnez "Python". -- Lancez le débogueur avec l'option "`Python: Current File (Integrated Terminal)`". +- Allez dans le panneau « Debug ». +- « Add configuration... ». +- Sélectionnez « Python ». +- Lancez le débogueur avec l'option « Python: Current File (Integrated Terminal) ». Il démarrera alors le serveur avec votre code **FastAPI**, s'arrêtera à vos points d'arrêt, etc. @@ -101,8 +101,8 @@ Voici à quoi cela pourrait ressembler : Si vous utilisez Pycharm, vous pouvez : -- Ouvrir le menu "Run". -- Sélectionnez l'option "Debug...". +- Ouvrez le menu « Run ». +- Sélectionnez l'option « Debug... ». - Un menu contextuel s'affiche alors. - Sélectionnez le fichier à déboguer (dans ce cas, `main.py`). diff --git a/docs/fr/docs/tutorial/first-steps.md b/docs/fr/docs/tutorial/first-steps.md index 96ea56e62..b2693b3e5 100644 --- a/docs/fr/docs/tutorial/first-steps.md +++ b/docs/fr/docs/tutorial/first-steps.md @@ -1,107 +1,122 @@ -# Démarrage +# Démarrage { #first-steps } -Le fichier **FastAPI** le plus simple possible pourrait ressembler à cela : +Le fichier **FastAPI** le plus simple possible pourrait ressembler à ceci : -{* ../../docs_src/first_steps/tutorial001.py *} +{* ../../docs_src/first_steps/tutorial001_py39.py *} -Copiez ce code dans un fichier nommé `main.py`. +Copiez cela dans un fichier `main.py`. -Démarrez le serveur : +Démarrez le serveur en direct :
```console -$ uvicorn main:app --reload +$ fastapi dev main.py -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: Waiting for application startup. -INFO: Application startup complete. + 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. ```
-/// note - -La commande `uvicorn main:app` fait référence à : - -* `main` : le fichier `main.py` (le module Python). -* `app` : l'objet créé dans `main.py` via la ligne `app = FastAPI()`. -* `--reload` : l'option disant à uvicorn de redémarrer le serveur à chaque changement du code. À ne pas utiliser en production ! - -/// - -Vous devriez voir dans la console, une ligne semblable à la suivante : +Dans la sortie, il y a une ligne semblable à : ```hl_lines="4" INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` -Cette ligne montre l'URL par laquelle l'app est actuellement accessible, sur votre machine locale. +Cette ligne montre l’URL où votre application est servie, sur votre machine locale. -### Allez voir le résultat +### Vérifiez { #check-it } -Ouvrez votre navigateur à l'adresse
http://127.0.0.1:8000. +Ouvrez votre navigateur à l’adresse http://127.0.0.1:8000. -Vous obtiendrez cette réponse JSON : +Vous verrez la réponse JSON suivante : ```JSON {"message": "Hello World"} ``` -### Documentation interactive de l'API +### Documentation interactive de l’API { #interactive-api-docs } -Rendez-vous sur http://127.0.0.1:8000/docs. +Allez maintenant sur http://127.0.0.1:8000/docs. -Vous verrez la documentation interactive de l'API générée automatiquement (via Swagger UI) : +Vous verrez la documentation interactive de l’API générée automatiquement (fournie par Swagger UI) : ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Documentation alternative +### Documentation alternative de l’API { #alternative-api-docs } -Ensuite, rendez-vous sur http://127.0.0.1:8000/redoc. +Et maintenant, allez sur http://127.0.0.1:8000/redoc. -Vous y verrez la documentation alternative (via ReDoc) : +Vous verrez la documentation automatique alternative (fournie par ReDoc) : ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -### OpenAPI +### OpenAPI { #openapi } -**FastAPI** génère un "schéma" contenant toute votre API dans le standard de définition d'API **OpenAPI**. +**FastAPI** génère un « schéma » contenant toute votre API en utilisant le standard **OpenAPI** pour définir des API. -#### "Schéma" +#### « Schéma » { #schema } -Un "schéma" est une définition ou une description de quelque chose. Pas le code qui l'implémente, uniquement une description abstraite. +Un « schéma » est une définition ou une description de quelque chose. Pas le code qui l’implémente, mais uniquement une description abstraite. -#### "Schéma" d'API +#### « Schéma » d’API { #api-schema } Ici, OpenAPI est une spécification qui dicte comment définir le schéma de votre API. -Le schéma inclut les chemins de votre API, les paramètres potentiels de chaque chemin, etc. +Cette définition de schéma inclut les chemins de votre API, les paramètres possibles qu’ils prennent, etc. -#### "Schéma" de données +#### « Schéma » de données { #data-schema } -Le terme "schéma" peut aussi faire référence à la forme de la donnée, comme un contenu JSON. +Le terme « schéma » peut également faire référence à la forme d’une donnée, comme un contenu JSON. -Dans ce cas, cela signifierait les attributs JSON, ainsi que les types de ces attributs, etc. +Dans ce cas, cela désignerait les attributs JSON, ainsi que leurs types, etc. -#### OpenAPI et JSON Schema +#### OpenAPI et JSON Schema { #openapi-and-json-schema } -**OpenAPI** définit un schéma d'API pour votre API. Il inclut des définitions (ou "schémas") de la donnée envoyée et reçue par votre API en utilisant **JSON Schema**, le standard des schémas de données JSON. +OpenAPI définit un schéma d’API pour votre API. Et ce schéma inclut des définitions (ou « schémas ») des données envoyées et reçues par votre API en utilisant **JSON Schema**, le standard pour les schémas de données JSON. -#### Allez voir `openapi.json` +#### Voir le `openapi.json` { #check-the-openapi-json } -Si vous êtes curieux d'à quoi ressemble le schéma brut **OpenAPI**, **FastAPI** génère automatiquement un (schéma) JSON avec les descriptions de toute votre API. +Si vous êtes curieux de voir à quoi ressemble le schéma OpenAPI brut, FastAPI génère automatiquement un JSON (schéma) avec les descriptions de toute votre API. -Vous pouvez le voir directement à cette adresse : http://127.0.0.1:8000/openapi.json. - -Le schéma devrait ressembler à ceci : +Vous pouvez le voir directement à l’adresse : http://127.0.0.1:8000/openapi.json. +Il affichera un JSON commençant par quelque chose comme : ```JSON { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" @@ -120,79 +135,87 @@ Le schéma devrait ressembler à ceci : ... ``` -#### À quoi sert OpenAPI +#### À quoi sert OpenAPI { #what-is-openapi-for } -Le schéma **OpenAPI** est ce qui alimente les deux systèmes de documentation interactive. +Le schéma OpenAPI est ce qui alimente les deux systèmes de documentation interactive inclus. -Et il existe des dizaines d'alternatives, toutes basées sur **OpenAPI**. Vous pourriez facilement ajouter n'importe laquelle de ces alternatives à votre application **FastAPI**. +Et il existe des dizaines d’alternatives, toutes basées sur OpenAPI. Vous pourriez facilement ajouter n’importe laquelle de ces alternatives à votre application construite avec **FastAPI**. -Vous pourriez aussi l'utiliser pour générer du code automatiquement, pour les clients qui communiquent avec votre API. Comme par exemple, des applications frontend, mobiles ou IOT. +Vous pourriez également l’utiliser pour générer du code automatiquement, pour les clients qui communiquent avec votre API. Par exemple, des applications frontend, mobiles ou IoT. -## Récapitulatif, étape par étape +### Déployer votre application (optionnel) { #deploy-your-app-optional } -### Étape 1 : import `FastAPI` +Vous pouvez, si vous le souhaitez, déployer votre application FastAPI sur FastAPI Cloud, allez rejoindre la liste d’attente si ce n’est pas déjà fait. 🚀 -{* ../../docs_src/first_steps/tutorial001.py hl[1] *} +Si vous avez déjà un compte **FastAPI Cloud** (nous vous avons invité depuis la liste d’attente 😉), vous pouvez déployer votre application avec une seule commande. -`FastAPI` est une classe Python qui fournit toutes les fonctionnalités nécessaires au lancement de votre API. +Avant de déployer, vous devez vous assurer que vous êtes connecté : + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +Puis déployez votre application : + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +C’est tout ! Vous pouvez maintenant accéder à votre application à cette URL. ✨ + +## Récapitulatif, étape par étape { #recap-step-by-step } + +### Étape 1 : importer `FastAPI` { #step-1-import-fastapi } + +{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *} + +`FastAPI` est une classe Python qui fournit toutes les fonctionnalités nécessaires à votre API. /// note | Détails techniques -`FastAPI` est une classe héritant directement de `Starlette`. +`FastAPI` est une classe qui hérite directement de `Starlette`. -Vous pouvez donc aussi utiliser toutes les fonctionnalités de Starlette depuis `FastAPI`. +Vous pouvez donc aussi utiliser toutes les fonctionnalités de Starlette avec `FastAPI`. /// -### Étape 2 : créer une "instance" `FastAPI` +### Étape 2 : créer une « instance » `FastAPI` { #step-2-create-a-fastapi-instance } -{* ../../docs_src/first_steps/tutorial001.py hl[3] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[3] *} -Ici la variable `app` sera une "instance" de la classe `FastAPI`. +Ici, la variable `app` sera une « instance » de la classe `FastAPI`. -Ce sera le point principal d'interaction pour créer toute votre API. +Ce sera le point principal d’interaction pour créer toute votre API. -Cette `app` est la même que celle à laquelle fait référence `uvicorn` dans la commande : +### Étape 3 : créer un « chemin d’accès » { #step-3-create-a-path-operation } -
+#### Chemin { #path } -```console -$ uvicorn main:app --reload +« Chemin » fait ici référence à la dernière partie de l’URL à partir du premier `/`. -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -Si vous créez votre app avec : - -{* ../../docs_src/first_steps/tutorial002.py hl[3] *} - -Et la mettez dans un fichier `main.py`, alors vous appelleriez `uvicorn` avec : - -
- -```console -$ uvicorn main:my_awesome_api --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -### Étape 3: créer une *opération de chemin* - -#### Chemin - -Chemin, ou "path" fait référence ici à la dernière partie de l'URL démarrant au premier `/`. - -Donc, dans un URL tel que : +Donc, dans une URL telle que : ``` https://example.com/items/foo ``` -...le "path" serait : +... le chemin serait : ``` /items/foo @@ -200,66 +223,67 @@ https://example.com/items/foo /// info -Un chemin, ou "path" est aussi souvent appelé route ou "endpoint". +Un « chemin » est aussi couramment appelé « endpoint » ou « route ». /// -#### Opération +Lors de la création d’une API, le « chemin » est la manière principale de séparer les « préoccupations » et les « ressources ». -"Opération" fait référence à une des "méthodes" HTTP. +#### Opération { #operation } -Une de : +« Opération » fait ici référence à l’une des « méthodes » HTTP. + +L’une de : * `POST` * `GET` * `PUT` * `DELETE` -...ou une des plus exotiques : +... et les plus exotiques : * `OPTIONS` * `HEAD` * `PATCH` * `TRACE` -Dans le protocol HTTP, vous pouvez communiquer avec chaque chemin en utilisant une (ou plus) de ces "méthodes". +Dans le protocole HTTP, vous pouvez communiquer avec chaque chemin en utilisant une (ou plusieurs) de ces « méthodes ». --- -En construisant des APIs, vous utilisez généralement ces méthodes HTTP spécifiques pour effectuer une action précise. +En construisant des APIs, vous utilisez normalement ces méthodes HTTP spécifiques pour effectuer une action précise. -Généralement vous utilisez : +En général, vous utilisez : -* `POST` : pour créer de la donnée. -* `GET` : pour lire de la donnée. -* `PUT` : pour mettre à jour de la donnée. -* `DELETE` : pour supprimer de la donnée. +* `POST` : pour créer des données. +* `GET` : pour lire des données. +* `PUT` : pour mettre à jour des données. +* `DELETE` : pour supprimer des données. -Donc, dans **OpenAPI**, chaque méthode HTTP est appelée une "opération". +Donc, dans OpenAPI, chacune des méthodes HTTP est appelée une « opération ». -Nous allons donc aussi appeler ces dernières des "**opérations**". +Nous allons donc aussi les appeler « opérations ». +#### Définir un « décorateur de chemin d’accès » { #define-a-path-operation-decorator } -#### Définir un *décorateur d'opération de chemin* +{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *} -{* ../../docs_src/first_steps/tutorial001.py hl[6] *} - -Le `@app.get("/")` dit à **FastAPI** que la fonction en dessous est chargée de gérer les requêtes qui vont sur : +Le `@app.get("/")` indique à **FastAPI** que la fonction juste en dessous est chargée de gérer les requêtes qui vont vers : * le chemin `/` -* en utilisant une opération get +* en utilisant une get opération /// info | `@décorateur` Info -Cette syntaxe `@something` en Python est appelée un "décorateur". +Cette syntaxe `@something` en Python est appelée un « décorateur ». -Vous la mettez au dessus d'une fonction. Comme un joli chapeau décoratif (j'imagine que ce terme vient de là 🤷🏻‍♂). +Vous la mettez au-dessus d’une fonction. Comme un joli chapeau décoratif (j’imagine que c’est de là que vient le terme 🤷🏻‍♂). -Un "décorateur" prend la fonction en dessous et en fait quelque chose. +Un « décorateur » prend la fonction en dessous et fait quelque chose avec. -Dans notre cas, ce décorateur dit à **FastAPI** que la fonction en dessous correspond au **chemin** `/` avec l'**opération** `get`. +Dans notre cas, ce décorateur indique à **FastAPI** que la fonction en dessous correspond au **chemin** `/` avec une **opération** `get`. -C'est le "**décorateur d'opération de chemin**". +C’est le « décorateur de chemin d’accès ». /// @@ -269,7 +293,7 @@ Vous pouvez aussi utiliser les autres opérations : * `@app.put()` * `@app.delete()` -Tout comme celles les plus exotiques : +Ainsi que les plus exotiques : * `@app.options()` * `@app.head()` @@ -278,58 +302,79 @@ Tout comme celles les plus exotiques : /// tip | Astuce -Vous êtes libres d'utiliser chaque opération (méthode HTTP) comme vous le désirez. +Vous êtes libre d’utiliser chaque opération (méthode HTTP) comme vous le souhaitez. -**FastAPI** n'impose pas de sens spécifique à chacune d'elle. +**FastAPI** n’impose aucune signification spécifique. -Les informations qui sont présentées ici forment une directive générale, pas des obligations. +Les informations ici sont présentées comme des lignes directrices, pas comme une obligation. -Par exemple, quand l'on utilise **GraphQL**, toutes les actions sont effectuées en utilisant uniquement des opérations `POST`. +Par exemple, lorsque vous utilisez GraphQL, vous effectuez normalement toutes les actions en utilisant uniquement des opérations `POST`. /// -### Étape 4 : définir la **fonction de chemin**. +### Étape 4 : définir la **fonction de chemin d’accès** { #step-4-define-the-path-operation-function } -Voici notre "**fonction de chemin**" (ou fonction d'opération de chemin) : +Voici notre « fonction de chemin d’accès » : * **chemin** : `/`. * **opération** : `get`. -* **fonction** : la fonction sous le "décorateur" (sous `@app.get("/")`). +* **fonction** : la fonction sous le « décorateur » (sous `@app.get("/")`). -{* ../../docs_src/first_steps/tutorial001.py hl[7] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *} -C'est une fonction Python. +C’est une fonction Python. -Elle sera appelée par **FastAPI** quand une requête sur l'URL `/` sera reçue via une opération `GET`. +Elle sera appelée par **FastAPI** chaque fois qu’il recevra une requête vers l’URL « / » en utilisant une opération `GET`. -Ici, c'est une fonction asynchrone (définie avec `async def`). +Dans ce cas, c’est une fonction `async`. --- -Vous pourriez aussi la définir comme une fonction classique plutôt qu'avec `async def` : +Vous pouvez aussi la définir comme une fonction normale au lieu de `async def` : -{* ../../docs_src/first_steps/tutorial003.py hl[7] *} +{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *} /// note -Si vous ne connaissez pas la différence, allez voir la section [Concurrence : *"Vous êtes pressés ?"*](../async.md#vous-etes-presses){.internal-link target=_blank}. +Si vous ne connaissez pas la différence, consultez [Asynchrone : « Pressé ? »](../async.md#in-a-hurry){.internal-link target=_blank}. /// -### Étape 5 : retourner le contenu +### Étape 5 : retourner le contenu { #step-5-return-the-content } -{* ../../docs_src/first_steps/tutorial001.py hl[8] *} +{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *} -Vous pouvez retourner un dictionnaire (`dict`), une liste (`list`), des valeurs seules comme des chaines de caractères (`str`) et des entiers (`int`), etc. +Vous pouvez retourner un `dict`, une `list`, des valeurs uniques comme `str`, `int`, etc. -Vous pouvez aussi retourner des models **Pydantic** (qui seront détaillés plus tard). +Vous pouvez également retourner des modèles Pydantic (vous en verrez plus à ce sujet plus tard). -Il y a de nombreux autres objets et modèles qui seront automatiquement convertis en JSON. Essayez d'utiliser vos favoris, il est fort probable qu'ils soient déjà supportés. +Il existe de nombreux autres objets et modèles qui seront automatiquement convertis en JSON (y compris des ORM, etc.). Essayez d’utiliser vos favoris, il est fort probable qu’ils soient déjà pris en charge. -## Récapitulatif +### Étape 6 : le déployer { #step-6-deploy-it } + +Déployez votre application sur **FastAPI Cloud** avec une seule commande : `fastapi deploy`. 🎉 + +#### À propos de FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** est construit par le même auteur et l’équipe derrière **FastAPI**. + +Il simplifie le processus de **construction**, de **déploiement** et d’**accès** à une API avec un minimum d’effort. + +Il apporte la même **expérience développeur** de création d’applications avec FastAPI au **déploiement** dans le cloud. 🎉 + +FastAPI Cloud est le sponsor principal et le financeur des projets open source *FastAPI and friends*. ✨ + +#### Déployer sur d’autres fournisseurs cloud { #deploy-to-other-cloud-providers } + +FastAPI est open source et basé sur des standards. Vous pouvez déployer des applications FastAPI chez n’importe quel fournisseur cloud de votre choix. + +Suivez les guides de votre fournisseur cloud pour y déployer des applications FastAPI. 🤓 + +## Récapitulatif { #recap } * Importez `FastAPI`. -* Créez une instance d'`app`. -* Ajoutez une **décorateur d'opération de chemin** (tel que `@app.get("/")`). -* Ajoutez une **fonction de chemin** (telle que `def root(): ...` comme ci-dessus). -* Lancez le serveur de développement (avec `uvicorn main:app --reload`). +* Créez une instance `app`. +* Écrivez un **décorateur de chemin d’accès** avec des décorateurs comme `@app.get("/")`. +* Définissez une **fonction de chemin d’accès** ; par exemple, `def root(): ...`. +* Exécutez le serveur de développement avec la commande `fastapi dev`. +* Déployez éventuellement votre application avec `fastapi deploy`. diff --git a/docs/fr/docs/tutorial/index.md b/docs/fr/docs/tutorial/index.md index 83cc5f9e8..0251b9b4b 100644 --- a/docs/fr/docs/tutorial/index.md +++ b/docs/fr/docs/tutorial/index.md @@ -1,29 +1,53 @@ -# Tutoriel - Guide utilisateur - Introduction +# Tutoriel - Guide utilisateur { #tutorial-user-guide } Ce tutoriel vous montre comment utiliser **FastAPI** avec la plupart de ses fonctionnalités, étape par étape. -Chaque section s'appuie progressivement sur les précédentes, mais elle est structurée de manière à séparer les sujets, afin que vous puissiez aller directement à l'un d'entre eux pour résoudre vos besoins spécifiques en matière d'API. +Chaque section s'appuie progressivement sur les précédentes, mais elle est structurée de manière à séparer les sujets, afin que vous puissiez aller directement à l'un d'entre eux pour répondre à vos besoins spécifiques d'API. -Il est également conçu pour fonctionner comme une référence future. +Il est également conçu pour servir de référence ultérieure, afin que vous puissiez revenir voir exactement ce dont vous avez besoin. -Vous pouvez donc revenir et voir exactement ce dont vous avez besoin. - -## Exécuter le code +## Exécuter le code { #run-the-code } Tous les blocs de code peuvent être copiés et utilisés directement (il s'agit en fait de fichiers Python testés). -Pour exécuter l'un de ces exemples, copiez le code dans un fichier `main.py`, et commencez `uvicorn` avec : +Pour exécuter l'un de ces exemples, copiez le code dans un fichier `main.py`, et démarrez `fastapi dev` avec :
```console -$ uvicorn main:app --reload +$ fastapi dev main.py -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: Waiting for application startup. -INFO: Application startup complete. + 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. ```
@@ -34,45 +58,33 @@ L'utiliser dans votre éditeur est ce qui vous montre vraiment les avantages de --- -## Installer FastAPI +## Installer FastAPI { #install-fastapi } La première étape consiste à installer FastAPI. -Pour le tutoriel, vous voudrez peut-être l'installer avec toutes les dépendances et fonctionnalités optionnelles : +Assurez-vous de créer un [environnement virtuel](../virtual-environments.md){.internal-link target=_blank}, de l'activer, puis **d'installer FastAPI** :
```console -$ pip install fastapi[all] +$ pip install "fastapi[standard]" ---> 100% ```
-... qui comprend également `uvicorn`, que vous pouvez utiliser comme serveur pour exécuter votre code. +/// note | Remarque -/// note +Lorsque vous installez avec `pip install "fastapi[standard]"` cela inclut des dépendances standard optionnelles par défaut, y compris `fastapi-cloud-cli`, qui vous permet de déployer sur FastAPI Cloud. -Vous pouvez également l'installer pièce par pièce. +Si vous ne souhaitez pas avoir ces dépendances optionnelles, vous pouvez à la place installer `pip install fastapi`. -C'est ce que vous feriez probablement une fois que vous voudrez déployer votre application en production : - -``` -pip install fastapi -``` - -Installez également `uvicorn` pour qu'il fonctionne comme serveur : - -``` -pip install uvicorn -``` - -Et la même chose pour chacune des dépendances facultatives que vous voulez utiliser. +Si vous souhaitez installer les dépendances standard mais sans `fastapi-cloud-cli`, vous pouvez installer avec `pip install "fastapi[standard-no-fastapi-cloud-cli]"`. /// -## Guide utilisateur avancé +## Guide d'utilisation avancé { #advanced-user-guide } Il existe également un **Guide d'utilisation avancé** que vous pouvez lire plus tard après ce **Tutoriel - Guide d'utilisation**. diff --git a/docs/fr/docs/tutorial/path-params-numeric-validations.md b/docs/fr/docs/tutorial/path-params-numeric-validations.md index 3f3280e64..c80710777 100644 --- a/docs/fr/docs/tutorial/path-params-numeric-validations.md +++ b/docs/fr/docs/tutorial/path-params-numeric-validations.md @@ -1,8 +1,8 @@ -# Paramètres de chemin et validations numériques +# Paramètres de chemin et validations numériques { #path-parameters-and-numeric-validations } De la même façon que vous pouvez déclarer plus de validations et de métadonnées pour les paramètres de requête avec `Query`, vous pouvez déclarer le même type de validations et de métadonnées pour les paramètres de chemin avec `Path`. -## Importer Path +## Importer `Path` { #import-path } Tout d'abord, importez `Path` de `fastapi`, et importez `Annotated` : @@ -14,11 +14,11 @@ FastAPI a ajouté le support pour `Annotated` (et a commencé à le recommander) Si vous avez une version plus ancienne, vous obtiendrez des erreurs en essayant d'utiliser `Annotated`. -Assurez-vous de [Mettre à jour la version de FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} à la version 0.95.1 à minima avant d'utiliser `Annotated`. +Assurez-vous de [Mettre à niveau la version de FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} à la version 0.95.1 à minima avant d'utiliser `Annotated`. /// -## Déclarer des métadonnées +## Déclarer des métadonnées { #declare-metadata } Vous pouvez déclarer les mêmes paramètres que pour `Query`. @@ -26,15 +26,15 @@ Par exemple, pour déclarer une valeur de métadonnée `title` pour le paramètr {* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} -/// note +/// note | Remarque Un paramètre de chemin est toujours requis car il doit faire partie du chemin. Même si vous l'avez déclaré avec `None` ou défini une valeur par défaut, cela ne changerait rien, il serait toujours requis. /// -## Ordonnez les paramètres comme vous le souhaitez +## Ordonner les paramètres comme vous le souhaitez { #order-the-parameters-as-you-need } -/// tip +/// tip | Astuce Ce n'est probablement pas aussi important ou nécessaire si vous utilisez `Annotated`. @@ -46,7 +46,7 @@ Et vous n'avez pas besoin de déclarer autre chose pour ce paramètre, donc vous Mais vous avez toujours besoin d'utiliser `Path` pour le paramètre de chemin `item_id`. Et vous ne voulez pas utiliser `Annotated` pour une raison quelconque. -Python se plaindra si vous mettez une valeur avec une "défaut" avant une valeur qui n'a pas de "défaut". +Python se plaindra si vous mettez une valeur avec une « valeur par défaut » avant une valeur qui n'a pas de « valeur par défaut ». Mais vous pouvez les réorganiser, et avoir la valeur sans défaut (le paramètre de requête `q`) en premier. @@ -54,15 +54,15 @@ Cela n'a pas d'importance pour **FastAPI**. Il détectera les paramètres par le Ainsi, vous pouvez déclarer votre fonction comme suit : -{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} Mais gardez à l'esprit que si vous utilisez `Annotated`, vous n'aurez pas ce problème, cela n'aura pas d'importance car vous n'utilisez pas les valeurs par défaut des paramètres de fonction pour `Query()` ou `Path()`. -{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py hl[10] *} +{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} -## Ordonnez les paramètres comme vous le souhaitez (astuces) +## Ordonner les paramètres comme vous le souhaitez, astuces { #order-the-parameters-as-you-need-tricks } -/// tip +/// tip | Astuce Ce n'est probablement pas aussi important ou nécessaire si vous utilisez `Annotated`. @@ -77,38 +77,29 @@ Si vous voulez : * les avoir dans un ordre différent * ne pas utiliser `Annotated` -...Python a une petite syntaxe spéciale pour cela. +... Python a une petite syntaxe spéciale pour cela. Passez `*`, comme premier paramètre de la fonction. Python ne fera rien avec ce `*`, mais il saura que tous les paramètres suivants doivent être appelés comme arguments "mots-clés" (paires clé-valeur), également connus sous le nom de kwargs. Même s'ils n'ont pas de valeur par défaut. -{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *} +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} -# Avec `Annotated` +### Mieux avec `Annotated` { #better-with-annotated } Gardez à l'esprit que si vous utilisez `Annotated`, comme vous n'utilisez pas les valeurs par défaut des paramètres de fonction, vous n'aurez pas ce problème, et vous n'aurez probablement pas besoin d'utiliser `*`. {* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} -## Validations numériques : supérieur ou égal +## Validations numériques : supérieur ou égal { #number-validations-greater-than-or-equal } Avec `Query` et `Path` (et d'autres que vous verrez plus tard) vous pouvez déclarer des contraintes numériques. -Ici, avec `ge=1`, `item_id` devra être un nombre entier "`g`reater than or `e`qual" à `1`. +Ici, avec `ge=1`, `item_id` devra être un nombre entier « `g`reater than or `e`qual » à `1`. {* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} -## Validations numériques : supérieur ou égal et inférieur ou égal - -La même chose s'applique pour : - -* `gt` : `g`reater `t`han -* `le` : `l`ess than or `e`qual - -{* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} - -## Validations numériques : supérieur et inférieur ou égal +## Validations numériques : supérieur et inférieur ou égal { #number-validations-greater-than-and-less-than-or-equal } La même chose s'applique pour : @@ -117,7 +108,7 @@ La même chose s'applique pour : {* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *} -## Validations numériques : flottants, supérieur et inférieur +## Validations numériques : flottants, supérieur et inférieur { #number-validations-floats-greater-than-and-less-than } Les validations numériques fonctionnent également pour les valeurs `float`. @@ -129,7 +120,7 @@ Et la même chose pour lt. {* ../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py hl[13] *} -## Pour résumer +## Pour résumer { #recap } Avec `Query`, `Path` (et d'autres que vous verrez plus tard) vous pouvez déclarer des métadonnées et des validations de chaînes de la même manière qu'avec les [Paramètres de requête et validations de chaînes](query-params-str-validations.md){.internal-link target=_blank}. diff --git a/docs/fr/docs/tutorial/path-params.md b/docs/fr/docs/tutorial/path-params.md index 71c96b18e..3b2955a95 100644 --- a/docs/fr/docs/tutorial/path-params.md +++ b/docs/fr/docs/tutorial/path-params.md @@ -1,205 +1,196 @@ -# Paramètres de chemin +# Paramètres de chemin { #path-parameters } -Vous pouvez déclarer des "paramètres" ou "variables" de chemin avec la même syntaxe que celle utilisée par le -formatage de chaîne Python : +Vous pouvez déclarer des « paramètres » ou « variables » de chemin avec la même syntaxe utilisée par les chaînes de format Python : +{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *} -{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} +La valeur du paramètre de chemin `item_id` sera transmise à votre fonction dans l'argument `item_id`. -La valeur du paramètre `item_id` sera transmise à la fonction dans l'argument `item_id`. - -Donc, si vous exécutez cet exemple et allez sur http://127.0.0.1:8000/items/foo, -vous verrez comme réponse : +Donc, si vous exécutez cet exemple et allez sur http://127.0.0.1:8000/items/foo, vous verrez comme réponse : ```JSON {"item_id":"foo"} ``` -## Paramètres de chemin typés +## Paramètres de chemin typés { #path-parameters-with-types } -Vous pouvez déclarer le type d'un paramètre de chemin dans la fonction, en utilisant les annotations de type Python : +Vous pouvez déclarer le type d'un paramètre de chemin dans la fonction, en utilisant les annotations de type Python standard : - -{* ../../docs_src/path_params/tutorial002.py hl[7] *} +{* ../../docs_src/path_params/tutorial002_py39.py hl[7] *} Ici, `item_id` est déclaré comme `int`. -/// check | vérifier +/// check | Vérifications -Ceci vous permettra d'obtenir des fonctionnalités de l'éditeur dans votre fonction, telles -que des vérifications d'erreur, de l'auto-complétion, etc. +Cela vous apporte la prise en charge par l'éditeur dans votre fonction, avec vérifications d'erreurs, autocomplétion, etc. /// -## Conversion de données +## Conversion de données { #data-conversion } -Si vous exécutez cet exemple et allez sur http://127.0.0.1:8000/items/3, vous aurez comme réponse : +Si vous exécutez cet exemple et ouvrez votre navigateur sur http://127.0.0.1:8000/items/3, vous verrez comme réponse : ```JSON {"item_id":3} ``` -/// check | vérifier +/// check | Vérifications -Comme vous l'avez remarqué, la valeur reçue par la fonction (et renvoyée ensuite) est `3`, -en tant qu'entier (`int`) Python, pas la chaîne de caractères (`string`) `"3"`. +Remarquez que la valeur reçue par votre fonction (et renvoyée) est `3`, en tant qu'entier (`int`) Python, pas la chaîne de caractères « 3 ». -Grâce aux déclarations de types, **FastAPI** fournit du -"parsing" automatique. +Ainsi, avec cette déclaration de type, **FastAPI** vous fournit automatiquement le « parsing » de la requête. /// -## Validation de données +## Validation de données { #data-validation } -Si vous allez sur http://127.0.0.1:8000/items/foo, vous aurez une belle erreur HTTP : +Mais si vous allez dans le navigateur sur http://127.0.0.1:8000/items/foo, vous verrez une belle erreur HTTP : ```JSON { - "detail": [ - { - "loc": [ - "path", - "item_id" - ], - "msg": "value is not a valid integer", - "type": "type_error.integer" - } - ] + "detail": [ + { + "type": "int_parsing", + "loc": [ + "path", + "item_id" + ], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo" + } + ] } ``` -car le paramètre de chemin `item_id` possède comme valeur `"foo"`, qui ne peut pas être convertie en entier (`int`). +car le paramètre de chemin `item_id` a pour valeur « foo », qui n'est pas un `int`. -La même erreur se produira si vous passez un nombre flottant (`float`) et non un entier, comme ici -http://127.0.0.1:8000/items/4.2. +La même erreur apparaîtrait si vous fournissiez un `float` au lieu d'un `int`, comme ici : http://127.0.0.1:8000/items/4.2 +/// check | Vérifications -/// check | vérifier +Ainsi, avec la même déclaration de type Python, **FastAPI** vous fournit la validation de données. -Donc, avec ces mêmes déclarations de type Python, **FastAPI** vous fournit de la validation de données. +Remarquez que l'erreur indique clairement l'endroit exact où la validation n'a pas réussi. -Notez que l'erreur mentionne le point exact où la validation n'a pas réussi. - -Ce qui est incroyablement utile au moment de développer et débugger du code qui interagit avec votre API. +C'est incroyablement utile lors du développement et du débogage du code qui interagit avec votre API. /// -## Documentation +## Documentation { #documentation } -Et quand vous vous rendez sur http://127.0.0.1:8000/docs, vous verrez la -documentation générée automatiquement et interactive : +Et lorsque vous ouvrez votre navigateur sur http://127.0.0.1:8000/docs, vous verrez une documentation d'API automatique et interactive comme : -/// info +/// check | Vérifications -À nouveau, en utilisant uniquement les déclarations de type Python, **FastAPI** vous fournit automatiquement une documentation interactive (via Swagger UI). +À nouveau, simplement avec cette même déclaration de type Python, **FastAPI** vous fournit une documentation interactive automatique (intégrant Swagger UI). -On voit bien dans la documentation que `item_id` est déclaré comme entier. +Remarquez que le paramètre de chemin est déclaré comme entier. /// -## Les avantages d'avoir une documentation basée sur une norme, et la documentation alternative. +## Les avantages d'une norme, documentation alternative { #standards-based-benefits-alternative-documentation } -Le schéma généré suivant la norme OpenAPI, -il existe de nombreux outils compatibles. +Et comme le schéma généré suit la norme OpenAPI, il existe de nombreux outils compatibles. -Grâce à cela, **FastAPI** lui-même fournit une documentation alternative (utilisant ReDoc), qui peut être lue -sur http://127.0.0.1:8000/redoc : +Grâce à cela, **FastAPI** fournit lui-même une documentation d'API alternative (utilisant ReDoc), accessible sur http://127.0.0.1:8000/redoc : -De la même façon, il existe bien d'autres outils compatibles, y compris des outils de génération de code -pour de nombreux langages. +De la même façon, il existe de nombreux outils compatibles, y compris des outils de génération de code pour de nombreux langages. -## Pydantic +## Pydantic { #pydantic } -Toute la validation de données est effectué en arrière-plan avec Pydantic, -dont vous bénéficierez de tous les avantages. Vous savez donc que vous êtes entre de bonnes mains. +Toute la validation de données est effectuée sous le capot par Pydantic, vous en bénéficiez donc pleinement. Vous savez ainsi que vous êtes entre de bonnes mains. -## L'ordre importe +Vous pouvez utiliser les mêmes déclarations de type avec `str`, `float`, `bool` et de nombreux autres types de données complexes. -Quand vous créez des *fonctions de chemins*, vous pouvez vous retrouver dans une situation où vous avez un chemin fixe. +Plusieurs d'entre eux sont explorés dans les prochains chapitres du tutoriel. -Tel que `/users/me`, disons pour récupérer les données sur l'utilisateur actuel. +## L'ordre importe { #order-matters } -Et vous avez un second chemin : `/users/{user_id}` pour récupérer de la donnée sur un utilisateur spécifique grâce à son identifiant d'utilisateur +Quand vous créez des *chemins d'accès*, vous pouvez vous retrouver dans une situation avec un chemin fixe. -Les *fonctions de chemin* étant évaluées dans l'ordre, il faut s'assurer que la fonction correspondant à `/users/me` est déclarée avant celle de `/users/{user_id}` : +Par exemple `/users/me`, disons pour récupérer les données de l'utilisateur actuel. -{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} +Et vous pouvez aussi avoir un chemin `/users/{user_id}` pour récupérer des données sur un utilisateur spécifique grâce à un identifiant d'utilisateur. -Sinon, le chemin `/users/{user_id}` correspondrait aussi à `/users/me`, la fonction "croyant" qu'elle a reçu un paramètre `user_id` avec pour valeur `"me"`. +Comme les *chemins d'accès* sont évalués dans l'ordre, vous devez vous assurer que le chemin `/users/me` est déclaré avant celui de `/users/{user_id}` : -## Valeurs prédéfinies +{* ../../docs_src/path_params/tutorial003_py39.py hl[6,11] *} -Si vous avez une *fonction de chemin* qui reçoit un *paramètre de chemin*, mais que vous voulez que les valeurs possibles des paramètres soient prédéfinies, vous pouvez utiliser les `Enum` de Python. +Sinon, le chemin `/users/{user_id}` correspondrait aussi à `/users/me`, « pensant » qu'il reçoit un paramètre `user_id` avec la valeur « me ». -### Création d'un `Enum` +De même, vous ne pouvez pas redéfinir un chemin d'accès : -Importez `Enum` et créez une sous-classe qui hérite de `str` et `Enum`. +{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *} -En héritant de `str` la documentation sera capable de savoir que les valeurs doivent être de type `string` et pourra donc afficher cette `Enum` correctement. +Le premier sera toujours utilisé puisque le chemin correspond en premier. -Créez ensuite des attributs de classe avec des valeurs fixes, qui seront les valeurs autorisées pour cette énumération. +## Valeurs prédéfinies { #predefined-values } -{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} +Si vous avez un *chemin d'accès* qui reçoit un *paramètre de chemin*, mais que vous voulez que les valeurs possibles de ce *paramètre de chemin* soient prédéfinies, vous pouvez utiliser une `Enum` Python standard. -/// info +### Créer une classe `Enum` { #create-an-enum-class } -Les énumérations (ou enums) sont disponibles en Python depuis la version 3.4. +Importez `Enum` et créez une sous-classe qui hérite de `str` et de `Enum`. -/// +En héritant de `str`, la documentation de l'API saura que les valeurs doivent être de type `string` et pourra donc s'afficher correctement. + +Créez ensuite des attributs de classe avec des valeurs fixes, qui seront les valeurs valides disponibles : + +{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *} /// tip | Astuce -Pour ceux qui se demandent, "AlexNet", "ResNet", et "LeNet" sont juste des noms de modèles de Machine Learning. +Si vous vous demandez, « AlexNet », « ResNet » et « LeNet » sont juste des noms de modèles de Machine Learning. /// -### Déclarer un paramètre de chemin +### Déclarer un paramètre de chemin { #declare-a-path-parameter } -Créez ensuite un *paramètre de chemin* avec une annotation de type désignant l'énumération créée précédemment (`ModelName`) : +Créez ensuite un *paramètre de chemin* avec une annotation de type utilisant la classe d'énumération que vous avez créée (`ModelName`) : -{* ../../docs_src/path_params/tutorial005.py hl[16] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *} -### Documentation +### Consulter la documentation { #check-the-docs } -Les valeurs disponibles pour le *paramètre de chemin* sont bien prédéfinies, la documentation les affiche correctement : +Comme les valeurs disponibles pour le *paramètre de chemin* sont prédéfinies, la documentation interactive peut les afficher clairement : -### Manipuler les *énumérations* Python +### Travailler avec les *énumérations* Python { #working-with-python-enumerations } -La valeur du *paramètre de chemin* sera un des "membres" de l'énumération. +La valeur du *paramètre de chemin* sera un *membre d'énumération*. -#### Comparer les *membres d'énumération* +#### Comparer des *membres d'énumération* { #compare-enumeration-members } -Vous pouvez comparer ce paramètre avec les membres de votre énumération `ModelName` : +Vous pouvez le comparer avec le *membre d'énumération* dans votre enum `ModelName` : -{* ../../docs_src/path_params/tutorial005.py hl[17] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *} -#### Récupérer la *valeur de l'énumération* +#### Obtenir la *valeur de l'énumération* { #get-the-enumeration-value } -Vous pouvez obtenir la valeur réel d'un membre (une chaîne de caractères ici), avec `model_name.value`, ou en général, `votre_membre_d'enum.value` : +Vous pouvez obtenir la valeur réelle (une `str` dans ce cas) avec `model_name.value`, ou en général, `votre_membre_d_enum.value` : -{* ../../docs_src/path_params/tutorial005.py hl[20] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *} /// tip | Astuce -Vous pouvez aussi accéder la valeur `"lenet"` avec `ModelName.lenet.value`. +Vous pouvez aussi accéder à la valeur « lenet » avec `ModelName.lenet.value`. /// -#### Retourner des *membres d'énumération* +#### Retourner des *membres d'énumération* { #return-enumeration-members } -Vous pouvez retourner des *membres d'énumération* dans vos *fonctions de chemin*, même imbriquée dans un JSON (e.g. un `dict`). +Vous pouvez retourner des *membres d'énumération* depuis votre *chemin d'accès*, même imbriqués dans un corps JSON (par ex. un `dict`). -Ils seront convertis vers leurs valeurs correspondantes (chaînes de caractères ici) avant d'être transmis au client : +Ils seront convertis vers leurs valeurs correspondantes (des chaînes de caractères ici) avant d'être renvoyés au client : -{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} +{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *} -Le client recevra une réponse JSON comme celle-ci : +Dans votre client, vous recevrez une réponse JSON comme : ```JSON { @@ -208,53 +199,53 @@ Le client recevra une réponse JSON comme celle-ci : } ``` -## Paramètres de chemin contenant des chemins +## Paramètres de chemin contenant des chemins { #path-parameters-containing-paths } -Disons que vous avez une *fonction de chemin* liée au chemin `/files/{file_path}`. +Disons que vous avez un *chemin d'accès* avec un chemin `/files/{file_path}`. -Mais que `file_path` lui-même doit contenir un *chemin*, comme `home/johndoe/myfile.txt` par exemple. +Mais vous avez besoin que `file_path` lui-même contienne un *chemin*, comme `home/johndoe/myfile.txt`. -Donc, l'URL pour ce fichier pourrait être : `/files/home/johndoe/myfile.txt`. +Ainsi, l'URL pour ce fichier serait : `/files/home/johndoe/myfile.txt`. -### Support d'OpenAPI +### Support d'OpenAPI { #openapi-support } -OpenAPI ne supporte pas de manière de déclarer un paramètre de chemin contenant un *chemin*, cela pouvant causer des scénarios difficiles à tester et définir. +OpenAPI ne prend pas en charge une manière de déclarer un *paramètre de chemin* contenant un *chemin* à l'intérieur, car cela peut conduire à des scénarios difficiles à tester et à définir. -Néanmoins, cela reste faisable dans **FastAPI**, via les outils internes de Starlette. +Néanmoins, vous pouvez toujours le faire dans **FastAPI**, en utilisant l'un des outils internes de Starlette. -Et la documentation fonctionne quand même, bien qu'aucune section ne soit ajoutée pour dire que la paramètre devrait contenir un *chemin*. +Et la documentation fonctionnera quand même, même si aucune indication supplémentaire ne sera ajoutée pour dire que le paramètre doit contenir un chemin. -### Convertisseur de *chemin* +### Convertisseur de chemin { #path-convertor } -En utilisant une option de Starlette directement, vous pouvez déclarer un *paramètre de chemin* contenant un *chemin* avec une URL comme : +En utilisant une option directement depuis Starlette, vous pouvez déclarer un *paramètre de chemin* contenant un *chemin* avec une URL comme : ``` /files/{file_path:path} ``` -Dans ce cas, le nom du paramètre est `file_path`, et la dernière partie, `:path`, indique à Starlette que le paramètre devrait correspondre à un *chemin*. +Dans ce cas, le nom du paramètre est `file_path`, et la dernière partie, `:path`, indique que le paramètre doit correspondre à n'importe quel *chemin*. -Vous pouvez donc l'utilisez comme tel : +Vous pouvez donc l'utiliser ainsi : -{* ../../docs_src/path_params/tutorial004.py hl[6] *} +{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *} /// tip | Astuce -Vous pourriez avoir besoin que le paramètre contienne `/home/johndoe/myfile.txt`, avec un slash au début (`/`). +Vous pourriez avoir besoin que le paramètre contienne `/home/johndoe/myfile.txt`, avec un slash initial (`/`). Dans ce cas, l'URL serait : `/files//home/johndoe/myfile.txt`, avec un double slash (`//`) entre `files` et `home`. /// -## Récapitulatif +## Récapitulatif { #recap } -Avec **FastAPI**, en utilisant les déclarations de type rapides, intuitives et standards de Python, vous bénéficiez de : +Avec **FastAPI**, en utilisant des déclarations de type Python courtes, intuitives et standard, vous obtenez : -* Support de l'éditeur : vérification d'erreurs, auto-complétion, etc. -* "Parsing" de données. -* Validation de données. -* Annotations d'API et documentation automatique. +* Support de l'éditeur : vérifications d'erreurs, autocomplétion, etc. +* Données « parsing » +* Validation de données +* Annotations d'API et documentation automatique -Et vous n'avez besoin de le déclarer qu'une fois. +Et vous n'avez besoin de les déclarer qu'une seule fois. -C'est probablement l'avantage visible principal de **FastAPI** comparé aux autres *frameworks* (outre les performances pures). +C'est probablement l'avantage visible principal de **FastAPI** comparé aux autres frameworks (outre les performances pures). diff --git a/docs/fr/docs/tutorial/query-params-str-validations.md b/docs/fr/docs/tutorial/query-params-str-validations.md index c54c0c717..544d10328 100644 --- a/docs/fr/docs/tutorial/query-params-str-validations.md +++ b/docs/fr/docs/tutorial/query-params-str-validations.md @@ -1,166 +1,273 @@ -# Paramètres de requête et validations de chaînes de caractères +# Paramètres de requête et validations de chaînes de caractères { #query-parameters-and-string-validations } -**FastAPI** vous permet de déclarer des informations et des validateurs additionnels pour vos paramètres de requêtes. +**FastAPI** vous permet de déclarer des informations et des validations supplémentaires pour vos paramètres. -Commençons avec cette application pour exemple : +Prenons cette application comme exemple : -{* ../../docs_src/query_params_str_validations/tutorial001.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} -Le paramètre de requête `q` a pour type `Union[str, None]` (ou `str | None` en Python 3.10), signifiant qu'il est de type `str` mais pourrait aussi être égal à `None`, et bien sûr, la valeur par défaut est `None`, donc **FastAPI** saura qu'il n'est pas requis. +Le paramètre de requête `q` est de type `str | None`, cela signifie qu’il est de type `str` mais peut aussi être `None`, et en effet, la valeur par défaut est `None`, donc FastAPI saura qu’il n’est pas requis. -/// note +/// note | Remarque -**FastAPI** saura que la valeur de `q` n'est pas requise grâce à la valeur par défaut `= None`. +FastAPI saura que la valeur de `q` n’est pas requise grâce à la valeur par défaut `= None`. -Le `Union` dans `Union[str, None]` permettra à votre éditeur de vous offrir un meilleur support et de détecter les erreurs. +Avoir `str | None` permettra à votre éditeur de vous offrir un meilleur support et de détecter les erreurs. /// -## Validation additionnelle +## Validation additionnelle { #additional-validation } -Nous allons imposer que bien que `q` soit un paramètre optionnel, dès qu'il est fourni, **sa longueur n'excède pas 50 caractères**. +Nous allons imposer que, même si `q` est optionnel, dès qu’il est fourni, **sa longueur n’excède pas 50 caractères**. -## Importer `Query` +### Importer `Query` et `Annotated` { #import-query-and-annotated } -Pour cela, importez d'abord `Query` depuis `fastapi` : +Pour ce faire, importez d’abord : -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[3] *} +- `Query` depuis `fastapi` +- `Annotated` depuis `typing` -## Utiliser `Query` comme valeur par défaut +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} -Construisez ensuite la valeur par défaut de votre paramètre avec `Query`, en choisissant 50 comme `max_length` : +/// info -{* ../../docs_src/query_params_str_validations/tutorial002.py hl[9] *} +FastAPI a ajouté la prise en charge de `Annotated` (et a commencé à le recommander) dans la version 0.95.0. -Comme nous devons remplacer la valeur par défaut `None` dans la fonction par `Query()`, nous pouvons maintenant définir la valeur par défaut avec le paramètre `Query(default=None)`, il sert le même objectif qui est de définir cette valeur par défaut. +Si vous avez une version plus ancienne, vous obtiendrez des erreurs en essayant d’utiliser `Annotated`. -Donc : +Assurez-vous de [mettre à niveau la version de FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} vers au moins 0.95.1 avant d’utiliser `Annotated`. + +/// + +## Utiliser `Annotated` dans le type pour le paramètre `q` { #use-annotated-in-the-type-for-the-q-parameter } + +Vous vous souvenez que je vous ai dit plus tôt que `Annotated` peut être utilisé pour ajouter des métadonnées à vos paramètres dans l’[Introduction aux types Python](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank} ? + +C’est le moment de l’utiliser avec FastAPI. 🚀 + +Nous avions cette annotation de type : + +//// tab | Python 3.10+ ```Python -q: Union[str, None] = Query(default=None) +q: str | None = None ``` -... rend le paramètre optionnel, et est donc équivalent à : +//// + +//// tab | Python 3.9+ ```Python q: Union[str, None] = None ``` -Mais déclare explicitement `q` comme étant un paramètre de requête. +//// -/// info +Ce que nous allons faire, c’est l’englober avec `Annotated`, de sorte que cela devienne : -Gardez à l'esprit que la partie la plus importante pour rendre un paramètre optionnel est : +//// tab | Python 3.10+ ```Python -= None +q: Annotated[str | None] = None ``` -ou : +//// + +//// tab | Python 3.9+ ```Python -= Query(None) +q: Annotated[Union[str, None]] = None ``` -et utilisera ce `None` pour détecter que ce paramètre de requête **n'est pas requis**. +//// -Le `Union[str, None]` est uniquement là pour permettre à votre éditeur un meilleur support. +Les deux versions signifient la même chose, `q` est un paramètre qui peut être une `str` ou `None`, et par défaut, c’est `None`. + +Passons maintenant aux choses amusantes. 🎉 + +## Ajouter `Query` à `Annotated` dans le paramètre `q` { #add-query-to-annotated-in-the-q-parameter } + +Maintenant que nous avons cet `Annotated` dans lequel nous pouvons mettre plus d’informations (dans ce cas une validation supplémentaire), ajoutez `Query` à l’intérieur de `Annotated`, et définissez le paramètre `max_length` à `50` : + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} + +Remarquez que la valeur par défaut est toujours `None`, donc le paramètre est toujours optionnel. + +Mais maintenant, avec `Query(max_length=50)` à l’intérieur de `Annotated`, nous indiquons à FastAPI que nous voulons **une validation supplémentaire** pour cette valeur, nous voulons qu’elle ait au maximum 50 caractères. 😎 + +/// tip | Astuce + +Ici nous utilisons `Query()` parce qu’il s’agit d’un **paramètre de requête**. Plus tard nous verrons d’autres comme `Path()`, `Body()`, `Header()` et `Cookie()`, qui acceptent également les mêmes arguments que `Query()`. /// -Ensuite, nous pouvons passer d'autres paramètres à `Query`. Dans cet exemple, le paramètre `max_length` qui s'applique aux chaînes de caractères : +FastAPI va maintenant : -```Python -q: Union[str, None] = Query(default=None, max_length=50) -``` +- **Valider** les données en s’assurant que la longueur maximale est de 50 caractères +- Afficher une **erreur claire** au client quand les données ne sont pas valides +- **Documenter** le paramètre dans la *chemin d'accès* du schéma OpenAPI (il apparaîtra donc dans l’**interface de documentation automatique**) -Cela va valider les données, montrer une erreur claire si ces dernières ne sont pas valides, et documenter le paramètre dans le schéma `OpenAPI` de cette *path operation*. +## Alternative (ancienne) : `Query` comme valeur par défaut { #alternative-old-query-as-the-default-value } -## Rajouter plus de validation +Les versions précédentes de FastAPI (avant 0.95.0) exigeaient d’utiliser `Query` comme valeur par défaut de votre paramètre, au lieu de le mettre dans `Annotated`. Il y a de fortes chances que vous voyiez du code qui l’utilise encore, je vais donc vous l’expliquer. -Vous pouvez aussi rajouter un second paramètre `min_length` : +/// tip | Astuce -{* ../../docs_src/query_params_str_validations/tutorial003.py hl[9] *} - -## Ajouter des validations par expressions régulières - -On peut définir une expression régulière à laquelle le paramètre doit correspondre : - -{* ../../docs_src/query_params_str_validations/tutorial004.py hl[10] *} - -Cette expression régulière vérifie que la valeur passée comme paramètre : - -* `^` : commence avec les caractères qui suivent, avec aucun caractère avant ceux-là. -* `fixedquery` : a pour valeur exacte `fixedquery`. -* `$` : se termine directement ensuite, n'a pas d'autres caractères après `fixedquery`. - -Si vous vous sentez perdu avec le concept d'**expression régulière**, pas d'inquiétudes. Il s'agit d'une notion difficile pour beaucoup, et l'on peut déjà réussir à faire beaucoup sans jamais avoir à les manipuler. - -Mais si vous décidez d'apprendre à les utiliser, sachez qu'ensuite vous pouvez les utiliser directement dans **FastAPI**. - -## Valeurs par défaut - -De la même façon que vous pouvez passer `None` comme premier argument pour l'utiliser comme valeur par défaut, vous pouvez passer d'autres valeurs. - -Disons que vous déclarez le paramètre `q` comme ayant une longueur minimale de `3`, et une valeur par défaut étant `"fixedquery"` : - -{* ../../docs_src/query_params_str_validations/tutorial005.py hl[7] *} - -/// note | Rappel - -Avoir une valeur par défaut rend le paramètre optionnel. +Pour du nouveau code et dès que possible, utilisez `Annotated` comme expliqué ci-dessus. Il y a de multiples avantages (expliqués ci-dessous) et aucun inconvénient. 🍰 /// -## Rendre ce paramètre requis +Voici comment vous utiliseriez `Query()` comme valeur par défaut du paramètre de votre fonction, en définissant le paramètre `max_length` à 50 : -Quand on ne déclare ni validation, ni métadonnée, on peut rendre le paramètre `q` requis en ne lui déclarant juste aucune valeur par défaut : +{* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} + +Comme, dans ce cas (sans utiliser `Annotated`), nous devons remplacer la valeur par défaut `None` dans la fonction par `Query()`, nous devons maintenant définir la valeur par défaut avec le paramètre `Query(default=None)`, cela sert le même objectif de définir cette valeur par défaut (au moins pour FastAPI). + +Donc : + +```Python +q: str | None = Query(default=None) +``` + +... rend le paramètre optionnel, avec une valeur par défaut de `None`, comme : + +```Python +q: str | None = None +``` + +Mais la version avec `Query` le déclare explicitement comme étant un paramètre de requête. + +Ensuite, nous pouvons passer plus de paramètres à `Query`. Dans ce cas, le paramètre `max_length` qui s’applique aux chaînes de caractères : + +```Python +q: str | None = Query(default=None, max_length=50) +``` + +Cela validera les données, affichera une erreur claire lorsque les données ne sont pas valides et documentera le paramètre dans la *chemin d'accès* du schéma OpenAPI. + +### `Query` comme valeur par défaut ou dans `Annotated` { #query-as-the-default-value-or-in-annotated } + +Gardez à l’esprit qu’en utilisant `Query` à l’intérieur de `Annotated`, vous ne pouvez pas utiliser le paramètre `default` de `Query`. + +Utilisez à la place la valeur par défaut réelle du paramètre de fonction. Sinon, ce serait incohérent. + +Par exemple, ceci n’est pas autorisé : + +```Python +q: Annotated[str, Query(default="rick")] = "morty" +``` + +... parce qu’il n’est pas clair si la valeur par défaut doit être « rick » ou « morty ». + +Donc, vous utiliseriez (de préférence) : + +```Python +q: Annotated[str, Query()] = "rick" +``` + +... ou dans des bases de code plus anciennes, vous trouverez : + +```Python +q: str = Query(default="rick") +``` + +### Avantages de `Annotated` { #advantages-of-annotated } + +**L’utilisation de `Annotated` est recommandée** plutôt que la valeur par défaut dans les paramètres de fonction, c’est **mieux** pour plusieurs raisons. 🤓 + +La valeur **par défaut** du **paramètre de fonction** est la **vraie valeur par défaut**, c’est plus intuitif en Python en général. 😌 + +Vous pouvez **appeler** cette même fonction dans **d’autres endroits** sans FastAPI, et elle **fonctionnera comme prévu**. S’il y a un paramètre **requis** (sans valeur par défaut), votre **éditeur** vous le signalera avec une erreur, **Python** se plaindra aussi si vous l’exécutez sans passer le paramètre requis. + +Quand vous n’utilisez pas `Annotated` et utilisez à la place l’**ancienne** méthode avec la **valeur par défaut**, si vous appelez cette fonction sans FastAPI dans **d’autres endroits**, vous devez **penser** à passer les arguments à la fonction pour qu’elle fonctionne correctement, sinon les valeurs seront différentes de ce que vous attendez (par ex. `QueryInfo` ou quelque chose de similaire au lieu d’une `str`). Et votre éditeur ne se plaindra pas, et Python ne se plaindra pas en exécutant cette fonction, seulement quand les opérations internes échoueront. + +Comme `Annotated` peut avoir plus d’une annotation de métadonnées, vous pouvez maintenant même utiliser la même fonction avec d’autres outils, comme Typer. 🚀 + +## Ajouter plus de validations { #add-more-validations } + +Vous pouvez également ajouter un paramètre `min_length` : + +{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} + +## Ajouter des expressions régulières { #add-regular-expressions } + +Vous pouvez définir un `pattern` d’expression régulière auquel le paramètre doit correspondre : + +{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} + +Ce pattern d’expression régulière spécifique vérifie que la valeur reçue pour le paramètre : + +- `^` : commence avec les caractères qui suivent, n’a pas de caractères avant. +- `fixedquery` : a exactement la valeur `fixedquery`. +- `$` : se termine là, n’a pas d’autres caractères après `fixedquery`. + +Si vous vous sentez perdu avec toutes ces idées d’**« expression régulière »**, pas d’inquiétude. C’est un sujet difficile pour beaucoup. Vous pouvez déjà faire beaucoup de choses sans avoir besoin d’expressions régulières. + +Désormais, vous savez que, lorsque vous en aurez besoin, vous pourrez les utiliser dans **FastAPI**. + +## Valeurs par défaut { #default-values } + +Vous pouvez, bien sûr, utiliser des valeurs par défaut autres que `None`. + +Disons que vous voulez déclarer le paramètre de requête `q` avec un `min_length` de `3`, et avec une valeur par défaut de « fixedquery » : + +{* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *} + +/// note | Remarque + +Avoir une valeur par défaut de n’importe quel type, y compris `None`, rend le paramètre optionnel (non requis). + +/// + +## Paramètres requis { #required-parameters } + +Quand nous n’avons pas besoin de déclarer plus de validations ou de métadonnées, nous pouvons rendre le paramètre de requête `q` requis en n’indiquant simplement pas de valeur par défaut, comme : ```Python q: str ``` -à la place de : +au lieu de : ```Python -q: Union[str, None] = None +q: str | None = None ``` -Mais maintenant, on déclare `q` avec `Query`, comme ceci : +Mais maintenant nous le déclarons avec `Query`, par exemple ainsi : ```Python -q: Union[str, None] = Query(default=None, min_length=3) +q: Annotated[str | None, Query(min_length=3)] = None ``` -Donc pour déclarer une valeur comme requise tout en utilisant `Query`, il faut utiliser `...` comme premier argument : +Donc, lorsque vous avez besoin de déclarer une valeur comme requise tout en utilisant `Query`, vous pouvez simplement ne pas déclarer de valeur par défaut : -{* ../../docs_src/query_params_str_validations/tutorial006.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} -/// info +### Requis, peut valoir `None` { #required-can-be-none } -Si vous n'avez jamais vu ce `...` auparavant : c'est une des constantes natives de Python appelée "Ellipsis". +Vous pouvez déclarer qu’un paramètre accepte `None`, mais qu’il est tout de même requis. Cela obligerait les clients à envoyer une valeur, même si la valeur est `None`. -/// +Pour ce faire, vous pouvez déclarer que `None` est un type valide tout en ne déclarant pas de valeur par défaut : -Cela indiquera à **FastAPI** que la présence de ce paramètre est obligatoire. +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} -## Liste de paramètres / valeurs multiples via Query +## Liste de paramètres de requête / valeurs multiples { #query-parameter-list-multiple-values } -Quand on définit un paramètre de requête explicitement avec `Query` on peut aussi déclarer qu'il reçoit une liste de valeur, ou des "valeurs multiples". +Quand vous définissez un paramètre de requête explicitement avec `Query`, vous pouvez aussi déclarer qu’il reçoit une liste de valeurs, autrement dit, qu’il reçoit des valeurs multiples. -Par exemple, pour déclarer un paramètre de requête `q` qui peut apparaître plusieurs fois dans une URL, on écrit : +Par exemple, pour déclarer un paramètre de requête `q` qui peut apparaître plusieurs fois dans l’URL, vous pouvez écrire : -{* ../../docs_src/query_params_str_validations/tutorial011.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *} -Ce qui fait qu'avec une URL comme : +Ensuite, avec une URL comme : ``` http://localhost:8000/items/?q=foo&q=bar ``` -vous recevriez les valeurs des multiples paramètres de requête `q` (`foo` et `bar`) dans une `list` Python au sein de votre fonction de **path operation**, dans le paramètre de fonction `q`. +vous recevriez les valeurs des multiples paramètres de requête `q` (`foo` et `bar`) dans une `list` Python à l’intérieur de votre fonction de *chemin d'accès*, dans le *paramètre de fonction* `q`. -Donc la réponse de cette URL serait : +Donc, la réponse pour cette URL serait : ```JSON { @@ -173,19 +280,19 @@ Donc la réponse de cette URL serait : /// tip | Astuce -Pour déclarer un paramètre de requête de type `list`, comme dans l'exemple ci-dessus, il faut explicitement utiliser `Query`, sinon cela sera interprété comme faisant partie du corps de la requête. +Pour déclarer un paramètre de requête avec un type `list`, comme dans l’exemple ci-dessus, vous devez explicitement utiliser `Query`, sinon il serait interprété comme faisant partie du corps de la requête. /// -La documentation sera donc mise à jour automatiquement pour autoriser plusieurs valeurs : +L’interface de documentation interactive de l’API sera mise à jour en conséquence, pour autoriser plusieurs valeurs : -### Combiner liste de paramètres et valeurs par défaut +### Liste de paramètres de requête / valeurs multiples avec valeurs par défaut { #query-parameter-list-multiple-values-with-defaults } -Et l'on peut aussi définir une liste de valeurs par défaut si aucune n'est fournie : +Vous pouvez également définir une `list` de valeurs par défaut si aucune n’est fournie : -{* ../../docs_src/query_params_str_validations/tutorial012.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} Si vous allez à : @@ -193,9 +300,7 @@ Si vous allez à : http://localhost:8000/items/ ``` -la valeur par défaut de `q` sera : `["foo", "bar"]` - -et la réponse sera : +la valeur par défaut de `q` sera : `["foo", "bar"]` et votre réponse sera : ```JSON { @@ -206,93 +311,163 @@ et la réponse sera : } ``` -#### Utiliser `list` +#### Utiliser simplement `list` { #using-just-list } -Il est aussi possible d'utiliser directement `list` plutôt que `List[str]` : +Vous pouvez aussi utiliser `list` directement au lieu de `list[str]` : -{* ../../docs_src/query_params_str_validations/tutorial013.py hl[7] *} +{* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} -/// note +/// note | Remarque -Dans ce cas-là, **FastAPI** ne vérifiera pas le contenu de la liste. +Gardez à l’esprit que dans ce cas, FastAPI ne vérifiera pas le contenu de la liste. -Par exemple, `List[int]` vérifiera (et documentera) que la liste est bien entièrement composée d'entiers. Alors qu'un simple `list` ne ferait pas cette vérification. +Par exemple, `list[int]` vérifierait (et documenterait) que le contenu de la liste est composé d’entiers. Mais un simple `list` ne le ferait pas. /// -## Déclarer des métadonnées supplémentaires +## Déclarer plus de métadonnées { #declare-more-metadata } -On peut aussi ajouter plus d'informations sur le paramètre. +Vous pouvez ajouter plus d’informations à propos du paramètre. -Ces informations seront incluses dans le schéma `OpenAPI` généré et utilisées par la documentation interactive ou les outils externes utilisés. +Ces informations seront incluses dans l’OpenAPI généré et utilisées par les interfaces de documentation et les outils externes. -/// note +/// note | Remarque -Gardez en tête que les outils externes utilisés ne supportent pas forcément tous parfaitement OpenAPI. +Gardez à l’esprit que différents outils peuvent avoir des niveaux de prise en charge d’OpenAPI différents. -Il se peut donc que certains d'entre eux n'utilisent pas toutes les métadonnées que vous avez déclarées pour le moment, bien que dans la plupart des cas, les fonctionnalités manquantes ont prévu d'être implémentées. +Certains d’entre eux pourraient ne pas encore afficher toutes les informations supplémentaires déclarées, bien que, dans la plupart des cas, la fonctionnalité manquante soit déjà prévue au développement. /// Vous pouvez ajouter un `title` : -{* ../../docs_src/query_params_str_validations/tutorial007.py hl[10] *} +{* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *} Et une `description` : -{* ../../docs_src/query_params_str_validations/tutorial008.py hl[13] *} +{* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} -## Alias de paramètres +## Paramètres avec alias { #alias-parameters } -Imaginez que vous vouliez que votre paramètre se nomme `item-query`. +Imaginez que vous vouliez que le paramètre soit `item-query`. -Comme dans la requête : +Comme dans : ``` http://127.0.0.1:8000/items/?item-query=foobaritems ``` -Mais `item-query` n'est pas un nom de variable valide en Python. +Mais `item-query` n’est pas un nom de variable Python valide. -Le nom le plus proche serait `item_query`. +Le plus proche serait `item_query`. -Mais vous avez vraiment envie que ce soit exactement `item-query`... +Mais vous avez quand même besoin que ce soit exactement `item-query` ... -Pour cela vous pouvez déclarer un `alias`, et cet alias est ce qui sera utilisé pour trouver la valeur du paramètre : +Vous pouvez alors déclarer un `alias`, et cet alias sera utilisé pour trouver la valeur du paramètre : -{* ../../docs_src/query_params_str_validations/tutorial009.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} -## Déprécier des paramètres +## Déprécier des paramètres { #deprecating-parameters } -Disons que vous ne vouliez plus utiliser ce paramètre désormais. +Disons que vous n’aimez plus ce paramètre. -Il faut qu'il continue à exister pendant un certain temps car vos clients l'utilisent, mais vous voulez que la documentation mentionne clairement que ce paramètre est déprécié. +Vous devez le laisser là quelque temps car des clients l’utilisent, mais vous voulez que les documents l’affichent clairement comme déprécié. -On utilise alors l'argument `deprecated=True` de `Query` : +Passez alors le paramètre `deprecated=True` à `Query` : -{* ../../docs_src/query_params_str_validations/tutorial010.py hl[18] *} +{* ../../docs_src/query_params_str_validations/tutorial010_an_py310.py hl[19] *} -La documentation le présentera comme il suit : +Les documents l’afficheront ainsi : -## Pour résumer +## Exclure des paramètres d’OpenAPI { #exclude-parameters-from-openapi } -Il est possible d'ajouter des validateurs et métadonnées pour vos paramètres. +Pour exclure un paramètre de requête du schéma OpenAPI généré (et donc, des systèmes de documentation automatiques), définissez le paramètre `include_in_schema` de `Query` à `False` : -Validateurs et métadonnées génériques: +{* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} -* `alias` -* `title` -* `description` -* `deprecated` +## Validation personnalisée { #custom-validation } -Validateurs spécifiques aux chaînes de caractères : +Il peut y avoir des cas où vous devez faire une **validation personnalisée** qui ne peut pas être réalisée avec les paramètres montrés ci-dessus. -* `min_length` -* `max_length` -* `regex` +Dans ces cas, vous pouvez utiliser une **fonction de validation personnalisée** qui est appliquée après la validation normale (par ex. après avoir validé que la valeur est une `str`). -Parmi ces exemples, vous avez pu voir comment déclarer des validateurs pour les chaînes de caractères. +Vous pouvez y parvenir en utilisant `AfterValidator` de Pydantic à l’intérieur de `Annotated`. -Dans les prochains chapitres, vous verrez comment déclarer des validateurs pour d'autres types, comme les nombres. +/// tip | Astuce + +Pydantic a aussi `BeforeValidator` et d’autres. 🤓 + +/// + +Par exemple, ce validateur personnalisé vérifie que l’ID d’item commence par `isbn-` pour un numéro de livre ISBN ou par `imdb-` pour un ID d’URL de film IMDB : + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} + +/// info + +C’est disponible avec Pydantic version 2 ou supérieure. 😎 + +/// + +/// tip | Astuce + +Si vous devez faire un type de validation qui nécessite de communiquer avec un **composant externe**, comme une base de données ou une autre API, vous devez plutôt utiliser les **Dépendances de FastAPI**, vous en apprendrez davantage plus tard. + +Ces validateurs personnalisés sont destinés aux éléments qui peuvent être vérifiés **uniquement** avec les **mêmes données** fournies dans la requête. + +/// + +### Comprendre ce code { #understand-that-code } + +Le point important est simplement d’utiliser **`AfterValidator` avec une fonction à l’intérieur de `Annotated`**. N’hésitez pas à passer cette partie. 🤸 + +--- + +Mais si vous êtes curieux de cet exemple de code spécifique et que vous êtes toujours partant, voici quelques détails supplémentaires. + +#### Chaîne avec `value.startswith()` { #string-with-value-startswith } + +Avez-vous remarqué ? Une chaîne utilisant `value.startswith()` peut prendre un tuple, et elle vérifiera chaque valeur du tuple : + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *} + +#### Un élément aléatoire { #a-random-item } + +Avec `data.items()` nous obtenons un objet itérable avec des tuples contenant la clé et la valeur pour chaque élément du dictionnaire. + +Nous convertissons cet objet itérable en une `list` propre avec `list(data.items())`. + +Ensuite, avec `random.choice()` nous pouvons obtenir une **valeur aléatoire** depuis la liste, nous obtenons donc un tuple `(id, name)`. Ce sera quelque chose comme `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`. + +Puis nous **affectons ces deux valeurs** du tuple aux variables `id` et `name`. + +Ainsi, si l’utilisateur n’a pas fourni d’ID d’item, il recevra quand même une suggestion aléatoire. + +... nous faisons tout cela en **une seule ligne simple**. 🤯 Vous n’adorez pas Python ? 🐍 + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} + +## Récapitulatif { #recap } + +Vous pouvez déclarer des validations et des métadonnées supplémentaires pour vos paramètres. + +Validations et métadonnées génériques : + +- `alias` +- `title` +- `description` +- `deprecated` + +Validations spécifiques aux chaînes : + +- `min_length` +- `max_length` +- `pattern` + +Validations personnalisées avec `AfterValidator`. + +Dans ces exemples, vous avez vu comment déclarer des validations pour des valeurs `str`. + +Voyez les prochains chapitres pour apprendre à déclarer des validations pour d’autres types, comme les nombres. diff --git a/docs/fr/docs/tutorial/query-params.md b/docs/fr/docs/tutorial/query-params.md index b87c26c78..1a4880ced 100644 --- a/docs/fr/docs/tutorial/query-params.md +++ b/docs/fr/docs/tutorial/query-params.md @@ -1,10 +1,10 @@ -# Paramètres de requête +# Paramètres de requête { #query-parameters } -Quand vous déclarez des paramètres dans votre fonction de chemin qui ne font pas partie des paramètres indiqués dans le chemin associé, ces paramètres sont automatiquement considérés comme des paramètres de "requête". +Quand vous déclarez d'autres paramètres de fonction qui ne font pas partie des paramètres de chemin, ils sont automatiquement interprétés comme des paramètres de « query ». -{* ../../docs_src/query_params/tutorial001.py hl[9] *} +{* ../../docs_src/query_params/tutorial001_py39.py hl[9] *} -La partie appelée requête (ou **query**) dans une URL est l'ensemble des paires clés-valeurs placées après le `?` , séparées par des `&`. +La query est l'ensemble des paires clé-valeur placées après le `?` dans une URL, séparées par des caractères `&`. Par exemple, dans l'URL : @@ -12,27 +12,27 @@ Par exemple, dans l'URL : http://127.0.0.1:8000/items/?skip=0&limit=10 ``` -...les paramètres de requête sont : +... les paramètres de requête sont : -* `skip` : avec une valeur de`0` +* `skip` : avec une valeur de `0` * `limit` : avec une valeur de `10` -Faisant partie de l'URL, ces valeurs sont des chaînes de caractères (`str`). +Comme ils font partie de l'URL, ce sont « naturellement » des chaînes de caractères. -Mais quand on les déclare avec des types Python (dans l'exemple précédent, en tant qu'`int`), elles sont converties dans les types renseignés. +Mais lorsque vous les déclarez avec des types Python (dans l'exemple ci-dessus, en tant que `int`), ils sont convertis vers ce type et validés par rapport à celui-ci. -Toutes les fonctionnalités qui s'appliquent aux paramètres de chemin s'appliquent aussi aux paramètres de requête : +Tous les mêmes processus qui s'appliquaient aux paramètres de chemin s'appliquent aussi aux paramètres de requête : -* Support de l'éditeur : vérification d'erreurs, auto-complétion, etc. -* "Parsing" de données. -* Validation de données. -* Annotations d'API et documentation automatique. +* Prise en charge de l'éditeur (évidemment) +* « parsing » des données +* Validation des données +* Documentation automatique -## Valeurs par défaut +## Valeurs par défaut { #defaults } -Les paramètres de requête ne sont pas une partie fixe d'un chemin, ils peuvent être optionnels et avoir des valeurs par défaut. +Comme les paramètres de requête ne sont pas une partie fixe d'un chemin, ils peuvent être optionnels et avoir des valeurs par défaut. -Dans l'exemple ci-dessus, ils ont des valeurs par défaut qui sont `skip=0` et `limit=10`. +Dans l'exemple ci-dessus, ils ont des valeurs par défaut `skip=0` et `limit=10`. Donc, accéder à l'URL : @@ -40,52 +40,44 @@ Donc, accéder à l'URL : http://127.0.0.1:8000/items/ ``` -serait équivalent à accéder à l'URL : +serait équivalent à accéder à : ``` http://127.0.0.1:8000/items/?skip=0&limit=10 ``` -Mais si vous accédez à, par exemple : +Mais si vous accédez, par exemple, à : ``` http://127.0.0.1:8000/items/?skip=20 ``` -Les valeurs des paramètres de votre fonction seront : +Les valeurs des paramètres dans votre fonction seront : -* `skip=20` : car c'est la valeur déclarée dans l'URL. -* `limit=10` : car `limit` n'a pas été déclaré dans l'URL, et que la valeur par défaut était `10`. +* `skip=20` : car vous l'avez défini dans l'URL +* `limit=10` : car c'était la valeur par défaut -## Paramètres optionnels +## Paramètres optionnels { #optional-parameters } -De la même façon, vous pouvez définir des paramètres de requête comme optionnels, en leur donnant comme valeur par défaut `None` : +De la même façon, vous pouvez déclarer des paramètres de requête optionnels, en définissant leur valeur par défaut à `None` : -{* ../../docs_src/query_params/tutorial002.py hl[9] *} +{* ../../docs_src/query_params/tutorial002_py310.py hl[7] *} -Ici, le paramètre `q` sera optionnel, et aura `None` comme valeur par défaut. +Dans ce cas, le paramètre de fonction `q` sera optionnel et vaudra `None` par défaut. -/// check | Remarque +/// check | Vérifications -On peut voir que **FastAPI** est capable de détecter que le paramètre de chemin `item_id` est un paramètre de chemin et que `q` n'en est pas un, c'est donc un paramètre de requête. +Notez également que FastAPI est suffisamment intelligent pour remarquer que le paramètre de chemin `item_id` est un paramètre de chemin et que `q` ne l'est pas, c'est donc un paramètre de requête. /// -/// note +## Conversion des types des paramètres de requête { #query-parameter-type-conversion } -**FastAPI** saura que `q` est optionnel grâce au `=None`. +Vous pouvez aussi déclarer des types `bool`, ils seront convertis : -Le `Optional` dans `Optional[str]` n'est pas utilisé par **FastAPI** (**FastAPI** n'en utilisera que la partie `str`), mais il servira tout de même à votre éditeur de texte pour détecter des erreurs dans votre code. +{* ../../docs_src/query_params/tutorial003_py310.py hl[7] *} -/// - -## Conversion des types des paramètres de requête - -Vous pouvez aussi déclarer des paramètres de requête comme booléens (`bool`), **FastAPI** les convertira : - -{* ../../docs_src/query_params/tutorial003.py hl[9] *} - -Avec ce code, en allant sur : +Dans ce cas, si vous allez sur : ``` http://127.0.0.1:8000/items/foo?short=1 @@ -115,60 +107,61 @@ ou http://127.0.0.1:8000/items/foo?short=yes ``` -ou n'importe quelle autre variation de casse (tout en majuscules, uniquement la première lettre en majuscule, etc.), votre fonction considérera le paramètre `short` comme ayant une valeur booléenne à `True`. Sinon la valeur sera à `False`. +ou n'importe quelle autre variation de casse (tout en majuscules, uniquement la première lettre en majuscule, etc.), votre fonction verra le paramètre `short` avec une valeur `bool` à `True`. Sinon la valeur sera à `False`. -## Multiples paramètres de chemin et de requête +## Multiples paramètres de chemin et de requête { #multiple-path-and-query-parameters } -Vous pouvez déclarer plusieurs paramètres de chemin et paramètres de requête dans la même fonction, **FastAPI** saura comment les gérer. +Vous pouvez déclarer plusieurs paramètres de chemin et paramètres de requête en même temps, FastAPI sait lequel est lequel. Et vous n'avez pas besoin de les déclarer dans un ordre spécifique. -Ils seront détectés par leurs noms : +Ils seront détectés par leur nom : -{* ../../docs_src/query_params/tutorial004.py hl[8,10] *} +{* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} -## Paramètres de requête requis +## Paramètres de requête requis { #required-query-parameters } -Quand vous déclarez une valeur par défaut pour un paramètre qui n'est pas un paramètre de chemin (actuellement, nous n'avons vu que les paramètres de requête), alors ce paramètre n'est pas requis. +Quand vous déclarez une valeur par défaut pour des paramètres qui ne sont pas des paramètres de chemin (pour l'instant, nous n'avons vu que les paramètres de requête), alors ils ne sont pas requis. -Si vous ne voulez pas leur donner de valeur par défaut mais juste les rendre optionnels, utilisez `None` comme valeur par défaut. +Si vous ne voulez pas leur donner de valeur spécifique mais simplement les rendre optionnels, définissez la valeur par défaut à `None`. -Mais si vous voulez rendre un paramètre de requête obligatoire, vous pouvez juste ne pas y affecter de valeur par défaut : +Mais si vous voulez rendre un paramètre de requête obligatoire, vous pouvez simplement ne déclarer aucune valeur par défaut : -{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} +{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *} -Ici le paramètre `needy` est un paramètre requis (ou obligatoire) de type `str`. +Ici, le paramètre de requête `needy` est un paramètre de requête requis de type `str`. -Si vous ouvrez une URL comme : +Si vous ouvrez dans votre navigateur une URL comme : ``` http://127.0.0.1:8000/items/foo-item ``` -...sans ajouter le paramètre requis `needy`, vous aurez une erreur : +... sans ajouter le paramètre requis `needy`, vous verrez une erreur comme : ```JSON { - "detail": [ - { - "loc": [ - "query", - "needy" - ], - "msg": "field required", - "type": "value_error.missing" - } - ] + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "needy" + ], + "msg": "Field required", + "input": null + } + ] } ``` -La présence de `needy` étant nécessaire, vous auriez besoin de l'insérer dans l'URL : +Comme `needy` est un paramètre requis, vous devez le définir dans l'URL : ``` http://127.0.0.1:8000/items/foo-item?needy=sooooneedy ``` -...ce qui fonctionnerait : +... cela fonctionnerait : ```JSON { @@ -177,18 +170,18 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy } ``` -Et bien sur, vous pouvez définir certains paramètres comme requis, certains avec des valeurs par défaut et certains entièrement optionnels : +Et bien sûr, vous pouvez définir certains paramètres comme requis, certains avec une valeur par défaut et certains entièrement optionnels : -{* ../../docs_src/query_params/tutorial006.py hl[10] *} +{* ../../docs_src/query_params/tutorial006_py310.py hl[8] *} -Ici, on a donc 3 paramètres de requête : +Dans ce cas, il y a 3 paramètres de requête : -* `needy`, requis et de type `str`. -* `skip`, un `int` avec comme valeur par défaut `0`. +* `needy`, un `str` requis. +* `skip`, un `int` avec une valeur par défaut de `0`. * `limit`, un `int` optionnel. /// tip | Astuce -Vous pouvez utiliser les `Enum`s de la même façon qu'avec les [Paramètres de chemin](path-params.md#valeurs-predefinies){.internal-link target=_blank}. +Vous pourriez aussi utiliser des `Enum`s de la même façon qu'avec les [Paramètres de chemin](path-params.md#predefined-values){.internal-link target=_blank}. /// From 2c5a844c44c053251d557ce888811ea40dea0b01 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 15:34:47 +0000 Subject: [PATCH 193/243] =?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 9f70eccb5..c54875f06 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Update translations for fr (update-outdated). PR [#14826](https://github.com/fastapi/fastapi/pull/14826) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for zh-hant (update-outdated). PR [#14825](https://github.com/fastapi/fastapi/pull/14825) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for uk (update-outdated). PR [#14822](https://github.com/fastapi/fastapi/pull/14822) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update docs and translations scripts, enable Turkish. PR [#14824](https://github.com/fastapi/fastapi/pull/14824) by [@tiangolo](https://github.com/tiangolo). From b5d276bda8d8f61e6df6ffe3472de2600588774c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 07:43:38 -0800 Subject: [PATCH 194/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20tr=20(add-missing)=20(#14790)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/tr/docs/_llm-test.md | 503 ++++++++++ docs/tr/docs/advanced/additional-responses.md | 247 +++++ .../docs/advanced/additional-status-codes.md | 41 + .../tr/docs/advanced/advanced-dependencies.md | 163 ++++ docs/tr/docs/advanced/async-tests.md | 99 ++ docs/tr/docs/advanced/behind-a-proxy.md | 466 ++++++++++ docs/tr/docs/advanced/custom-response.md | 312 +++++++ docs/tr/docs/advanced/dataclasses.md | 95 ++ docs/tr/docs/advanced/events.md | 165 ++++ docs/tr/docs/advanced/generate-clients.md | 208 +++++ docs/tr/docs/advanced/middleware.md | 97 ++ docs/tr/docs/advanced/openapi-callbacks.md | 186 ++++ docs/tr/docs/advanced/openapi-webhooks.md | 55 ++ .../path-operation-advanced-configuration.md | 172 ++++ .../advanced/response-change-status-code.md | 31 + docs/tr/docs/advanced/response-cookies.md | 51 ++ docs/tr/docs/advanced/response-directly.md | 65 ++ docs/tr/docs/advanced/response-headers.md | 41 + .../docs/advanced/security/http-basic-auth.md | 107 +++ .../docs/advanced/security/oauth2-scopes.md | 274 ++++++ docs/tr/docs/advanced/settings.md | 302 ++++++ docs/tr/docs/advanced/sub-applications.md | 67 ++ docs/tr/docs/advanced/templates.md | 126 +++ docs/tr/docs/advanced/testing-dependencies.md | 53 ++ docs/tr/docs/advanced/testing-events.md | 12 + .../docs/advanced/using-request-directly.md | 56 ++ docs/tr/docs/advanced/websockets.md | 186 ++++ docs/tr/docs/deployment/concepts.md | 321 +++++++ docs/tr/docs/deployment/docker.md | 620 +++++++++++++ docs/tr/docs/deployment/fastapicloud.md | 65 ++ docs/tr/docs/deployment/https.md | 231 +++++ docs/tr/docs/deployment/manually.md | 157 ++++ docs/tr/docs/deployment/server-workers.md | 139 +++ docs/tr/docs/deployment/versions.md | 93 ++ docs/tr/docs/environment-variables.md | 298 ++++++ docs/tr/docs/fastapi-cli.md | 75 ++ docs/tr/docs/help-fastapi.md | 256 ++++++ .../authentication-error-status-code.md | 17 + docs/tr/docs/how-to/conditional-openapi.md | 56 ++ docs/tr/docs/how-to/configure-swagger-ui.md | 70 ++ docs/tr/docs/how-to/custom-docs-ui-assets.md | 185 ++++ .../docs/how-to/custom-request-and-route.md | 109 +++ docs/tr/docs/how-to/extending-openapi.md | 80 ++ docs/tr/docs/how-to/graphql.md | 60 ++ ...migrate-from-pydantic-v1-to-pydantic-v2.md | 135 +++ .../docs/how-to/separate-openapi-schemas.md | 102 +++ docs/tr/docs/how-to/testing-database.md | 7 + docs/tr/docs/tutorial/background-tasks.md | 84 ++ docs/tr/docs/tutorial/bigger-applications.md | 504 ++++++++++ docs/tr/docs/tutorial/body-fields.md | 60 ++ docs/tr/docs/tutorial/body-multiple-params.md | 175 ++++ docs/tr/docs/tutorial/body-nested-models.md | 220 +++++ docs/tr/docs/tutorial/body-updates.md | 100 ++ docs/tr/docs/tutorial/body.md | 166 ++++ docs/tr/docs/tutorial/cookie-param-models.md | 76 ++ docs/tr/docs/tutorial/cors.md | 89 ++ docs/tr/docs/tutorial/debugging.md | 113 +++ .../dependencies/classes-as-dependencies.md | 288 ++++++ ...pendencies-in-path-operation-decorators.md | 69 ++ .../dependencies/dependencies-with-yield.md | 289 ++++++ .../dependencies/global-dependencies.md | 16 + docs/tr/docs/tutorial/dependencies/index.md | 250 +++++ .../tutorial/dependencies/sub-dependencies.md | 105 +++ docs/tr/docs/tutorial/encoder.md | 35 + docs/tr/docs/tutorial/extra-data-types.md | 62 ++ docs/tr/docs/tutorial/extra-models.md | 211 +++++ docs/tr/docs/tutorial/handling-errors.md | 244 +++++ docs/tr/docs/tutorial/header-param-models.md | 72 ++ docs/tr/docs/tutorial/header-params.md | 91 ++ docs/tr/docs/tutorial/index.md | 95 ++ docs/tr/docs/tutorial/metadata.md | 120 +++ docs/tr/docs/tutorial/middleware.md | 95 ++ .../tutorial/path-operation-configuration.md | 107 +++ .../path-params-numeric-validations.md | 154 ++++ docs/tr/docs/tutorial/query-param-models.md | 68 ++ .../tutorial/query-params-str-validations.md | 473 ++++++++++ docs/tr/docs/tutorial/request-files.md | 176 ++++ docs/tr/docs/tutorial/request-form-models.md | 78 ++ .../docs/tutorial/request-forms-and-files.md | 41 + docs/tr/docs/tutorial/response-model.md | 343 +++++++ docs/tr/docs/tutorial/response-status-code.md | 101 ++ docs/tr/docs/tutorial/schema-extra-example.md | 202 ++++ docs/tr/docs/tutorial/security/first-steps.md | 203 +++++ .../tutorial/security/get-current-user.md | 105 +++ docs/tr/docs/tutorial/security/index.md | 106 +++ docs/tr/docs/tutorial/security/oauth2-jwt.md | 273 ++++++ .../docs/tutorial/security/simple-oauth2.md | 289 ++++++ docs/tr/docs/tutorial/sql-databases.md | 357 ++++++++ docs/tr/docs/tutorial/testing.md | 190 ++++ docs/tr/docs/virtual-environments.md | 862 ++++++++++++++++++ 90 files changed, 15013 insertions(+) create mode 100644 docs/tr/docs/_llm-test.md create mode 100644 docs/tr/docs/advanced/additional-responses.md create mode 100644 docs/tr/docs/advanced/additional-status-codes.md create mode 100644 docs/tr/docs/advanced/advanced-dependencies.md create mode 100644 docs/tr/docs/advanced/async-tests.md create mode 100644 docs/tr/docs/advanced/behind-a-proxy.md create mode 100644 docs/tr/docs/advanced/custom-response.md create mode 100644 docs/tr/docs/advanced/dataclasses.md create mode 100644 docs/tr/docs/advanced/events.md create mode 100644 docs/tr/docs/advanced/generate-clients.md create mode 100644 docs/tr/docs/advanced/middleware.md create mode 100644 docs/tr/docs/advanced/openapi-callbacks.md create mode 100644 docs/tr/docs/advanced/openapi-webhooks.md create mode 100644 docs/tr/docs/advanced/path-operation-advanced-configuration.md create mode 100644 docs/tr/docs/advanced/response-change-status-code.md create mode 100644 docs/tr/docs/advanced/response-cookies.md create mode 100644 docs/tr/docs/advanced/response-directly.md create mode 100644 docs/tr/docs/advanced/response-headers.md create mode 100644 docs/tr/docs/advanced/security/http-basic-auth.md create mode 100644 docs/tr/docs/advanced/security/oauth2-scopes.md create mode 100644 docs/tr/docs/advanced/settings.md create mode 100644 docs/tr/docs/advanced/sub-applications.md create mode 100644 docs/tr/docs/advanced/templates.md create mode 100644 docs/tr/docs/advanced/testing-dependencies.md create mode 100644 docs/tr/docs/advanced/testing-events.md create mode 100644 docs/tr/docs/advanced/using-request-directly.md create mode 100644 docs/tr/docs/advanced/websockets.md create mode 100644 docs/tr/docs/deployment/concepts.md create mode 100644 docs/tr/docs/deployment/docker.md create mode 100644 docs/tr/docs/deployment/fastapicloud.md create mode 100644 docs/tr/docs/deployment/https.md create mode 100644 docs/tr/docs/deployment/manually.md create mode 100644 docs/tr/docs/deployment/server-workers.md create mode 100644 docs/tr/docs/deployment/versions.md create mode 100644 docs/tr/docs/environment-variables.md create mode 100644 docs/tr/docs/fastapi-cli.md create mode 100644 docs/tr/docs/help-fastapi.md create mode 100644 docs/tr/docs/how-to/authentication-error-status-code.md create mode 100644 docs/tr/docs/how-to/conditional-openapi.md create mode 100644 docs/tr/docs/how-to/configure-swagger-ui.md create mode 100644 docs/tr/docs/how-to/custom-docs-ui-assets.md create mode 100644 docs/tr/docs/how-to/custom-request-and-route.md create mode 100644 docs/tr/docs/how-to/extending-openapi.md create mode 100644 docs/tr/docs/how-to/graphql.md create mode 100644 docs/tr/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md create mode 100644 docs/tr/docs/how-to/separate-openapi-schemas.md create mode 100644 docs/tr/docs/how-to/testing-database.md create mode 100644 docs/tr/docs/tutorial/background-tasks.md create mode 100644 docs/tr/docs/tutorial/bigger-applications.md create mode 100644 docs/tr/docs/tutorial/body-fields.md create mode 100644 docs/tr/docs/tutorial/body-multiple-params.md create mode 100644 docs/tr/docs/tutorial/body-nested-models.md create mode 100644 docs/tr/docs/tutorial/body-updates.md create mode 100644 docs/tr/docs/tutorial/body.md create mode 100644 docs/tr/docs/tutorial/cookie-param-models.md create mode 100644 docs/tr/docs/tutorial/cors.md create mode 100644 docs/tr/docs/tutorial/debugging.md create mode 100644 docs/tr/docs/tutorial/dependencies/classes-as-dependencies.md create mode 100644 docs/tr/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md create mode 100644 docs/tr/docs/tutorial/dependencies/dependencies-with-yield.md create mode 100644 docs/tr/docs/tutorial/dependencies/global-dependencies.md create mode 100644 docs/tr/docs/tutorial/dependencies/index.md create mode 100644 docs/tr/docs/tutorial/dependencies/sub-dependencies.md create mode 100644 docs/tr/docs/tutorial/encoder.md create mode 100644 docs/tr/docs/tutorial/extra-data-types.md create mode 100644 docs/tr/docs/tutorial/extra-models.md create mode 100644 docs/tr/docs/tutorial/handling-errors.md create mode 100644 docs/tr/docs/tutorial/header-param-models.md create mode 100644 docs/tr/docs/tutorial/header-params.md create mode 100644 docs/tr/docs/tutorial/index.md create mode 100644 docs/tr/docs/tutorial/metadata.md create mode 100644 docs/tr/docs/tutorial/middleware.md create mode 100644 docs/tr/docs/tutorial/path-operation-configuration.md create mode 100644 docs/tr/docs/tutorial/path-params-numeric-validations.md create mode 100644 docs/tr/docs/tutorial/query-param-models.md create mode 100644 docs/tr/docs/tutorial/query-params-str-validations.md create mode 100644 docs/tr/docs/tutorial/request-files.md create mode 100644 docs/tr/docs/tutorial/request-form-models.md create mode 100644 docs/tr/docs/tutorial/request-forms-and-files.md create mode 100644 docs/tr/docs/tutorial/response-model.md create mode 100644 docs/tr/docs/tutorial/response-status-code.md create mode 100644 docs/tr/docs/tutorial/schema-extra-example.md create mode 100644 docs/tr/docs/tutorial/security/first-steps.md create mode 100644 docs/tr/docs/tutorial/security/get-current-user.md create mode 100644 docs/tr/docs/tutorial/security/index.md create mode 100644 docs/tr/docs/tutorial/security/oauth2-jwt.md create mode 100644 docs/tr/docs/tutorial/security/simple-oauth2.md create mode 100644 docs/tr/docs/tutorial/sql-databases.md create mode 100644 docs/tr/docs/tutorial/testing.md create mode 100644 docs/tr/docs/virtual-environments.md diff --git a/docs/tr/docs/_llm-test.md b/docs/tr/docs/_llm-test.md new file mode 100644 index 000000000..0ca218e6e --- /dev/null +++ b/docs/tr/docs/_llm-test.md @@ -0,0 +1,503 @@ +# LLM test dosyası { #llm-test-file } + +Bu doküman, dokümantasyonu çeviren LLM'nin `scripts/translate.py` içindeki `general_prompt`'u ve `docs/{language code}/llm-prompt.md` içindeki dile özel prompt'u anlayıp anlamadığını test eder. Dile özel prompt, `general_prompt`'a eklenir. + +Buraya eklenen testler, dile özel prompt'ları tasarlayan herkes tarafından görülecektir. + +Şu şekilde kullanın: + +* Dile özel bir prompt bulundurun: `docs/{language code}/llm-prompt.md`. +* Bu dokümanın hedeflediğiniz dile sıfırdan yeni bir çevirisini yapın (örneğin `translate.py` içindeki `translate-page` komutu). Bu, çeviriyi `docs/{language code}/docs/_llm-test.md` altında oluşturur. +* Çeviride her şeyin yolunda olup olmadığını kontrol edin. +* Gerekirse dile özel prompt'u, genel prompt'u veya İngilizce dokümanı iyileştirin. +* Ardından çeviride kalan sorunları elle düzeltin; böylece iyi bir çeviri elde edin. +* İyi çeviri yerindeyken yeniden çeviri yapın. İdeal sonuç, LLM'nin artık çeviride hiçbir değişiklik yapmamasıdır. Bu da genel prompt'un ve dile özel prompt'un olabilecek en iyi hâle geldiği anlamına gelir (bazen rastgele gibi görünen birkaç değişiklik yapabilir; çünkü LLM'ler deterministik algoritmalar değildir). + +Testler: + +## Code snippets { #code-snippets } + +//// tab | Test + +Bu bir code snippet: `foo`. Bu da başka bir code snippet: `bar`. Bir tane daha: `baz quux`. + +//// + +//// tab | Bilgi + +Code snippet'lerin içeriği olduğu gibi bırakılmalıdır. + +`script/translate.py` içindeki genel prompt'ta `### Content of code snippets` bölümüne bakın. + +//// + +## Alıntılar { #quotes } + +//// tab | Test + +Dün bir arkadaşım şunu yazdı: "If you spell incorrectly correctly, you have spelled it incorrectly". Ben de şunu yanıtladım: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'". + +/// note | Not + +LLM muhtemelen bunu yanlış çevirecektir. Yeniden çeviri yapıldığında düzeltilmiş çeviriyi koruyup korumadığı önemlidir. + +/// + +//// + +//// tab | Bilgi + +Prompt tasarlayan kişi, düz tırnakları tipografik tırnaklara dönüştürüp dönüştürmemeyi seçebilir. Olduğu gibi bırakmak da uygundur. + +Örneğin `docs/de/llm-prompt.md` içindeki `### Quotes` bölümüne bakın. + +//// + +## Code snippet'lerde alıntılar { #quotes-in-code-snippets } + +//// tab | Test + +`pip install "foo[bar]"` + +Code snippet'lerde string literal örnekleri: `"this"`, `'that'`. + +Code snippet'lerde string literal için zor bir örnek: `f"I like {'oranges' if orange else "apples"}"` + +Hardcore: `Yesterday, my friend wrote: "If you spell incorrectly correctly, you have spelled it incorrectly". To which I answered: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'"` + +//// + +//// tab | Bilgi + +... Ancak code snippet'lerin içindeki tırnaklar olduğu gibi kalmalıdır. + +//// + +## Code block'lar { #code-blocks } + +//// tab | Test + +Bir Bash code örneği... + +```bash +# Evrene bir selam yazdır +echo "Hello universe" +``` + +...ve bir console code örneği... + +```console +$ fastapi run main.py + FastAPI Starting server + Searching for package file structure +``` + +...ve bir başka console code örneği... + +```console +// "Code" adında bir dizin oluştur +$ mkdir code +// O dizine geç +$ cd code +``` + +...ve bir Python code örneği... + +```Python +wont_work() # This won't work 😱 +works(foo="bar") # This works 🎉 +``` + +...ve hepsi bu. + +//// + +//// tab | Bilgi + +Code block'ların içindeki code değiştirilmemelidir; tek istisna yorumlardır (comments). + +`script/translate.py` içindeki genel prompt'ta `### Content of code blocks` bölümüne bakın. + +//// + +## Sekmeler ve renkli kutular { #tabs-and-colored-boxes } + +//// tab | Test + +/// info | Bilgi +Some text +/// + +/// note | Not +Some text +/// + +/// note | Teknik Detaylar +Some text +/// + +/// check | Ek bilgi +Some text +/// + +/// tip | İpucu +Some text +/// + +/// warning | Uyarı +Some text +/// + +/// danger | Tehlike +Some text +/// + +//// + +//// tab | Bilgi + +Sekmelerin ve `Info`/`Note`/`Warning`/vb. blokların başlığı, dikey çizgiden (`|`) sonra çeviri olarak eklenmelidir. + +`script/translate.py` içindeki genel prompt'ta `### Special blocks` ve `### Tab blocks` bölümlerine bakın. + +//// + +## Web ve internal link'ler { #web-and-internal-links } + +//// tab | Test + +Link metni çevrilmelidir, link adresi değişmeden kalmalıdır: + +* [Yukarıdaki başlığa link](#code-snippets) +* [Internal link](index.md#installation){.internal-link target=_blank} +* External link +* Link to a style +* Link to a script +* Link to an image + +Link metni çevrilmelidir, link adresi çeviriye işaret etmelidir: + +* FastAPI link + +//// + +//// tab | Bilgi + +Link'ler çevrilmelidir, ancak adresleri değişmeden kalmalıdır. Bir istisna, FastAPI dokümantasyonunun sayfalarına verilen mutlak link'lerdir. Bu durumda link, çeviriye işaret etmelidir. + +`script/translate.py` içindeki genel prompt'ta `### Links` bölümüne bakın. + +//// + +## HTML "abbr" öğeleri { #html-abbr-elements } + +//// tab | Test + +Burada HTML "abbr" öğeleriyle sarılmış bazı şeyler var (bazıları uydurma): + +### abbr tam bir ifade verir { #the-abbr-gives-a-full-phrase } + +* GTD +* lt +* XWT +* PSGI + +### abbr bir açıklama verir { #the-abbr-gives-an-explanation } + +* cluster +* Deep Learning + +### abbr tam bir ifade ve bir açıklama verir { #the-abbr-gives-a-full-phrase-and-an-explanation } + +* MDN +* I/O. + +//// + +//// tab | Bilgi + +"abbr" öğelerinin "title" attribute'ları belirli talimatlara göre çevrilir. + +Çeviriler, LLM'nin kaldırmaması gereken kendi "abbr" öğelerini ekleyebilir. Örneğin İngilizce kelimeleri açıklamak için. + +`script/translate.py` içindeki genel prompt'ta `### HTML abbr elements` bölümüne bakın. + +//// + +## Başlıklar { #headings } + +//// tab | Test + +### Bir web uygulaması geliştirin - bir öğretici { #develop-a-webapp-a-tutorial } + +Merhaba. + +### Type hint'ler ve -annotation'lar { #type-hints-and-annotations } + +Tekrar merhaba. + +### Super- ve subclass'lar { #super-and-subclasses } + +Tekrar merhaba. + +//// + +//// tab | Bilgi + +Başlıklarla ilgili tek katı kural, LLM'nin süslü parantezler içindeki hash kısmını değiştirmemesidir; böylece link'ler bozulmaz. + +`script/translate.py` içindeki genel prompt'ta `### Headings` bölümüne bakın. + +Dile özel bazı talimatlar için örneğin `docs/de/llm-prompt.md` içindeki `### Headings` bölümüne bakın. + +//// + +## Dokümanlarda kullanılan terimler { #terms-used-in-the-docs } + +//// tab | Test + +* siz +* sizin + +* örn. +* vb. + +* `foo` bir `int` olarak +* `bar` bir `str` olarak +* `baz` bir `list` olarak + +* Tutorial - Kullanıcı kılavuzu +* İleri Düzey Kullanıcı Kılavuzu +* SQLModel dokümanları +* API dokümanları +* otomatik dokümanlar + +* Veri Bilimi +* Deep Learning +* Machine Learning +* Dependency Injection +* HTTP Basic authentication +* HTTP Digest +* ISO formatı +* JSON Schema standardı +* JSON schema +* schema tanımı +* Password Flow +* Mobil + +* deprecated +* designed +* invalid +* on the fly +* standard +* default +* case-sensitive +* case-insensitive + +* uygulamayı serve etmek +* sayfayı serve etmek + +* app +* application + +* request +* response +* error response + +* path operation +* path operation decorator +* path operation function + +* body +* request body +* response body +* JSON body +* form body +* file body +* function body + +* parameter +* body parameter +* path parameter +* query parameter +* cookie parameter +* header parameter +* form parameter +* function parameter + +* event +* startup event +* server'ın startup'ı +* shutdown event +* lifespan event + +* handler +* event handler +* exception handler +* handle etmek + +* model +* Pydantic model +* data model +* database model +* form model +* model object + +* class +* base class +* parent class +* subclass +* child class +* sibling class +* class method + +* header +* headers +* authorization header +* `Authorization` header +* forwarded header + +* dependency injection system +* dependency +* dependable +* dependant + +* I/O bound +* CPU bound +* concurrency +* parallelism +* multiprocessing + +* env var +* environment variable +* `PATH` +* `PATH` variable + +* authentication +* authentication provider +* authorization +* authorization form +* authorization provider +* kullanıcı authenticate olur +* sistem kullanıcıyı authenticate eder + +* CLI +* command line interface + +* server +* client + +* cloud provider +* cloud service + +* geliştirme +* geliştirme aşamaları + +* dict +* dictionary +* enumeration +* enum +* enum member + +* encoder +* decoder +* encode etmek +* decode etmek + +* exception +* raise etmek + +* expression +* statement + +* frontend +* backend + +* GitHub discussion +* GitHub issue + +* performance +* performance optimization + +* return type +* return value + +* security +* security scheme + +* task +* background task +* task function + +* template +* template engine + +* type annotation +* type hint + +* server worker +* Uvicorn worker +* Gunicorn Worker +* worker process +* worker class +* workload + +* deployment +* deploy etmek + +* SDK +* software development kit + +* `APIRouter` +* `requirements.txt` +* Bearer Token +* breaking change +* bug +* button +* callable +* code +* commit +* context manager +* coroutine +* database session +* disk +* domain +* engine +* fake X +* HTTP GET method +* item +* library +* lifespan +* lock +* middleware +* mobile application +* module +* mounting +* network +* origin +* override +* payload +* processor +* property +* proxy +* pull request +* query +* RAM +* remote machine +* status code +* string +* tag +* web framework +* wildcard +* return etmek +* validate etmek + +//// + +//// tab | Bilgi + +Bu, dokümanlarda görülen (çoğunlukla) teknik terimlerin eksiksiz ve normatif olmayan bir listesidir. Prompt tasarlayan kişi için, LLM'nin hangi terimlerde desteğe ihtiyaç duyduğunu anlamada yardımcı olabilir. Örneğin iyi bir çeviriyi sürekli daha zayıf bir çeviriye geri alıyorsa. Ya da sizin dilinizde bir terimi çekimlemekte (conjugating/declinating) zorlanıyorsa. + +Örneğin `docs/de/llm-prompt.md` içindeki `### List of English terms and their preferred German translations` bölümüne bakın. + +//// diff --git a/docs/tr/docs/advanced/additional-responses.md b/docs/tr/docs/advanced/additional-responses.md new file mode 100644 index 000000000..c8e372775 --- /dev/null +++ b/docs/tr/docs/advanced/additional-responses.md @@ -0,0 +1,247 @@ +# OpenAPI'de Ek Response'lar { #additional-responses-in-openapi } + +/// warning | Uyarı + +Bu konu oldukça ileri seviye bir konudur. + +**FastAPI**'ye yeni başlıyorsanız buna ihtiyaç duymayabilirsiniz. + +/// + +Ek status code'lar, media type'lar, açıklamalar vb. ile ek response'lar tanımlayabilirsiniz. + +Bu ek response'lar OpenAPI şemasına dahil edilir; dolayısıyla API dokümanlarında da görünürler. + +Ancak bu ek response'lar için, status code'unuzu ve içeriğinizi vererek `JSONResponse` gibi bir `Response`'u doğrudan döndürdüğünüzden emin olmanız gerekir. + +## `model` ile Ek Response { #additional-response-with-model } + +*Path operation decorator*'larınıza `responses` adlı bir parametre geçebilirsiniz. + +Bu parametre bir `dict` alır: anahtarlar her response için status code'lardır (`200` gibi), değerler ise her birine ait bilgileri içeren başka `dict`'lerdir. + +Bu response `dict`'lerinin her birinde, `response_model`'e benzer şekilde bir Pydantic model içeren `model` anahtarı olabilir. + +**FastAPI** bu modeli alır, JSON Schema'sını üretir ve OpenAPI'de doğru yere ekler. + +Örneğin, `404` status code'u ve `Message` Pydantic model'i ile başka bir response tanımlamak için şunu yazabilirsiniz: + +{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *} + +/// note | Not + +`JSONResponse`'u doğrudan döndürmeniz gerektiğini unutmayın. + +/// + +/// info | Bilgi + +`model` anahtarı OpenAPI'nin bir parçası değildir. + +**FastAPI** buradaki Pydantic model'i alır, JSON Schema'yı üretir ve doğru yere yerleştirir. + +Doğru yer şurasıdır: + +* Değeri başka bir JSON nesnesi (`dict`) olan `content` anahtarının içinde: + * Media type anahtarı (örn. `application/json`) bulunur; bunun değeri başka bir JSON nesnesidir ve onun içinde: + * Değeri model'den gelen JSON Schema olan `schema` anahtarı vardır; doğru yer burasıdır. + * **FastAPI** bunu doğrudan gömmek yerine OpenAPI'deki başka bir yerde bulunan global JSON Schema'lara bir referans ekler. Böylece diğer uygulamalar ve client'lar bu JSON Schema'ları doğrudan kullanabilir, daha iyi code generation araçları sağlayabilir, vb. + +/// + +Bu *path operation* için OpenAPI'de üretilen response'lar şöyle olur: + +```JSON hl_lines="3-12" +{ + "responses": { + "404": { + "description": "Additional Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } +} +``` + +Schema'lar OpenAPI şemasının içinde başka bir yere referanslanır: + +```JSON hl_lines="4-16" +{ + "components": { + "schemas": { + "Message": { + "title": "Message", + "required": [ + "message" + ], + "type": "object", + "properties": { + "message": { + "title": "Message", + "type": "string" + } + } + }, + "Item": { + "title": "Item", + "required": [ + "id", + "value" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + } + }, + "ValidationError": { + "title": "ValidationError", + "required": [ + "loc", + "msg", + "type" + ], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "type": "string" + } + }, + "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" + } + } + } + } + } + } +} +``` + +## Ana Response İçin Ek Media Type'lar { #additional-media-types-for-the-main-response } + +Aynı `responses` parametresini, aynı ana response için farklı media type'lar eklemek amacıyla da kullanabilirsiniz. + +Örneğin, `image/png` için ek bir media type ekleyerek, *path operation*'ınızın bir JSON nesnesi (media type `application/json`) ya da bir PNG görseli döndürebildiğini belirtebilirsiniz: + +{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} + +/// note | Not + +Görseli `FileResponse` kullanarak doğrudan döndürmeniz gerektiğine dikkat edin. + +/// + +/// info | Bilgi + +`responses` parametrenizde açıkça farklı bir media type belirtmediğiniz sürece FastAPI, response'un ana response class'ı ile aynı media type'a sahip olduğunu varsayar (varsayılan `application/json`). + +Ancak media type'ı `None` olan özel bir response class belirttiyseniz, FastAPI ilişkili bir model'i olan tüm ek response'lar için `application/json` kullanır. + +/// + +## Bilgileri Birleştirme { #combining-information } + +`response_model`, `status_code` ve `responses` parametreleri dahil olmak üzere, response bilgilerini birden fazla yerden birleştirebilirsiniz. + +Varsayılan `200` status code'unu (ya da gerekiyorsa özel bir tane) kullanarak bir `response_model` tanımlayabilir, ardından aynı response için ek bilgileri `responses` içinde, doğrudan OpenAPI şemasına ekleyebilirsiniz. + +**FastAPI**, `responses` içindeki ek bilgileri korur ve model'inizin JSON Schema'sı ile birleştirir. + +Örneğin, Pydantic model kullanan ve özel bir `description` içeren `404` status code'lu bir response tanımlayabilirsiniz. + +Ayrıca `response_model`'inizi kullanan, ancak özel bir `example` içeren `200` status code'lu bir response da tanımlayabilirsiniz: + +{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *} + +Bunların hepsi OpenAPI'nize birleştirilerek dahil edilir ve API dokümanlarında gösterilir: + + + +## Ön Tanımlı Response'ları Özel Olanlarla Birleştirme { #combine-predefined-responses-and-custom-ones } + +Birçok *path operation* için geçerli olacak bazı ön tanımlı response'larınız olabilir; ancak bunları her *path operation*'ın ihtiyaç duyduğu özel response'larla birleştirmek isteyebilirsiniz. + +Bu durumlarda, Python'daki bir `dict`'i `**dict_to_unpack` ile "unpacking" tekniğini kullanabilirsiniz: + +```Python +old_dict = { + "old key": "old value", + "second old key": "second old value", +} +new_dict = {**old_dict, "new key": "new value"} +``` + +Burada `new_dict`, `old_dict` içindeki tüm key-value çiftlerini ve buna ek olarak yeni key-value çiftini içerir: + +```Python +{ + "old key": "old value", + "second old key": "second old value", + "new key": "new value", +} +``` + +Bu tekniği, *path operation*'larınızda bazı ön tanımlı response'ları yeniden kullanmak ve bunları ek özel response'larla birleştirmek için kullanabilirsiniz. + +Örneğin: + +{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} + +## OpenAPI Response'ları Hakkında Daha Fazla Bilgi { #more-information-about-openapi-responses } + +Response'ların içine tam olarak neleri dahil edebileceğinizi görmek için OpenAPI spesifikasyonundaki şu bölümlere bakabilirsiniz: + +* OpenAPI Responses Object, `Response Object`'i içerir. +* OpenAPI Response Object, buradaki her şeyi `responses` parametreniz içinde, her bir response'un içine doğrudan ekleyebilirsiniz. Buna `description`, `headers`, `content` (bunun içinde farklı media type'lar ve JSON Schema'lar tanımlarsınız) ve `links` dahildir. diff --git a/docs/tr/docs/advanced/additional-status-codes.md b/docs/tr/docs/advanced/additional-status-codes.md new file mode 100644 index 000000000..710a6459f --- /dev/null +++ b/docs/tr/docs/advanced/additional-status-codes.md @@ -0,0 +1,41 @@ +# Ek Status Code'ları { #additional-status-codes } + +Varsayılan olarak **FastAPI**, response'ları bir `JSONResponse` kullanarak döndürür; *path operation*'ınızdan döndürdüğünüz içeriği bu `JSONResponse`'un içine yerleştirir. + +Varsayılan status code'u veya *path operation* içinde sizin belirlediğiniz status code'u kullanır. + +## Ek status code'ları { #additional-status-codes_1 } + +Ana status code'a ek olarak başka status code'lar da döndürmek istiyorsanız, `JSONResponse` gibi bir `Response`'u doğrudan döndürerek bunu yapabilirsiniz ve ek status code'u doğrudan orada ayarlarsınız. + +Örneğin, item'ları güncellemeye izin veren bir *path operation*'ınız olduğunu düşünelim; başarılı olduğunda 200 "OK" HTTP status code'unu döndürüyor olsun. + +Ancak yeni item'ları da kabul etmesini istiyorsunuz. Ve item daha önce yoksa, onu oluşturup 201 "Created" HTTP status code'unu döndürsün. + +Bunu yapmak için `JSONResponse` import edin ve içeriğinizi doğrudan onunla döndürün; istediğiniz `status_code`'u da ayarlayın: + +{* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *} + +/// warning | Uyarı + +Yukarıdaki örnekte olduğu gibi bir `Response`'u doğrudan döndürdüğünüzde, response aynen olduğu gibi döndürülür. + +Bir model ile serialize edilmez, vb. + +İçinde olmasını istediğiniz veriyi taşıdığından emin olun ve değerlerin geçerli JSON olduğundan emin olun (eğer `JSONResponse` kullanıyorsanız). + +/// + +/// note | Teknik Detaylar + +`from starlette.responses import JSONResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.responses` içindekileri `fastapi.responses` altında da sunar. Ancak mevcut response'ların çoğu doğrudan Starlette'ten gelir. `status` için de aynı durum geçerlidir. + +/// + +## OpenAPI ve API docs { #openapi-and-api-docs } + +Ek status code'ları ve response'ları doğrudan döndürürseniz, FastAPI sizin ne döndüreceğinizi önceden bilemeyeceği için bunlar OpenAPI şemasına (API docs) dahil edilmez. + +Ancak bunu kodunuzda şu şekilde dokümante edebilirsiniz: [Ek Response'lar](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/tr/docs/advanced/advanced-dependencies.md b/docs/tr/docs/advanced/advanced-dependencies.md new file mode 100644 index 000000000..48c0b6211 --- /dev/null +++ b/docs/tr/docs/advanced/advanced-dependencies.md @@ -0,0 +1,163 @@ +# Gelişmiş Dependency'ler { #advanced-dependencies } + +## Parametreli dependency'ler { #parameterized-dependencies } + +Şimdiye kadar gördüğümüz tüm dependency'ler sabit bir function ya da class idi. + +Ancak, birçok farklı function veya class tanımlamak zorunda kalmadan, dependency üzerinde bazı parametreler ayarlamak isteyebileceğiniz durumlar olabilir. + +Örneğin, query parametresi `q`'nun belirli bir sabit içeriği barındırıp barındırmadığını kontrol eden bir dependency istediğimizi düşünelim. + +Ama bu sabit içeriği parametreleştirebilmek istiyoruz. + +## "Callable" bir instance { #a-callable-instance } + +Python'da bir class'ın instance'ını "callable" yapmanın bir yolu vardır. + +Class'ın kendisini değil (zaten callable'dır), o class'ın bir instance'ını. + +Bunu yapmak için `__call__` adında bir method tanımlarız: + +{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[12] *} + +Bu durumda, ek parametreleri ve alt-dependency'leri kontrol etmek için **FastAPI**'nin kullanacağı şey bu `__call__` olacaktır; ayrıca daha sonra *path operation function* içindeki parametreye bir değer geçmek için çağrılacak olan da budur. + +## Instance'ı parametreleştirme { #parameterize-the-instance } + +Ve şimdi, dependency'yi "parametreleştirmek" için kullanacağımız instance parametrelerini tanımlamak üzere `__init__` kullanabiliriz: + +{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[9] *} + +Bu durumda **FastAPI**, `__init__` ile asla uğraşmaz veya onu önemsemez; onu doğrudan kendi kodumuzda kullanırız. + +## Bir instance oluşturma { #create-an-instance } + +Bu class'tan bir instance'ı şöyle oluşturabiliriz: + +{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[18] *} + +Böylece dependency'mizi "parametreleştirmiş" oluruz; artık içinde `"bar"` vardır ve bu değer `checker.fixed_content` attribute'u olarak durur. + +## Instance'ı dependency olarak kullanma { #use-the-instance-as-a-dependency } + +Sonra `Depends(FixedContentQueryChecker)` yerine `Depends(checker)` içinde bu `checker`'ı kullanabiliriz. Çünkü dependency, class'ın kendisi değil, `checker` instance'ıdır. + +Ve dependency çözülürken **FastAPI** bu `checker`'ı şöyle çağırır: + +```Python +checker(q="somequery") +``` + +...ve bunun döndürdüğü her şeyi, *path operation function* içinde `fixed_content_included` parametresine dependency değeri olarak geçirir: + +{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[22] *} + +/// tip | İpucu + +Bunların tamamı biraz yapay görünebilir. Ayrıca bunun nasıl faydalı olduğu da henüz çok net olmayabilir. + +Bu örnekler bilerek basit tutuldu; ama mekanizmanın nasıl çalıştığını gösteriyor. + +Security bölümlerinde, aynı şekilde implement edilmiş yardımcı function'lar bulunuyor. + +Buradaki her şeyi anladıysanız, security için kullanılan bu yardımcı araçların arka planda nasıl çalıştığını da zaten biliyorsunuz demektir. + +/// + +## `yield`, `HTTPException`, `except` ve Background Tasks ile Dependency'ler { #dependencies-with-yield-httpexception-except-and-background-tasks } + +/// warning | Uyarı + +Büyük ihtimalle bu teknik detaylara ihtiyacınız yok. + +Bu detaylar, özellikle 0.121.0'dan eski bir FastAPI uygulamanız varsa ve `yield` kullanan dependency'lerle ilgili sorunlar yaşıyorsanız faydalıdır. + +/// + +`yield` kullanan dependency'ler; farklı kullanım senaryolarını kapsamak ve bazı sorunları düzeltmek için zaman içinde evrildi. Aşağıda nelerin değiştiğinin bir özeti var. + +### `yield` ve `scope` ile dependency'ler { #dependencies-with-yield-and-scope } + +0.121.0 sürümünde FastAPI, `yield` kullanan dependency'ler için `Depends(scope="function")` desteğini ekledi. + +`Depends(scope="function")` kullanıldığında, `yield` sonrasındaki çıkış kodu, *path operation function* biter bitmez, response client'a geri gönderilmeden önce çalıştırılır. + +`Depends(scope="request")` (varsayılan) kullanıldığında ise `yield` sonrasındaki çıkış kodu, response gönderildikten sonra çalıştırılır. + +Daha fazlasını [Dependencies with `yield` - Early exit and `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope) dokümantasyonunda okuyabilirsiniz. + +### `yield` ve `StreamingResponse` ile dependency'ler, Teknik Detaylar { #dependencies-with-yield-and-streamingresponse-technical-details } + +FastAPI 0.118.0 öncesinde, `yield` kullanan bir dependency kullanırsanız, *path operation function* döndükten sonra ama response gönderilmeden hemen önce `yield` sonrasındaki çıkış kodu çalıştırılırdı. + +Amaç, response'un ağ üzerinden taşınmasını beklerken gereğinden uzun süre resource tutmaktan kaçınmaktı. + +Bu değişiklik aynı zamanda şunu da ifade ediyordu: `StreamingResponse` döndürürseniz, `yield` kullanan dependency'nin çıkış kodu zaten çalışmış olurdu. + +Örneğin, `yield` kullanan bir dependency içinde bir veritabanı session'ınız varsa, `StreamingResponse` veri stream ederken bu session'ı kullanamazdı; çünkü `yield` sonrasındaki çıkış kodunda session zaten kapatılmış olurdu. + +Bu davranış 0.118.0'da geri alındı ve `yield` sonrasındaki çıkış kodunun, response gönderildikten sonra çalıştırılması sağlandı. + +/// info | Bilgi + +Aşağıda göreceğiniz gibi, bu davranış 0.106.0 sürümünden önceki davranışa oldukça benzer; ancak köşe durumlar için çeşitli iyileştirmeler ve bug fix'ler içerir. + +/// + +#### Erken Çıkış Kodu için Kullanım Senaryoları { #use-cases-with-early-exit-code } + +Bazı özel koşullardaki kullanım senaryoları, response gönderilmeden önce `yield` kullanan dependency'lerin çıkış kodunun çalıştırıldığı eski davranıştan fayda görebilir. + +Örneğin, `yield` kullanan bir dependency içinde yalnızca bir kullanıcıyı doğrulamak için veritabanı session'ı kullanan bir kodunuz olduğunu düşünün; ama bu session *path operation function* içinde bir daha hiç kullanılmıyor, yalnızca dependency içinde kullanılıyor **ve** response'un gönderilmesi uzun sürüyor. Mesela veriyi yavaş gönderen bir `StreamingResponse` var, ama herhangi bir nedenle veritabanını kullanmıyor. + +Bu durumda veritabanı session'ı, response tamamen gönderilene kadar elde tutulur. Ancak session kullanılmıyorsa, bunu elde tutmak gerekli değildir. + +Şöyle görünebilir: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py *} + +`Session`'ın otomatik kapatılması olan çıkış kodu şurada: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} + +...yavaş veri gönderen response'un gönderimi bittikten sonra çalıştırılır: + +{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} + +Ama `generate_stream()` veritabanı session'ını kullanmadığı için, response gönderilirken session'ı açık tutmak aslında gerekli değildir. + +SQLModel (veya SQLAlchemy) kullanarak bu spesifik senaryoya sahipseniz, session'a artık ihtiyacınız kalmadıktan sonra session'ı açıkça kapatabilirsiniz: + +{* ../../docs_src/dependencies/tutorial014_an_py310.py ln[24:28] hl[28] *} + +Böylece session veritabanı bağlantısını serbest bırakır ve diğer request'ler bunu kullanabilir. + +`yield` kullanan bir dependency'den erken çıkış gerektiren farklı bir kullanım senaryonuz varsa, lütfen kullanım senaryonuzla birlikte ve `yield` kullanan dependency'ler için erken kapatmadan neden fayda göreceğinizi açıklayarak bir GitHub Discussion Question oluşturun. + +`yield` kullanan dependency'lerde erken kapatma için ikna edici kullanım senaryoları varsa, erken kapatmayı seçmeli (opt-in) hale getiren yeni bir yöntem eklemeyi düşünebilirim. + +### `yield` ve `except` ile dependency'ler, Teknik Detaylar { #dependencies-with-yield-and-except-technical-details } + +FastAPI 0.110.0 öncesinde, `yield` kullanan bir dependency kullanır, sonra o dependency içinde `except` ile bir exception yakalar ve exception'ı tekrar raise etmezseniz; exception otomatik olarak herhangi bir exception handler'a veya internal server error handler'a raise/forward edilirdi. + +Bu davranış 0.110.0 sürümünde değiştirildi. Amaç, handler olmayan (internal server errors) forward edilmiş exception'ların yönetilmemesinden kaynaklanan bellek tüketimini düzeltmek ve bunu normal Python kodunun davranışıyla tutarlı hale getirmekti. + +### Background Tasks ve `yield` ile dependency'ler, Teknik Detaylar { #background-tasks-and-dependencies-with-yield-technical-details } + +FastAPI 0.106.0 öncesinde, `yield` sonrasında exception raise etmek mümkün değildi; çünkü `yield` kullanan dependency'lerdeki çıkış kodu response gönderildikten *sonra* çalıştırılıyordu. Bu nedenle [Exception Handlers](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} zaten çalışmış olurdu. + +Bu tasarımın ana sebeplerinden biri, background task'lerin içinde dependency'lerin "yield ettiği" aynı objeleri kullanmaya izin vermekti; çünkü çıkış kodu, background task'ler bittikten sonra çalıştırılıyordu. + +Bu davranış FastAPI 0.106.0'da, response'un ağ üzerinde taşınmasını beklerken resource tutmamak amacıyla değiştirildi. + +/// tip | İpucu + +Ek olarak, bir background task normalde ayrı ele alınması gereken bağımsız bir mantık setidir ve kendi resource'larına sahip olmalıdır (ör. kendi veritabanı bağlantısı). + +Bu şekilde muhtemelen daha temiz bir kod elde edersiniz. + +/// + +Bu davranışa güvenerek kod yazdıysanız, artık background task'ler için resource'ları background task'in içinde oluşturmalı ve içeride yalnızca `yield` kullanan dependency'lerin resource'larına bağlı olmayan verileri kullanmalısınız. + +Örneğin, aynı veritabanı session'ını kullanmak yerine background task içinde yeni bir veritabanı session'ı oluşturur ve veritabanındaki objeleri bu yeni session ile alırsınız. Ardından, background task function'ına veritabanından gelen objeyi parametre olarak geçirmek yerine, o objenin ID'sini geçirir ve objeyi background task function'ı içinde yeniden elde edersiniz. diff --git a/docs/tr/docs/advanced/async-tests.md b/docs/tr/docs/advanced/async-tests.md new file mode 100644 index 000000000..82349bbec --- /dev/null +++ b/docs/tr/docs/advanced/async-tests.md @@ -0,0 +1,99 @@ +# Async Testler { #async-tests } + +Sağlanan `TestClient` ile **FastAPI** uygulamalarınızı nasıl test edeceğinizi zaten gördünüz. Şimdiye kadar yalnızca senkron testler yazdık, yani `async` fonksiyonlar kullanmadan. + +Testlerinizde asenkron fonksiyonlar kullanabilmek faydalı olabilir; örneğin veritabanınızı asenkron olarak sorguluyorsanız. Diyelim ki FastAPI uygulamanıza request gönderilmesini test etmek ve ardından async bir veritabanı kütüphanesi kullanırken backend'in doğru veriyi veritabanına başarıyla yazdığını doğrulamak istiyorsunuz. + +Bunu nasıl çalıştırabileceğimize bir bakalım. + +## pytest.mark.anyio { #pytest-mark-anyio } + +Testlerimizde asenkron fonksiyonlar çağırmak istiyorsak, test fonksiyonlarımızın da asenkron olması gerekir. AnyIO bunun için güzel bir plugin sağlar; böylece bazı test fonksiyonlarının asenkron olarak çağrılacağını belirtebiliriz. + +## HTTPX { #httpx } + +**FastAPI** uygulamanız `async def` yerine normal `def` fonksiyonları kullanıyor olsa bile, altta yatan yapı hâlâ bir `async` uygulamadır. + +`TestClient`, standart pytest kullanarak normal `def` test fonksiyonlarınızın içinden asenkron FastAPI uygulamasını çağırmak için içeride bazı “sihirli” işlemler yapar. Ancak bu sihir, onu asenkron fonksiyonların içinde kullandığımızda artık çalışmaz. Testlerimizi asenkron çalıştırdığımızda, test fonksiyonlarımızın içinde `TestClient` kullanamayız. + +`TestClient`, HTTPX tabanlıdır ve neyse ki API'yi test etmek için HTTPX'i doğrudan kullanabiliriz. + +## Örnek { #example } + +Basit bir örnek için, [Bigger Applications](../tutorial/bigger-applications.md){.internal-link target=_blank} ve [Testing](../tutorial/testing.md){.internal-link target=_blank} bölümlerinde anlatılana benzer bir dosya yapısı düşünelim: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +`main.py` dosyası şöyle olur: + +{* ../../docs_src/async_tests/app_a_py39/main.py *} + +`test_main.py` dosyasında `main.py` için testler yer alır, artık şöyle görünebilir: + +{* ../../docs_src/async_tests/app_a_py39/test_main.py *} + +## Çalıştırma { #run-it } + +Testlerinizi her zamanki gibi şu şekilde çalıştırabilirsiniz: + +
+ +```console +$ pytest + +---> 100% +``` + +
+ +## Detaylı Anlatım { #in-detail } + +`@pytest.mark.anyio` marker'ı, pytest'e bu test fonksiyonunun asenkron olarak çağrılması gerektiğini söyler: + +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[7] *} + +/// tip | İpucu + +Test fonksiyonu artık `TestClient` kullanırken eskiden olduğu gibi sadece `def` değil, `async def`. + +/// + +Ardından app ile bir `AsyncClient` oluşturup `await` kullanarak ona async request'ler gönderebiliriz. + +{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[9:12] *} + +Bu, şu kullanıma denktir: + +```Python +response = client.get('/') +``` + +...ki daha önce request'leri `TestClient` ile bu şekilde gönderiyorduk. + +/// tip | İpucu + +Yeni `AsyncClient` ile async/await kullandığımızı unutmayın; request asenkron çalışır. + +/// + +/// warning | Uyarı + +Uygulamanız lifespan event'lerine dayanıyorsa, `AsyncClient` bu event'leri tetiklemez. Tetiklendiklerinden emin olmak için florimondmanca/asgi-lifespan paketindeki `LifespanManager`'ı kullanın. + +/// + +## Diğer Asenkron Fonksiyon Çağrıları { #other-asynchronous-function-calls } + +Test fonksiyonu artık asenkron olduğundan, testlerinizde FastAPI uygulamanıza request göndermenin yanında başka `async` fonksiyonları da (çağırıp `await` ederek) kodunuzun başka yerlerinde yaptığınız gibi aynı şekilde kullanabilirsiniz. + +/// tip | İpucu + +Testlerinize asenkron fonksiyon çağrıları entegre ederken `RuntimeError: Task attached to a different loop` hatasıyla karşılaşırsanız (ör. MongoDB'nin MotorClient kullanımı), event loop gerektiren nesneleri yalnızca async fonksiyonların içinde oluşturmanız gerektiğini unutmayın; örneğin bir `@app.on_event("startup")` callback'i içinde. + +/// diff --git a/docs/tr/docs/advanced/behind-a-proxy.md b/docs/tr/docs/advanced/behind-a-proxy.md new file mode 100644 index 000000000..e70b16960 --- /dev/null +++ b/docs/tr/docs/advanced/behind-a-proxy.md @@ -0,0 +1,466 @@ +# Proxy Arkasında Çalıştırma { #behind-a-proxy } + +Birçok durumda, FastAPI uygulamanızın önünde Traefik veya Nginx gibi bir **proxy** kullanırsınız. + +Bu proxy'ler HTTPS sertifikalarını ve diğer bazı işleri üstlenebilir. + +## Proxy Forwarded Header'ları { #proxy-forwarded-headers } + +Uygulamanızın önündeki bir **proxy**, request'leri **server**'ınıza göndermeden önce genelde bazı header'ları dinamik olarak ayarlar. Böylece server, request'in proxy tarafından **forward** edildiğini; domain dahil orijinal (public) URL'yi, HTTPS kullanıldığını vb. bilgileri anlayabilir. + +**Server** programı (örneğin **FastAPI CLI** üzerinden **Uvicorn**) bu header'ları yorumlayabilir ve ardından bu bilgiyi uygulamanıza aktarabilir. + +Ancak güvenlik nedeniyle, server güvenilir bir proxy arkasında olduğunu bilmediği için bu header'ları yorumlamaz. + +/// note | Teknik Detaylar + +Proxy header'ları şunlardır: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +### Proxy Forwarded Header'larını Etkinleştirme { #enable-proxy-forwarded-headers } + +FastAPI CLI'yi `--forwarded-allow-ips` *CLI Option*'ı ile başlatıp, bu forwarded header'ları okumada güvenilecek IP adreslerini verebilirsiniz. + +Bunu `--forwarded-allow-ips="*"` olarak ayarlarsanız, gelen tüm IP'lere güvenir. + +**Server**'ınız güvenilir bir **proxy** arkasındaysa ve onunla sadece proxy konuşuyorsa, bu ayar server'ın o **proxy**'nin IP'si her neyse onu kabul etmesini sağlar. + +
+ +```console +$ fastapi run --forwarded-allow-ips="*" + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### HTTPS ile Redirect'ler { #redirects-with-https } + +Örneğin `/items/` adında bir *path operation* tanımladığınızı düşünelim: + +{* ../../docs_src/behind_a_proxy/tutorial001_01_py39.py hl[6] *} + +Client `/items`'a gitmeye çalışırsa, varsayılan olarak `/items/`'a redirect edilir. + +Ancak *CLI Option* `--forwarded-allow-ips` ayarlanmadan önce, `http://localhost:8000/items/`'a redirect edebilir. + +Oysa uygulamanız `https://mysuperapp.com` üzerinde host ediliyor olabilir ve redirect'in `https://mysuperapp.com/items/` olması gerekir. + +Artık `--proxy-headers` ayarını yaparak FastAPI'nin doğru adrese redirect edebilmesini sağlarsınız. 😎 + +``` +https://mysuperapp.com/items/ +``` + +/// tip | İpucu + +HTTPS hakkında daha fazla bilgi için [HTTPS Hakkında](../deployment/https.md){.internal-link target=_blank} rehberine bakın. + +/// + +### Proxy Forwarded Header'ları Nasıl Çalışır { #how-proxy-forwarded-headers-work } + +**Proxy**'nin, client ile **application server** arasında forwarded header'ları nasıl eklediğini gösteren görsel bir temsil: + +```mermaid +sequenceDiagram + participant Client + participant Proxy as Proxy/Load Balancer + participant Server as FastAPI Server + + Client->>Proxy: HTTPS Request
Host: mysuperapp.com
Path: /items + + Note over Proxy: Proxy adds forwarded headers + + Proxy->>Server: HTTP Request
X-Forwarded-For: [client IP]
X-Forwarded-Proto: https
X-Forwarded-Host: mysuperapp.com
Path: /items + + Note over Server: Server interprets headers
(if --forwarded-allow-ips is set) + + Server->>Proxy: HTTP Response
with correct HTTPS URLs + + Proxy->>Client: HTTPS Response +``` + +**Proxy**, orijinal client request'ini araya girerek (intercept) alır ve request'i **application server**'a iletmeden önce özel *forwarded* header'ları (`X-Forwarded-*`) ekler. + +Bu header'lar, aksi halde kaybolacak olan orijinal request bilgilerini korur: + +* **X-Forwarded-For**: Orijinal client'ın IP adresi +* **X-Forwarded-Proto**: Orijinal protokol (`https`) +* **X-Forwarded-Host**: Orijinal host (`mysuperapp.com`) + +**FastAPI CLI** `--forwarded-allow-ips` ile yapılandırıldığında bu header'lara güvenir ve örneğin redirect'lerde doğru URL'leri üretmek için bunları kullanır. + +## Path Prefix'i Kırpılan (Stripped) Bir Proxy { #proxy-with-a-stripped-path-prefix } + +Uygulamanıza bir path prefix ekleyen bir proxy'niz olabilir. + +Bu durumlarda uygulamanızı yapılandırmak için `root_path` kullanabilirsiniz. + +`root_path`, FastAPI'nin (Starlette üzerinden) üzerine kurulduğu ASGI spesifikasyonunun sağladığı bir mekanizmadır. + +`root_path` bu özel senaryoları yönetmek için kullanılır. + +Ayrıca sub-application mount ederken de içeride kullanılır. + +Path prefix'i kırpılan bir proxy kullanmak, şu anlama gelir: Kodunuzda `/app` altında bir path tanımlarsınız; ancak üstte bir katman (proxy) ekleyip **FastAPI** uygulamanızı `/api/v1` gibi bir path'in altına koyarsınız. + +Bu durumda, orijinal `/app` path'i aslında `/api/v1/app` altında servis edilir. + +Kodunuzun tamamı sadece `/app` varmış gibi yazılmış olsa bile. + +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[6] *} + +Proxy, request'i app server'a (muhtemelen FastAPI CLI üzerinden Uvicorn) iletmeden önce **path prefix**'i anlık olarak **"kırpar"** (strip). Böylece uygulamanız hâlâ `/app` altında servis ediliyormuş gibi davranır ve tüm kodunuzu `/api/v1` prefix'ini içerecek şekilde güncellemeniz gerekmez. + +Buraya kadar her şey normal çalışır. + +Ancak entegre doküman arayüzünü (frontend) açtığınızda, OpenAPI şemasını `/api/v1/openapi.json` yerine `/openapi.json` üzerinden almayı bekler. + +Dolayısıyla tarayıcıda çalışan frontend `/openapi.json`'a erişmeye çalışır ve OpenAPI şemasını alamaz. + +Çünkü uygulamamız proxy arkasında `/api/v1` path prefix'i ile çalışmaktadır; frontend'in OpenAPI şemasını `/api/v1/openapi.json` üzerinden çekmesi gerekir. + +```mermaid +graph LR + +browser("Browser") +proxy["Proxy on http://0.0.0.0:9999/api/v1/app"] +server["Server on http://127.0.0.1:8000/app"] + +browser --> proxy +proxy --> server +``` + +/// tip | İpucu + +`0.0.0.0` IP'si, genelde programın ilgili makine/server üzerindeki tüm kullanılabilir IP'lerde dinlediği anlamına gelir. + +/// + +Docs UI'nin, bu API `server`'ının (proxy arkasında) `/api/v1` altında bulunduğunu belirtmek için OpenAPI şemasına da ihtiyacı olur. Örneğin: + +```JSON hl_lines="4-8" +{ + "openapi": "3.1.0", + // More stuff here + "servers": [ + { + "url": "/api/v1" + } + ], + "paths": { + // More stuff here + } +} +``` + +Bu örnekte "Proxy", **Traefik** gibi bir şey olabilir. Server da FastAPI uygulamanızı çalıştıran (Uvicorn'lu) FastAPI CLI olabilir. + +### `root_path` Sağlama { #providing-the-root-path } + +Bunu yapmak için `--root-path` komut satırı seçeneğini şöyle kullanabilirsiniz: + +
+ +```console +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Hypercorn kullanıyorsanız, onda da `--root-path` seçeneği vardır. + +/// note | Teknik Detaylar + +ASGI spesifikasyonu bu kullanım senaryosu için bir `root_path` tanımlar. + +`--root-path` komut satırı seçeneği de bu `root_path`'i sağlar. + +/// + +### Mevcut `root_path`'i Kontrol Etme { #checking-the-current-root-path } + +Uygulamanızın her request için kullandığı mevcut `root_path` değerini alabilirsiniz; bu değer ASGI spesifikasyonunun bir parçası olan `scope` dict'inin içindedir. + +Burada sadece göstermek için bunu mesaja dahil ediyoruz. + +{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[8] *} + +Ardından Uvicorn'u şu şekilde başlatırsanız: + +
+ +```console +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Response şöyle bir şey olur: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +### FastAPI Uygulamasında `root_path` Ayarlama { #setting-the-root-path-in-the-fastapi-app } + +Alternatif olarak, `--root-path` gibi bir komut satırı seçeneği (veya muadili) sağlayamıyorsanız, FastAPI uygulamanızı oluştururken `root_path` parametresini ayarlayabilirsiniz: + +{* ../../docs_src/behind_a_proxy/tutorial002_py39.py hl[3] *} + +`FastAPI`'ye `root_path` vermek, Uvicorn veya Hypercorn'a `--root-path` komut satırı seçeneğini vermekle eşdeğerdir. + +### `root_path` Hakkında { #about-root-path } + +Şunu unutmayın: Server (Uvicorn) bu `root_path`'i, uygulamaya iletmek dışında başka bir amaçla kullanmaz. + +Ancak tarayıcınızla http://127.0.0.1:8000/app adresine giderseniz normal response'u görürsünüz: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +Yani `http://127.0.0.1:8000/api/v1/app` üzerinden erişilmeyi beklemez. + +Uvicorn, proxy'nin Uvicorn'a `http://127.0.0.1:8000/app` üzerinden erişmesini bekler; bunun üstüne ekstra `/api/v1` prefix'ini eklemek proxy'nin sorumluluğudur. + +## Stripped Path Prefix Kullanan Proxy'ler Hakkında { #about-proxies-with-a-stripped-path-prefix } + +Stripped path prefix kullanan bir proxy, yapılandırma yöntemlerinden yalnızca biridir. + +Birçok durumda varsayılan davranış, proxy'nin stripped path prefix kullanmaması olacaktır. + +Böyle bir durumda (stripped path prefix olmadan), proxy `https://myawesomeapp.com` gibi bir yerde dinler; tarayıcı `https://myawesomeapp.com/api/v1/app`'e giderse ve sizin server'ınız (ör. Uvicorn) `http://127.0.0.1:8000` üzerinde dinliyorsa, proxy (stripped path prefix olmadan) Uvicorn'a aynı path ile erişir: `http://127.0.0.1:8000/api/v1/app`. + +## Traefik ile Local Olarak Test Etme { #testing-locally-with-traefik } + +Traefik kullanarak, stripped path prefix'li deneyi local'de kolayca çalıştırabilirsiniz. + +Traefik'i indirin; tek bir binary'dir, sıkıştırılmış dosyayı çıkarıp doğrudan terminalden çalıştırabilirsiniz. + +Ardından `traefik.toml` adında bir dosya oluşturup şunu yazın: + +```TOML hl_lines="3" +[entryPoints] + [entryPoints.http] + address = ":9999" + +[providers] + [providers.file] + filename = "routes.toml" +``` + +Bu, Traefik'e 9999 portunda dinlemesini ve `routes.toml` adlı başka bir dosyayı kullanmasını söyler. + +/// tip | İpucu + +Standart HTTP portu 80 yerine 9999 portunu kullanıyoruz; böylece admin (`sudo`) yetkileriyle çalıştırmanız gerekmez. + +/// + +Şimdi diğer dosyayı, `routes.toml`'u oluşturun: + +```TOML hl_lines="5 12 20" +[http] + [http.middlewares] + + [http.middlewares.api-stripprefix.stripPrefix] + prefixes = ["/api/v1"] + + [http.routers] + + [http.routers.app-http] + entryPoints = ["http"] + service = "app" + rule = "PathPrefix(`/api/v1`)" + middlewares = ["api-stripprefix"] + + [http.services] + + [http.services.app] + [http.services.app.loadBalancer] + [[http.services.app.loadBalancer.servers]] + url = "http://127.0.0.1:8000" +``` + +Bu dosya, Traefik'i `/api/v1` path prefix'ini kullanacak şekilde yapılandırır. + +Ardından Traefik, request'leri `http://127.0.0.1:8000` üzerinde çalışan Uvicorn'unuza yönlendirir. + +Şimdi Traefik'i başlatın: + +
+ +```console +$ ./traefik --configFile=traefik.toml + +INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml +``` + +
+ +Ve şimdi uygulamanızı `--root-path` seçeneğiyle başlatın: + +
+ +```console +$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Response'ları Kontrol Edin { #check-the-responses } + +Şimdi Uvicorn'un portundaki URL'ye giderseniz: http://127.0.0.1:8000/app, normal response'u görürsünüz: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +/// tip | İpucu + +`http://127.0.0.1:8000/app` üzerinden erişiyor olsanız bile, `root_path` değerinin `--root-path` seçeneğinden alınıp `/api/v1` olarak gösterildiğine dikkat edin. + +/// + +Şimdi de Traefik'in portundaki URL'yi, path prefix ile birlikte açın: http://127.0.0.1:9999/api/v1/app. + +Aynı response'u alırız: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +ama bu sefer proxy'nin sağladığı prefix path olan `/api/v1` ile gelen URL'de. + +Elbette buradaki fikir, herkesin uygulamaya proxy üzerinden erişmesidir; dolayısıyla `/api/v1` path prefix'li sürüm "doğru" olandır. + +Uvicorn'un doğrudan sunduğu, path prefix olmayan sürüm (`http://127.0.0.1:8000/app`) ise sadece _proxy_'nin (Traefik) erişmesi için kullanılmalıdır. + +Bu da Proxy'nin (Traefik) path prefix'i nasıl kullandığını ve server'ın (Uvicorn) `--root-path` seçeneğinden gelen `root_path`'i nasıl kullandığını gösterir. + +### Docs UI'yi Kontrol Edin { #check-the-docs-ui } + +Şimdi işin eğlenceli kısmı. ✨ + +Uygulamaya erişmenin "resmi" yolu, tanımladığımız path prefix ile proxy üzerinden erişmektir. Bu yüzden beklendiği gibi, Uvicorn'un doğrudan servis ettiği docs UI'yi URL'de path prefix olmadan açarsanız çalışmaz; çünkü proxy üzerinden erişileceğini varsayar. + +Şuradan kontrol edebilirsiniz: http://127.0.0.1:8000/docs: + + + +Ancak docs UI'yi proxy üzerinden, `9999` portuyla, `/api/v1/docs` altında "resmi" URL'den açarsak doğru çalışır! 🎉 + +Şuradan kontrol edebilirsiniz: http://127.0.0.1:9999/api/v1/docs: + + + +Tam istediğimiz gibi. ✔️ + +Bunun nedeni, FastAPI'nin OpenAPI içinde varsayılan `server`'ı, `root_path` tarafından verilen URL ile oluşturmak için bu `root_path`'i kullanmasıdır. + +## Ek `server`'lar { #additional-servers } + +/// warning | Uyarı + +Bu daha ileri seviye bir kullanım senaryosudur. İsterseniz atlayabilirsiniz. + +/// + +Varsayılan olarak **FastAPI**, OpenAPI şemasında `root_path` için bir `server` oluşturur. + +Ancak başka alternatif `servers` da sağlayabilirsiniz; örneğin *aynı* docs UI'nin hem staging hem de production ortamıyla etkileşime girmesini istiyorsanız. + +Özel bir `servers` listesi verirseniz ve bir `root_path` varsa (çünkü API'niz proxy arkasındadır), **FastAPI** bu `root_path` ile bir "server"ı listenin başına ekler. + +Örneğin: + +{* ../../docs_src/behind_a_proxy/tutorial003_py39.py hl[4:7] *} + +Şöyle bir OpenAPI şeması üretir: + +```JSON hl_lines="5-7" +{ + "openapi": "3.1.0", + // More stuff here + "servers": [ + { + "url": "/api/v1" + }, + { + "url": "https://stag.example.com", + "description": "Staging environment" + }, + { + "url": "https://prod.example.com", + "description": "Production environment" + } + ], + "paths": { + // More stuff here + } +} +``` + +/// tip | İpucu + +`url` değeri `/api/v1` olan, `root_path`'ten alınmış otomatik üretilen server'a dikkat edin. + +/// + +Docs UI'de, http://127.0.0.1:9999/api/v1/docs adresinde şöyle görünür: + + + +/// tip | İpucu + +Docs UI, seçtiğiniz server ile etkileşime girer. + +/// + +/// note | Teknik Detaylar + +OpenAPI spesifikasyonunda `servers` özelliği opsiyoneldir. + +`servers` parametresini belirtmezseniz ve `root_path` `/` ile aynıysa, üretilen OpenAPI şemasında `servers` özelliği varsayılan olarak tamamen çıkarılır; bu da `url` değeri `/` olan tek bir server ile eşdeğerdir. + +/// + +### `root_path`'ten Otomatik `server` Eklenmesini Kapatma { #disable-automatic-server-from-root-path } + +**FastAPI**'nin `root_path` kullanarak otomatik bir server eklemesini istemiyorsanız, `root_path_in_servers=False` parametresini kullanabilirsiniz: + +{* ../../docs_src/behind_a_proxy/tutorial004_py39.py hl[9] *} + +Böylece OpenAPI şemasına dahil etmez. + +## Bir Sub-Application Mount Etme { #mounting-a-sub-application } + +Bir sub-application'ı ( [Sub Applications - Mounts](sub-applications.md){.internal-link target=_blank} bölümünde anlatıldığı gibi) mount etmeniz gerekiyorsa ve aynı zamanda `root_path` ile bir proxy kullanıyorsanız, bunu beklendiği gibi normal şekilde yapabilirsiniz. + +FastAPI içeride `root_path`'i akıllıca kullanır; dolayısıyla doğrudan çalışır. ✨ diff --git a/docs/tr/docs/advanced/custom-response.md b/docs/tr/docs/advanced/custom-response.md new file mode 100644 index 000000000..c5148f428 --- /dev/null +++ b/docs/tr/docs/advanced/custom-response.md @@ -0,0 +1,312 @@ +# Özel Response - HTML, Stream, File ve Diğerleri { #custom-response-html-stream-file-others } + +Varsayılan olarak **FastAPI**, response'ları `JSONResponse` kullanarak döndürür. + +Bunu, [Doğrudan bir Response döndür](response-directly.md){.internal-link target=_blank} bölümünde gördüğünüz gibi doğrudan bir `Response` döndürerek geçersiz kılabilirsiniz. + +Ancak doğrudan bir `Response` döndürürseniz (veya `JSONResponse` gibi herhangi bir alt sınıfını), veri otomatik olarak dönüştürülmez (bir `response_model` tanımlamış olsanız bile) ve dokümantasyon da otomatik üretilmez (örneğin, üretilen OpenAPI’nin parçası olarak HTTP header `Content-Type` içindeki ilgili "media type" dahil edilmez). + +Bununla birlikte, *path operation decorator* içinde `response_class` parametresini kullanarak hangi `Response`’un (örn. herhangi bir `Response` alt sınıfı) kullanılacağını da ilan edebilirsiniz. + +*path operation function*’ınızdan döndürdüğünüz içerik, o `Response`’un içine yerleştirilir. + +Ve eğer bu `Response` ( `JSONResponse` ve `UJSONResponse`’ta olduğu gibi) bir JSON media type’a (`application/json`) sahipse, döndürdüğünüz veri; *path operation decorator* içinde tanımladığınız herhangi bir Pydantic `response_model` ile otomatik olarak dönüştürülür (ve filtrelenir). + +/// note | Not + +Media type’ı olmayan bir response class kullanırsanız, FastAPI response’unuzun content içermediğini varsayar; bu yüzden ürettiği OpenAPI dokümanında response formatını dokümante etmez. + +/// + +## `ORJSONResponse` Kullan { #use-orjsonresponse } + +Örneğin performansı sıkıştırmaya çalışıyorsanız, `orjson` kurup kullanabilir ve response’u `ORJSONResponse` olarak ayarlayabilirsiniz. + +Kullanmak istediğiniz `Response` class’ını (alt sınıfını) import edin ve *path operation decorator* içinde tanımlayın. + +Büyük response'larda, doğrudan bir `Response` döndürmek bir dictionary döndürmekten çok daha hızlıdır. + +Çünkü varsayılan olarak FastAPI, içindeki her item’ı inceleyip JSON olarak serialize edilebilir olduğundan emin olur; tutorial’da anlatılan aynı [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} mekanizmasını kullanır. Bu da örneğin veritabanı modelleri gibi **keyfi objeleri** döndürebilmenizi sağlar. + +Ancak döndürdüğünüz içeriğin **JSON ile serialize edilebilir** olduğundan eminseniz, onu doğrudan response class’ına verebilir ve FastAPI’nin response class’ına vermeden önce dönüş içeriğinizi `jsonable_encoder` içinden geçirirken oluşturacağı ek yükten kaçınabilirsiniz. + +{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *} + +/// info | Bilgi + +`response_class` parametresi, response’un "media type"’ını tanımlamak için de kullanılır. + +Bu durumda HTTP header `Content-Type`, `application/json` olarak ayarlanır. + +Ve OpenAPI’de de bu şekilde dokümante edilir. + +/// + +/// tip | İpucu + +`ORJSONResponse` yalnızca FastAPI’de vardır, Starlette’te yoktur. + +/// + +## HTML Response { #html-response } + +**FastAPI**’den doğrudan HTML içeren bir response döndürmek için `HTMLResponse` kullanın. + +* `HTMLResponse` import edin. +* *path operation decorator*’ınızın `response_class` parametresi olarak `HTMLResponse` verin. + +{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *} + +/// info | Bilgi + +`response_class` parametresi, response’un "media type"’ını tanımlamak için de kullanılır. + +Bu durumda HTTP header `Content-Type`, `text/html` olarak ayarlanır. + +Ve OpenAPI’de de bu şekilde dokümante edilir. + +/// + +### Bir `Response` Döndür { #return-a-response } + +[Doğrudan bir Response döndür](response-directly.md){.internal-link target=_blank} bölümünde görüldüğü gibi, *path operation* içinde doğrudan bir response döndürerek response’u override edebilirsiniz. + +Yukarıdaki örneğin aynısı, bu sefer bir `HTMLResponse` döndürerek, şöyle görünebilir: + +{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *} + +/// warning | Uyarı + +*path operation function*’ınızın doğrudan döndürdüğü bir `Response`, OpenAPI’de dokümante edilmez (örneğin `Content-Type` dokümante edilmez) ve otomatik interaktif dokümanlarda görünmez. + +/// + +/// info | Bilgi + +Elbette gerçek `Content-Type` header’ı, status code vb. değerler, döndürdüğünüz `Response` objesinden gelir. + +/// + +### OpenAPI’de Dokümante Et ve `Response`’u Override Et { #document-in-openapi-and-override-response } + +Response’u fonksiyonun içinden override etmek ama aynı zamanda OpenAPI’de "media type"’ı dokümante etmek istiyorsanız, `response_class` parametresini kullanıp ayrıca bir `Response` objesi döndürebilirsiniz. + +Bu durumda `response_class` sadece OpenAPI *path operation*’ını dokümante etmek için kullanılır; sizin `Response`’unuz ise olduğu gibi kullanılır. + +#### Doğrudan bir `HTMLResponse` Döndür { #return-an-htmlresponse-directly } + +Örneğin şöyle bir şey olabilir: + +{* ../../docs_src/custom_response/tutorial004_py39.py hl[7,21,23] *} + +Bu örnekte `generate_html_response()` fonksiyonu, HTML’i bir `str` olarak döndürmek yerine zaten bir `Response` üretip döndürmektedir. + +`generate_html_response()` çağrısının sonucunu döndürerek, varsayılan **FastAPI** davranışını override edecek bir `Response` döndürmüş olursunuz. + +Ama `response_class` içinde `HTMLResponse` da verdiğiniz için **FastAPI**, bunu OpenAPI’de ve interaktif dokümanlarda `text/html` ile HTML olarak nasıl dokümante edeceğini bilir: + + + +## Mevcut Response'lar { #available-responses } + +Mevcut response'lardan bazıları aşağıdadır. + +Unutmayın: `Response` ile başka herhangi bir şeyi döndürebilir, hatta özel bir alt sınıf da oluşturabilirsiniz. + +/// note | Teknik Detaylar + +`from starlette.responses import HTMLResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici için kolaylık olsun diye `starlette.responses` içindekileri `fastapi.responses` olarak da sağlar. Ancak mevcut response'ların çoğu doğrudan Starlette’ten gelir. + +/// + +### `Response` { #response } + +Ana `Response` class’ıdır; diğer tüm response'lar bundan türetilir. + +Bunu doğrudan döndürebilirsiniz. + +Şu parametreleri kabul eder: + +* `content` - Bir `str` veya `bytes`. +* `status_code` - Bir `int` HTTP status code. +* `headers` - String’lerden oluşan bir `dict`. +* `media_type` - Media type’ı veren bir `str`. Örn. `"text/html"`. + +FastAPI (aslında Starlette) otomatik olarak bir Content-Length header’ı ekler. Ayrıca `media_type`’a göre bir Content-Type header’ı ekler ve text türleri için sona bir charset ekler. + +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} + +### `HTMLResponse` { #htmlresponse } + +Yukarıda okuduğunuz gibi, bir miktar text veya bytes alır ve HTML response döndürür. + +### `PlainTextResponse` { #plaintextresponse } + +Bir miktar text veya bytes alır ve düz metin response döndürür. + +{* ../../docs_src/custom_response/tutorial005_py39.py hl[2,7,9] *} + +### `JSONResponse` { #jsonresponse } + +Bir miktar veri alır ve `application/json` olarak encode edilmiş bir response döndürür. + +Yukarıda okuduğunuz gibi, **FastAPI**’de varsayılan response budur. + +### `ORJSONResponse` { #orjsonresponse } + +Yukarıda okuduğunuz gibi `orjson` kullanan hızlı bir alternatif JSON response. + +/// info | Bilgi + +Bunun için `orjson` kurulmalıdır; örneğin `pip install orjson`. + +/// + +### `UJSONResponse` { #ujsonresponse } + +`ujson` kullanan alternatif bir JSON response. + +/// info | Bilgi + +Bunun için `ujson` kurulmalıdır; örneğin `pip install ujson`. + +/// + +/// warning | Uyarı + +`ujson`, bazı edge-case’leri ele alma konusunda Python’un built-in implementasyonu kadar dikkatli değildir. + +/// + +{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *} + +/// tip | İpucu + +`ORJSONResponse` daha hızlı bir alternatif olabilir. + +/// + +### `RedirectResponse` { #redirectresponse } + +HTTP redirect döndürür. Varsayılan olarak 307 status code (Temporary Redirect) kullanır. + +`RedirectResponse`’u doğrudan döndürebilirsiniz: + +{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *} + +--- + +Veya `response_class` parametresi içinde kullanabilirsiniz: + +{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *} + +Bunu yaparsanız, *path operation* function’ınızdan doğrudan URL döndürebilirsiniz. + +Bu durumda kullanılan `status_code`, `RedirectResponse` için varsayılan olan `307` olur. + +--- + +Ayrıca `status_code` parametresini `response_class` parametresiyle birlikte kullanabilirsiniz: + +{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *} + +### `StreamingResponse` { #streamingresponse } + +Bir async generator veya normal generator/iterator alır ve response body’yi stream eder. + +{* ../../docs_src/custom_response/tutorial007_py39.py hl[2,14] *} + +#### `StreamingResponse`’u file-like objelerle kullanma { #using-streamingresponse-with-file-like-objects } + +Bir file-like objeniz varsa (örn. `open()`’ın döndürdüğü obje), o file-like obje üzerinde iterate eden bir generator function oluşturabilirsiniz. + +Böylece önce hepsini memory’ye okumak zorunda kalmazsınız; bu generator function’ı `StreamingResponse`’a verip döndürebilirsiniz. + +Buna cloud storage ile etkileşime giren, video işleyen ve benzeri birçok kütüphane dahildir. + +{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *} + +1. Bu generator function’dır. İçinde `yield` ifadeleri olduğu için "generator function" denir. +2. Bir `with` bloğu kullanarak, generator function bittiğinde file-like objenin kapandığından emin oluruz. Yani response göndermeyi bitirdikten sonra kapanır. +3. Bu `yield from`, fonksiyona `file_like` isimli şeyi iterate etmesini söyler. Ardından iterate edilen her parça için, o parçayı bu generator function’dan (`iterfile`) geliyormuş gibi yield eder. + + Yani, içerdeki "üretme" (generating) işini başka bir şeye devreden bir generator function’dır. + + Bunu bu şekilde yaptığımızda `with` bloğu içinde tutabilir ve böylece iş bitince file-like objenin kapanmasını garanti edebiliriz. + +/// tip | İpucu + +Burada `async` ve `await` desteklemeyen standart `open()` kullandığımız için path operation’ı normal `def` ile tanımlarız. + +/// + +### `FileResponse` { #fileresponse } + +Asenkron olarak bir dosyayı response olarak stream eder. + +Diğer response türlerine göre instantiate ederken farklı argümanlar alır: + +* `path` - Stream edilecek dosyanın dosya path'i. +* `headers` - Eklenecek özel header’lar; dictionary olarak. +* `media_type` - Media type’ı veren string. Ayarlanmazsa, dosya adı veya path kullanılarak media type tahmin edilir. +* `filename` - Ayarlanırsa response içindeki `Content-Disposition`’a dahil edilir. + +File response'ları uygun `Content-Length`, `Last-Modified` ve `ETag` header’larını içerir. + +{* ../../docs_src/custom_response/tutorial009_py39.py hl[2,10] *} + +`response_class` parametresini de kullanabilirsiniz: + +{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *} + +Bu durumda *path operation* function’ınızdan doğrudan dosya path'ini döndürebilirsiniz. + +## Özel response class { #custom-response-class } + +`Response`’dan türeterek kendi özel response class’ınızı oluşturabilir ve kullanabilirsiniz. + +Örneğin, dahil gelen `ORJSONResponse` class’ında kullanılmayan bazı özel ayarlarla `orjson` kullanmak istediğinizi varsayalım. + +Diyelim ki girintili ve biçimlendirilmiş JSON döndürmek istiyorsunuz; bunun için `orjson.OPT_INDENT_2` seçeneğini kullanmak istiyorsunuz. + +Bir `CustomORJSONResponse` oluşturabilirsiniz. Burada yapmanız gereken temel şey, content’i `bytes` olarak döndüren bir `Response.render(content)` metodu yazmaktır: + +{* ../../docs_src/custom_response/tutorial009c_py39.py hl[9:14,17] *} + +Artık şunu döndürmek yerine: + +```json +{"message": "Hello World"} +``` + +...bu response şunu döndürür: + +```json +{ + "message": "Hello World" +} +``` + +Elbette JSON’u formatlamaktan çok daha iyi şekillerde bundan faydalanabilirsiniz. 😉 + +## Varsayılan response class { #default-response-class } + +Bir **FastAPI** class instance’ı veya bir `APIRouter` oluştururken, varsayılan olarak hangi response class’ının kullanılacağını belirtebilirsiniz. + +Bunu tanımlayan parametre `default_response_class`’tır. + +Aşağıdaki örnekte **FastAPI**, tüm *path operations* için varsayılan olarak `JSONResponse` yerine `ORJSONResponse` kullanır. + +{* ../../docs_src/custom_response/tutorial010_py39.py hl[2,4] *} + +/// tip | İpucu + +Daha önce olduğu gibi, *path operations* içinde `response_class`’ı yine override edebilirsiniz. + +/// + +## Ek dokümantasyon { #additional-documentation } + +OpenAPI’de media type’ı ve daha birçok detayı `responses` kullanarak da tanımlayabilirsiniz: [OpenAPI’de Ek Response'lar](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/tr/docs/advanced/dataclasses.md b/docs/tr/docs/advanced/dataclasses.md new file mode 100644 index 000000000..263976007 --- /dev/null +++ b/docs/tr/docs/advanced/dataclasses.md @@ -0,0 +1,95 @@ +# Dataclass Kullanımı { #using-dataclasses } + +FastAPI, **Pydantic** üzerine inşa edilmiştir ve request/response tanımlamak için Pydantic model'lerini nasıl kullanacağınızı gösteriyordum. + +Ancak FastAPI, `dataclasses` kullanmayı da aynı şekilde destekler: + +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} + +Bu destek hâlâ **Pydantic** sayesinde vardır; çünkü Pydantic, `dataclasses` için dahili destek sunar. + +Yani yukarıdaki kod Pydantic'i doğrudan kullanmasa bile, FastAPI bu standart dataclass'ları Pydantic'in kendi dataclass biçimine dönüştürmek için Pydantic'i kullanmaktadır. + +Ve elbette aynı özellikleri destekler: + +* veri doğrulama (data validation) +* veri serileştirme (data serialization) +* veri dokümantasyonu (data documentation), vb. + +Bu, Pydantic model'lerinde olduğu gibi çalışır. Aslında arka planda da aynı şekilde, Pydantic kullanılarak yapılır. + +/// info | Bilgi + +Dataclass'ların, Pydantic model'lerinin yapabildiği her şeyi yapamadığını unutmayın. + +Bu yüzden yine de Pydantic model'lerini kullanmanız gerekebilir. + +Ancak elinizde zaten bir sürü dataclass varsa, bunları FastAPI ile bir web API'yi beslemek için kullanmak güzel bir numaradır. 🤓 + +/// + +## `response_model` İçinde Dataclass'lar { #dataclasses-in-response-model } + +`response_model` parametresinde `dataclasses` da kullanabilirsiniz: + +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} + +Dataclass otomatik olarak bir Pydantic dataclass'ına dönüştürülür. + +Bu sayede şeması API docs kullanıcı arayüzünde görünür: + + + +## İç İçe Veri Yapılarında Dataclass'lar { #dataclasses-in-nested-data-structures } + +İç içe veri yapıları oluşturmak için `dataclasses` ile diğer type annotation'ları da birleştirebilirsiniz. + +Bazı durumlarda yine de Pydantic'in `dataclasses` sürümünü kullanmanız gerekebilir. Örneğin, otomatik oluşturulan API dokümantasyonunda hata alıyorsanız. + +Bu durumda standart `dataclasses` yerine, drop-in replacement olan `pydantic.dataclasses` kullanabilirsiniz: + +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} + +1. `field` hâlâ standart `dataclasses` içinden import edilir. + +2. `pydantic.dataclasses`, `dataclasses` için bir drop-in replacement'tır. + +3. `Author` dataclass'ı, `Item` dataclass'larından oluşan bir liste içerir. + +4. `Author` dataclass'ı, `response_model` parametresi olarak kullanılır. + +5. Request body olarak dataclass'larla birlikte diğer standart type annotation'ları da kullanabilirsiniz. + + Bu örnekte, `Item` dataclass'larından oluşan bir listedir. + +6. Burada `items` içeren bir dictionary döndürüyoruz; `items` bir dataclass listesi. + + FastAPI, veriyi JSON'a serializing etmeyi yine başarır. + +7. Burada `response_model`, `Author` dataclass'larından oluşan bir listenin type annotation'ını kullanıyor. + + Yine `dataclasses` ile standart type annotation'ları birleştirebilirsiniz. + +8. Bu *path operation function*, `async def` yerine normal `def` kullanıyor. + + Her zaman olduğu gibi, FastAPI'de ihtiyaca göre `def` ve `async def`’i birlikte kullanabilirsiniz. + + Hangisini ne zaman kullanmanız gerektiğine dair hızlı bir hatırlatma isterseniz, [`async` ve `await`](../async.md#in-a-hurry){.internal-link target=_blank} dokümanındaki _"In a hurry?"_ bölümüne bakın. + +9. Bu *path operation function* dataclass döndürmüyor (isterse döndürebilir), onun yerine dahili verilerle bir dictionary listesi döndürüyor. + + FastAPI, response'u dönüştürmek için (dataclass'ları içeren) `response_model` parametresini kullanacaktır. + +Karmaşık veri yapıları oluşturmak için `dataclasses` ile diğer type annotation'ları pek çok farklı kombinasyonda birleştirebilirsiniz. + +Daha spesifik ayrıntılar için yukarıdaki kod içi annotation ipuçlarına bakın. + +## Daha Fazla Öğrenin { #learn-more } + +`dataclasses`'ı diğer Pydantic model'leriyle de birleştirebilir, onlardan kalıtım alabilir, kendi model'lerinize dahil edebilirsiniz, vb. + +Daha fazlası için Pydantic'in dataclasses dokümantasyonuna bakın. + +## Sürüm { #version } + +Bu özellik FastAPI `0.67.0` sürümünden beri mevcuttur. 🔖 diff --git a/docs/tr/docs/advanced/events.md b/docs/tr/docs/advanced/events.md new file mode 100644 index 000000000..257b952f9 --- /dev/null +++ b/docs/tr/docs/advanced/events.md @@ -0,0 +1,165 @@ +# Lifespan Olayları { #lifespan-events } + +Uygulama **başlamadan** önce çalıştırılması gereken mantığı (kodu) tanımlayabilirsiniz. Bu, bu kodun **bir kez**, uygulama **request almaya başlamadan önce** çalıştırılacağı anlamına gelir. + +Benzer şekilde, uygulama **kapanırken** çalıştırılması gereken mantığı (kodu) da tanımlayabilirsiniz. Bu durumda bu kod, muhtemelen **çok sayıda request** işlendi **sonra**, **bir kez** çalıştırılır. + +Bu kod, uygulama request almaya **başlamadan** önce ve request’leri işlemeyi **bitirdikten** hemen sonra çalıştığı için, uygulamanın tüm **lifespan**’ını (birazdan "lifespan" kelimesi önemli olacak 😉) kapsar. + +Bu yaklaşım, tüm uygulama boyunca kullanacağınız ve request’ler arasında **paylaşılan** **resource**’ları kurmak ve/veya sonrasında bunları **temizlemek** için çok faydalıdır. Örneğin bir veritabanı connection pool’u ya da paylaşılan bir machine learning modelini yüklemek gibi. + +## Kullanım Senaryosu { #use-case } + +Önce bir **kullanım senaryosu** örneğiyle başlayalım, sonra bunu bununla nasıl çözeceğimize bakalım. + +Request’leri işlemek için kullanmak istediğiniz bazı **machine learning modelleriniz** olduğunu hayal edelim. 🤖 + +Aynı modeller request’ler arasında paylaşılır; yani request başına bir model, kullanıcı başına bir model vb. gibi değil. + +Modeli yüklemenin, diskten çok fazla **data** okunması gerektiği için **oldukça uzun sürebildiğini** düşünelim. Dolayısıyla bunu her request için yapmak istemezsiniz. + +Modeli modülün/dosyanın en üst seviyesinde yükleyebilirdiniz; ancak bu, basit bir otomatik test çalıştırdığınızda bile **modelin yükleneceği** anlamına gelir. Böyle olunca test, kodun bağımsız bir kısmını çalıştırabilmek için önce modelin yüklenmesini beklemek zorunda kalır ve **yavaş** olur. + +Burada çözeceğimiz şey bu: modeli request’ler işlenmeden önce yükleyelim, ama kod yüklenirken değil; yalnızca uygulama request almaya başlamadan hemen önce. + +## Lifespan { #lifespan } + +Bu *startup* ve *shutdown* mantığını, `FastAPI` uygulamasının `lifespan` parametresi ve bir "context manager" kullanarak tanımlayabilirsiniz (bunun ne olduğunu birazdan göstereceğim). + +Önce bir örnekle başlayıp sonra ayrıntılarına bakalım. + +Aşağıdaki gibi `yield` kullanan async bir `lifespan()` fonksiyonu oluşturuyoruz: + +{* ../../docs_src/events/tutorial003_py39.py hl[16,19] *} + +Burada, `yield` öncesinde (sahte) model fonksiyonunu machine learning modellerini içeren dictionary’e koyarak, modeli yükleme gibi maliyetli bir *startup* işlemini simüle ediyoruz. Bu kod, *startup* sırasında, uygulama **request almaya başlamadan önce** çalıştırılır. + +Ardından `yield`’den hemen sonra modeli bellekten kaldırıyoruz (unload). Bu kod, uygulama **request’leri işlemeyi bitirdikten sonra**, *shutdown*’dan hemen önce çalıştırılır. Örneğin memory veya GPU gibi resource’ları serbest bırakabilir. + +/// tip | İpucu + +`shutdown`, uygulamayı **durdurduğunuzda** gerçekleşir. + +Belki yeni bir sürüm başlatmanız gerekiyordur, ya da çalıştırmaktan sıkılmışsınızdır. 🤷 + +/// + +### Lifespan fonksiyonu { #lifespan-function } + +Dikkat edilmesi gereken ilk şey, `yield` içeren async bir fonksiyon tanımlıyor olmamız. Bu, `yield` kullanan Dependencies’e oldukça benzer. + +{* ../../docs_src/events/tutorial003_py39.py hl[14:19] *} + +Fonksiyonun `yield`’den önceki kısmı, uygulama başlamadan **önce** çalışır. + +`yield`’den sonraki kısım ise, uygulama işini bitirdikten **sonra** çalışır. + +### Async Context Manager { #async-context-manager } + +Bakarsanız, fonksiyon `@asynccontextmanager` ile dekore edilmiş. + +Bu da fonksiyonu "**async context manager**" denen şeye dönüştürür. + +{* ../../docs_src/events/tutorial003_py39.py hl[1,13] *} + +Python’da **context manager**, `with` ifadesi içinde kullanabildiğiniz bir yapıdır. Örneğin `open()` bir context manager olarak kullanılabilir: + +```Python +with open("file.txt") as file: + file.read() +``` + +Python’ın güncel sürümlerinde bir de **async context manager** vardır. Bunu `async with` ile kullanırsınız: + +```Python +async with lifespan(app): + await do_stuff() +``` + +Yukarıdaki gibi bir context manager veya async context manager oluşturduğunuzda, yaptığı şey şudur: `with` bloğuna girmeden önce `yield`’den önceki kodu çalıştırır, `with` bloğundan çıktıktan sonra da `yield`’den sonraki kodu çalıştırır. + +Yukarıdaki kod örneğimizde bunu doğrudan kullanmıyoruz; bunun yerine FastAPI’ye veriyoruz ki o kullansın. + +`FastAPI` uygulamasının `lifespan` parametresi bir **async context manager** alır; dolayısıyla oluşturduğumuz yeni `lifespan` async context manager’ını buraya geçebiliriz. + +{* ../../docs_src/events/tutorial003_py39.py hl[22] *} + +## Alternatif Events (kullanımdan kaldırıldı) { #alternative-events-deprecated } + +/// warning | Uyarı + +*startup* ve *shutdown* işlemlerini yönetmenin önerilen yolu, yukarıda anlatıldığı gibi `FastAPI` uygulamasının `lifespan` parametresini kullanmaktır. Bir `lifespan` parametresi sağlarsanız, `startup` ve `shutdown` event handler’ları artık çağrılmaz. Ya tamamen `lifespan` ya da tamamen events; ikisi birden değil. + +Muhtemelen bu bölümü atlayabilirsiniz. + +/// + +*startup* ve *shutdown* sırasında çalıştırılacak bu mantığı tanımlamanın alternatif bir yolu daha vardır. + +Uygulama başlamadan önce veya uygulama kapanırken çalıştırılması gereken event handler’ları (fonksiyonları) tanımlayabilirsiniz. + +Bu fonksiyonlar `async def` ile veya normal `def` ile tanımlanabilir. + +### `startup` eventi { #startup-event } + +Uygulama başlamadan önce çalıştırılacak bir fonksiyon eklemek için, `"startup"` event’i ile tanımlayın: + +{* ../../docs_src/events/tutorial001_py39.py hl[8] *} + +Bu durumda `startup` event handler fonksiyonu, "database" öğesini (sadece bir `dict`) bazı değerlerle başlatır. + +Birden fazla event handler fonksiyonu ekleyebilirsiniz. + +Ve tüm `startup` event handler’ları tamamlanmadan uygulamanız request almaya başlamaz. + +### `shutdown` eventi { #shutdown-event } + +Uygulama kapanırken çalıştırılacak bir fonksiyon eklemek için, `"shutdown"` event’i ile tanımlayın: + +{* ../../docs_src/events/tutorial002_py39.py hl[6] *} + +Burada `shutdown` event handler fonksiyonu, `log.txt` dosyasına `"Application shutdown"` satırını yazar. + +/// info | Bilgi + +`open()` fonksiyonunda `mode="a"` "append" anlamına gelir; yani satır, önceki içeriği silmeden dosyada ne varsa onun sonuna eklenir. + +/// + +/// tip | İpucu + +Dikkat edin, bu örnekte bir dosyayla etkileşen standart Python `open()` fonksiyonunu kullanıyoruz. + +Dolayısıyla disk’e yazılmasını beklemeyi gerektiren I/O (input/output) söz konusu. + +Ancak `open()` `async` ve `await` kullanmaz. + +Bu yüzden event handler fonksiyonunu `async def` yerine standart `def` ile tanımlarız. + +/// + +### `startup` ve `shutdown` birlikte { #startup-and-shutdown-together } + +*startup* ve *shutdown* mantığınızın birbiriyle bağlantılı olma ihtimali yüksektir; bir şeyi başlatıp sonra bitirmek, bir resource edinip sonra serbest bırakmak vb. isteyebilirsiniz. + +Bunu, ortak mantık veya değişken paylaşmayan ayrı fonksiyonlarda yapmak daha zordur; çünkü değerleri global değişkenlerde tutmanız veya benzer numaralar yapmanız gerekir. + +Bu nedenle artık bunun yerine, yukarıda açıklandığı gibi `lifespan` kullanmanız önerilmektedir. + +## Teknik Detaylar { #technical-details } + +Meraklı nerd’ler için küçük bir teknik detay. 🤓 + +Altta, ASGI teknik spesifikasyonunda bu, Lifespan Protocol’ün bir parçasıdır ve `startup` ile `shutdown` adında event’ler tanımlar. + +/// info | Bilgi + +Starlette `lifespan` handler’ları hakkında daha fazlasını Starlette's Lifespan docs içinde okuyabilirsiniz. + +Ayrıca kodunuzun başka bölgelerinde de kullanılabilecek lifespan state’i nasıl yöneteceğinizi de kapsar. + +/// + +## Alt Uygulamalar { #sub-applications } + +🚨 Unutmayın: Bu lifespan event’leri (`startup` ve `shutdown`) yalnızca ana uygulama için çalıştırılır; [Alt Uygulamalar - Mounts](sub-applications.md){.internal-link target=_blank} için çalıştırılmaz. diff --git a/docs/tr/docs/advanced/generate-clients.md b/docs/tr/docs/advanced/generate-clients.md new file mode 100644 index 000000000..af278f2fe --- /dev/null +++ b/docs/tr/docs/advanced/generate-clients.md @@ -0,0 +1,208 @@ +# SDK Üretme { #generating-sdks } + +**FastAPI**, **OpenAPI** spesifikasyonunu temel aldığı için API'leri birçok aracın anlayabildiği standart bir formatta tanımlanabilir. + +Bu sayede güncel **dokümantasyon**, birden fazla dilde istemci kütüphaneleri (**SDKs**) ve kodunuzla senkron kalan **test** veya **otomasyon iş akışları** üretmek kolaylaşır. + +Bu rehberde, FastAPI backend'iniz için bir **TypeScript SDK** üretmeyi öğreneceksiniz. + +## Açık Kaynak SDK Üreteçleri { #open-source-sdk-generators } + +Esnek bir seçenek olan OpenAPI Generator, **birçok programlama dilini** destekler ve OpenAPI spesifikasyonunuzdan SDK üretebilir. + +**TypeScript client**'lar için Hey API, TypeScript ekosistemi için özel olarak tasarlanmış, optimize bir deneyim sunan bir çözümdür. + +Daha fazla SDK üretecini OpenAPI.Tools üzerinde keşfedebilirsiniz. + +/// tip | İpucu + +FastAPI otomatik olarak **OpenAPI 3.1** spesifikasyonları üretir; bu yüzden kullanacağınız aracın bu sürümü desteklemesi gerekir. + +/// + +## FastAPI Sponsorlarından SDK Üreteçleri { #sdk-generators-from-fastapi-sponsors } + +Bu bölüm, FastAPI'yi sponsorlayan şirketlerin sunduğu **yatırım destekli** ve **şirket destekli** çözümleri öne çıkarır. Bu ürünler, yüksek kaliteli üretilen SDK'ların üzerine **ek özellikler** ve **entegrasyonlar** sağlar. + +✨ [**FastAPI'ye sponsor olarak**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ bu şirketler, framework'ün ve **ekosisteminin** sağlıklı ve **sürdürülebilir** kalmasına yardımcı olur. + +Sponsor olmaları aynı zamanda FastAPI **topluluğuna** (size) güçlü bir bağlılığı da gösterir; yalnızca **iyi bir hizmet** sunmayı değil, aynı zamanda **güçlü ve gelişen bir framework** olan FastAPI'yi desteklemeyi de önemsediklerini gösterir. 🙇 + +Örneğin şunları deneyebilirsiniz: + +* Speakeasy +* Stainless +* liblab + +Bu çözümlerin bazıları açık kaynak olabilir veya ücretsiz katman sunabilir; yani finansal bir taahhüt olmadan deneyebilirsiniz. Başka ticari SDK üreteçleri de vardır ve internette bulunabilir. 🤓 + +## TypeScript SDK Oluşturma { #create-a-typescript-sdk } + +Basit bir FastAPI uygulamasıyla başlayalım: + +{* ../../docs_src/generate_clients/tutorial001_py39.py hl[7:9,12:13,16:17,21] *} + +*Path operation*'ların, request payload ve response payload için kullandıkları modelleri `Item` ve `ResponseMessage` modelleriyle tanımladıklarına dikkat edin. + +### API Dokümanları { #api-docs } + +`/docs` adresine giderseniz, request'lerde gönderilecek ve response'larda alınacak veriler için **schema**'ları içerdiğini görürsünüz: + + + +Bu schema'ları görebilirsiniz, çünkü uygulamada modellerle birlikte tanımlandılar. + +Bu bilgi uygulamanın **OpenAPI schema**'sında bulunur ve sonrasında API dokümanlarında gösterilir. + +OpenAPI'ye dahil edilen, modellerden gelen bu bilginin aynısı **client code üretmek** için kullanılabilir. + +### Hey API { #hey-api } + +Modelleri olan bir FastAPI uygulamamız olduğunda, Hey API ile bir TypeScript client üretebiliriz. Bunu yapmanın en hızlı yolu npx kullanmaktır. + +```sh +npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client +``` + +Bu komut `./src/client` içine bir TypeScript SDK üretecektir. + +Web sitelerinde `@hey-api/openapi-ts` kurulumunu öğrenebilir ve üretilen çıktıyı inceleyebilirsiniz. + +### SDK'yı Kullanma { #using-the-sdk } + +Artık client code'u import edip kullanabilirsiniz. Şuna benzer görünebilir; method'lar için otomatik tamamlama aldığınıza dikkat edin: + + + +Ayrıca gönderilecek payload için de otomatik tamamlama alırsınız: + + + +/// tip | İpucu + +`name` ve `price` için otomatik tamamlamaya dikkat edin; bunlar FastAPI uygulamasında, `Item` modelinde tanımlanmıştı. + +/// + +Gönderdiğiniz veriler için satır içi hatalar (inline errors) da alırsınız: + + + +Response objesi de otomatik tamamlama sunacaktır: + + + +## Tag'lerle FastAPI Uygulaması { #fastapi-app-with-tags } + +Birçok durumda FastAPI uygulamanız daha büyük olacaktır ve farklı *path operation* gruplarını ayırmak için muhtemelen tag'leri kullanacaksınız. + +Örneğin **items** için bir bölüm, **users** için başka bir bölüm olabilir ve bunları tag'lerle ayırabilirsiniz: + +{* ../../docs_src/generate_clients/tutorial002_py39.py hl[21,26,34] *} + +### Tag'lerle TypeScript Client Üretme { #generate-a-typescript-client-with-tags } + +Tag'leri kullanan bir FastAPI uygulaması için client ürettiğinizde, genelde client code da tag'lere göre ayrılır. + +Bu sayede client code tarafında her şey doğru şekilde sıralanır ve gruplandırılır: + + + +Bu örnekte şunlar var: + +* `ItemsService` +* `UsersService` + +### Client Method İsimleri { #client-method-names } + +Şu an üretilen `createItemItemsPost` gibi method isimleri çok temiz görünmüyor: + +```TypeScript +ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) +``` + +...çünkü client üreteci, her *path operation* için OpenAPI'nin dahili **operation ID** değerini kullanır. + +OpenAPI, her operation ID'nin tüm *path operation*'lar arasında benzersiz olmasını ister. Bu yüzden FastAPI; operation ID'yi benzersiz tutabilmek için **function adı**, **path** ve **HTTP method/operation** bilgilerini birleştirerek üretir. + +Ancak bunu bir sonraki adımda nasıl iyileştirebileceğinizi göstereceğim. 🤓 + +## Özel Operation ID'ler ve Daha İyi Method İsimleri { #custom-operation-ids-and-better-method-names } + +Bu operation ID'lerin **üretilme** şeklini **değiştirerek**, client'larda daha basit **method isimleri** elde edebilirsiniz. + +Bu durumda, her operation ID'nin **benzersiz** olduğundan başka bir şekilde emin olmanız gerekir. + +Örneğin, her *path operation*'ın bir tag'i olmasını sağlayabilir ve operation ID'yi **tag** ve *path operation* **adı**na (function adı) göre üretebilirsiniz. + +### Benzersiz ID Üreten Özel Fonksiyon { #custom-generate-unique-id-function } + +FastAPI, her *path operation* için bir **unique ID** kullanır. Bu ID, **operation ID** için ve ayrıca request/response'lar için gerekebilecek özel model isimleri için de kullanılır. + +Bu fonksiyonu özelleştirebilirsiniz. Bir `APIRoute` alır ve string döndürür. + +Örneğin burada ilk tag'i (muhtemelen tek tag'iniz olur) ve *path operation* adını (function adı) kullanıyor. + +Sonrasında bu özel fonksiyonu `generate_unique_id_function` parametresiyle **FastAPI**'ye geçebilirsiniz: + +{* ../../docs_src/generate_clients/tutorial003_py39.py hl[6:7,10] *} + +### Özel Operation ID'lerle TypeScript Client Üretme { #generate-a-typescript-client-with-custom-operation-ids } + +Artık client'ı tekrar üretirseniz, geliştirilmiş method isimlerini göreceksiniz: + + + +Gördüğünüz gibi method isimleri artık önce tag'i, sonra function adını içeriyor; URL path'i ve HTTP operation bilgisini artık taşımıyor. + +### Client Üretecine Vermeden Önce OpenAPI Spesifikasyonunu Ön İşlemek { #preprocess-the-openapi-specification-for-the-client-generator } + +Üretilen kodda hâlâ bazı **tekrarlanan bilgiler** var. + +Bu method'un **items** ile ilişkili olduğunu zaten biliyoruz; çünkü bu kelime `ItemsService` içinde var (tag'den geliyor). Ama method adında da tag adı önek olarak duruyor. 😕 + +OpenAPI genelinde muhtemelen bunu korumak isteriz; çünkü operation ID'lerin **benzersiz** olmasını sağlar. + +Ancak üretilen client için, client'ları üretmeden hemen önce OpenAPI operation ID'lerini **değiştirip**, method isimlerini daha hoş ve **temiz** hale getirebiliriz. + +OpenAPI JSON'u `openapi.json` diye bir dosyaya indirip, şu tarz bir script ile **öndeki tag'i kaldırabiliriz**: + +{* ../../docs_src/generate_clients/tutorial004_py39.py *} + +//// tab | Node.js + +```Javascript +{!> ../../docs_src/generate_clients/tutorial004.js!} +``` + +//// + +Bununla operation ID'ler `items-get_items` gibi değerlerden sadece `get_items` olacak şekilde yeniden adlandırılır; böylece client üreteci daha basit method isimleri üretebilir. + +### Ön İşlenmiş OpenAPI ile TypeScript Client Üretme { #generate-a-typescript-client-with-the-preprocessed-openapi } + +Sonuç artık bir `openapi.json` dosyasında olduğuna göre, input konumunu güncellemeniz gerekir: + +```sh +npx @hey-api/openapi-ts -i ./openapi.json -o src/client +``` + +Yeni client'ı ürettikten sonra, tüm **otomatik tamamlama**, **satır içi hatalar**, vb. ile birlikte **temiz method isimleri** elde edersiniz: + + + +## Faydalar { #benefits } + +Otomatik üretilen client'ları kullanınca şu alanlarda **otomatik tamamlama** elde edersiniz: + +* Method'lar. +* Body'deki request payload'ları, query parametreleri, vb. +* Response payload'ları. + +Ayrıca her şey için **satır içi hatalar** (inline errors) da olur. + +Backend kodunu her güncellediğinizde ve frontend'i **yeniden ürettiğinizde**, yeni *path operation*'lar method olarak eklenir, eskileri kaldırılır ve diğer değişiklikler de üretilen koda yansır. 🤓 + +Bu, bir şey değiştiğinde client code'a otomatik olarak **yansıyacağı** anlamına gelir. Ayrıca client'ı **build** ettiğinizde, kullanılan verilerde bir **uyuşmazlık** (mismatch) varsa hata alırsınız. + +Böylece üretimde son kullanıcılara hata yansımasını beklemek ve sonra sorunun nerede olduğunu debug etmeye çalışmak yerine, geliştirme sürecinin çok erken aşamalarında **birçok hatayı tespit edersiniz**. ✨ diff --git a/docs/tr/docs/advanced/middleware.md b/docs/tr/docs/advanced/middleware.md new file mode 100644 index 000000000..a22644a09 --- /dev/null +++ b/docs/tr/docs/advanced/middleware.md @@ -0,0 +1,97 @@ +# İleri Seviye Middleware { #advanced-middleware } + +Ana tutorial'da uygulamanıza [Özel Middleware](../tutorial/middleware.md){.internal-link target=_blank} eklemeyi gördünüz. + +Ardından [`CORSMiddleware` ile CORS'u yönetmeyi](../tutorial/cors.md){.internal-link target=_blank} de okudunuz. + +Bu bölümde diğer middleware'leri nasıl kullanacağımıza bakacağız. + +## ASGI middleware'leri ekleme { #adding-asgi-middlewares } + +**FastAPI**, Starlette üzerine kurulu olduğu ve ASGI spesifikasyonunu uyguladığı için, herhangi bir ASGI middleware'ini kullanabilirsiniz. + +Bir middleware'in çalışması için özellikle FastAPI ya da Starlette için yazılmış olması gerekmez; ASGI spec'ine uyduğu sürece yeterlidir. + +Genel olarak ASGI middleware'leri, ilk argüman olarak bir ASGI app almayı bekleyen class'lar olur. + +Dolayısıyla üçüncü taraf ASGI middleware'lerinin dokümantasyonunda muhtemelen şöyle bir şey yapmanızı söylerler: + +```Python +from unicorn import UnicornMiddleware + +app = SomeASGIApp() + +new_app = UnicornMiddleware(app, some_config="rainbow") +``` + +Ancak FastAPI (aslında Starlette) bunu yapmanın daha basit bir yolunu sunar; böylece dahili middleware'ler server hatalarını doğru şekilde ele alır ve özel exception handler'lar düzgün çalışır. + +Bunun için `app.add_middleware()` kullanırsınız (CORS örneğindeki gibi). + +```Python +from fastapi import FastAPI +from unicorn import UnicornMiddleware + +app = FastAPI() + +app.add_middleware(UnicornMiddleware, some_config="rainbow") +``` + +`app.add_middleware()` ilk argüman olarak bir middleware class'ı alır ve middleware'e aktarılacak ek argümanları da kabul eder. + +## Entegre middleware'ler { #integrated-middlewares } + +**FastAPI**, yaygın kullanım senaryoları için birkaç middleware içerir; şimdi bunları nasıl kullanacağımıza bakacağız. + +/// note | Teknik Detaylar + +Bir sonraki örneklerde `from starlette.middleware.something import SomethingMiddleware` kullanmanız da mümkündür. + +**FastAPI**, size (geliştirici olarak) kolaylık olsun diye `fastapi.middleware` içinde bazı middleware'leri sağlar. Ancak mevcut middleware'lerin çoğu doğrudan Starlette'ten gelir. + +/// + +## `HTTPSRedirectMiddleware` { #httpsredirectmiddleware } + +Gelen tüm request'lerin `https` veya `wss` olmasını zorunlu kılar. + +`http` veya `ws` olarak gelen herhangi bir request, bunun yerine güvenli şemaya redirect edilir. + +{* ../../docs_src/advanced_middleware/tutorial001_py39.py hl[2,6] *} + +## `TrustedHostMiddleware` { #trustedhostmiddleware } + +HTTP Host Header saldırılarına karşı korunmak için, gelen tüm request'lerde `Host` header'ının doğru ayarlanmış olmasını zorunlu kılar. + +{* ../../docs_src/advanced_middleware/tutorial002_py39.py hl[2,6:8] *} + +Aşağıdaki argümanlar desteklenir: + +* `allowed_hosts` - Hostname olarak izin verilmesi gereken domain adlarının listesi. `*.example.com` gibi wildcard domain'ler subdomain eşleştirmesi için desteklenir. Herhangi bir hostname'e izin vermek için `allowed_hosts=["*"]` kullanın veya middleware'i hiç eklemeyin. +* `www_redirect` - True olarak ayarlanırsa, izin verilen host'ların www olmayan sürümlerine gelen request'ler www sürümlerine redirect edilir. Varsayılanı `True`'dur. + +Gelen bir request doğru şekilde doğrulanmazsa `400` response gönderilir. + +## `GZipMiddleware` { #gzipmiddleware } + +`Accept-Encoding` header'ında `"gzip"` içeren herhangi bir request için GZip response'larını yönetir. + +Middleware hem standart hem de streaming response'ları ele alır. + +{* ../../docs_src/advanced_middleware/tutorial003_py39.py hl[2,6] *} + +Aşağıdaki argümanlar desteklenir: + +* `minimum_size` - Bayt cinsinden bu minimum boyuttan küçük response'lara GZip uygulama. Varsayılanı `500`'dür. +* `compresslevel` - GZip sıkıştırması sırasında kullanılır. 1 ile 9 arasında bir tamsayıdır. Varsayılanı `9`'dur. Daha düşük değer daha hızlı sıkıştırma ama daha büyük dosya boyutları üretir; daha yüksek değer daha yavaş sıkıştırma ama daha küçük dosya boyutları üretir. + +## Diğer middleware'ler { #other-middlewares } + +Başka birçok ASGI middleware'i vardır. + +Örneğin: + +* Uvicorn'un `ProxyHeadersMiddleware`'i +* MessagePack + +Diğer mevcut middleware'leri görmek için Starlette'in Middleware dokümanlarına ve ASGI Awesome List listesine bakın. diff --git a/docs/tr/docs/advanced/openapi-callbacks.md b/docs/tr/docs/advanced/openapi-callbacks.md new file mode 100644 index 000000000..61135b7e0 --- /dev/null +++ b/docs/tr/docs/advanced/openapi-callbacks.md @@ -0,0 +1,186 @@ +# OpenAPI Callback'leri { #openapi-callbacks } + +Başka biri tarafından (muhtemelen API'nizi *kullanacak* olan aynı geliştirici tarafından) oluşturulmuş bir *external API*'ye request tetikleyebilen bir *path operation* ile bir API oluşturabilirsiniz. + +API uygulamanızın *external API*'yi çağırdığı sırada gerçekleşen sürece "callback" denir. Çünkü dış geliştiricinin yazdığı yazılım API'nize bir request gönderir ve ardından API'niz *geri çağrı* yaparak (*call back*), bir *external API*'ye request gönderir (muhtemelen aynı geliştiricinin oluşturduğu). + +Bu durumda, o external API'nin nasıl görünmesi *gerektiğini* dokümante etmek isteyebilirsiniz. Hangi *path operation*'a sahip olmalı, hangi body'yi beklemeli, hangi response'u döndürmeli, vb. + +## Callback'leri olan bir uygulama { #an-app-with-callbacks } + +Bunların hepsine bir örnekle bakalım. + +Fatura oluşturmayı sağlayan bir uygulama geliştirdiğinizi düşünün. + +Bu faturaların `id`, `title` (opsiyonel), `customer` ve `total` alanları olacak. + +API'nizin kullanıcısı (external bir geliştirici) API'nizde bir POST request ile fatura oluşturacak. + +Sonra API'niz (varsayalım ki): + +* Faturayı external geliştiricinin bir müşterisine gönderir. +* Parayı tahsil eder. +* API kullanıcısına (external geliştiriciye) tekrar bir bildirim gönderir. + * Bu, external geliştiricinin sağladığı bir *external API*'ye (*sizin API'nizden*) bir POST request gönderilerek yapılır (işte bu "callback"tir). + +## Normal **FastAPI** uygulaması { #the-normal-fastapi-app } + +Önce callback eklemeden önce normal API uygulamasının nasıl görüneceğine bakalım. + +Bir `Invoice` body alacak bir *path operation*'ı ve callback için URL'yi taşıyacak `callback_url` adlı bir query parametresi olacak. + +Bu kısım oldukça standart; kodun çoğu muhtemelen size zaten tanıdık gelecektir: + +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} + +/// tip | İpucu + +`callback_url` query parametresi, Pydantic'in Url tipini kullanır. + +/// + +Tek yeni şey, *path operation decorator*'ına argüman olarak verilen `callbacks=invoices_callback_router.routes`. Bunun ne olduğuna şimdi bakacağız. + +## Callback'i dokümante etmek { #documenting-the-callback } + +Callback'in gerçek kodu, büyük ölçüde sizin API uygulamanıza bağlıdır. + +Ve bir uygulamadan diğerine oldukça değişebilir. + +Sadece bir-iki satır kod bile olabilir, örneğin: + +```Python +callback_url = "https://example.com/api/v1/invoices/events/" +httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) +``` + +Ancak callback'in belki de en önemli kısmı, API'nizin kullanıcısının (external geliştiricinin) *external API*'yi doğru şekilde uyguladığından emin olmaktır; çünkü *sizin API'niz* callback'in request body'sinde belirli veriler gönderecektir, vb. + +Dolayısıyla sıradaki adım olarak, *sizin API'nizden* callback almak için o *external API*'nin nasıl görünmesi gerektiğini dokümante eden kodu ekleyeceğiz. + +Bu dokümantasyon, API'nizde `/docs` altındaki Swagger UI'da görünecek ve external geliştiricilere *external API*'yi nasıl inşa edeceklerini gösterecek. + +Bu örnek callback'in kendisini implemente etmiyor (o zaten tek satır kod olabilir), sadece dokümantasyon kısmını ekliyor. + +/// tip | İpucu + +Gerçek callback, sadece bir HTTP request'tir. + +Callback'i kendiniz implemente ederken HTTPX veya Requests gibi bir şey kullanabilirsiniz. + +/// + +## Callback dokümantasyon kodunu yazın { #write-the-callback-documentation-code } + +Bu kod uygulamanızda çalıştırılmayacak; sadece o *external API*'nin nasıl görünmesi gerektiğini *dokümante etmek* için gerekiyor. + +Ancak **FastAPI** ile bir API için otomatik dokümantasyonu kolayca nasıl üreteceğinizi zaten biliyorsunuz. + +O halde aynı bilgiyi kullanarak, *external API*'nin nasıl görünmesi gerektiğini dokümante edeceğiz... external API'nin implemente etmesi gereken *path operation*'ları oluşturarak (API'nizin çağıracağı olanlar). + +/// tip | İpucu + +Bir callback'i dokümante eden kodu yazarken, kendinizi *external geliştirici* olarak hayal etmek faydalı olabilir. Ve şu anda *sizin API'nizi* değil, *external API*'yi implemente ettiğinizi düşünün. + +Bu bakış açısını (external geliştiricinin bakış açısını) geçici olarak benimsemek; parametreleri nereye koyacağınızı, body için Pydantic modelini, response için modelini vb. external API tarafında nasıl tasarlayacağınızı daha net hale getirebilir. + +/// + +### Bir callback `APIRouter` oluşturun { #create-a-callback-apirouter } + +Önce bir veya daha fazla callback içerecek yeni bir `APIRouter` oluşturun. + +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *} + +### Callback *path operation*'ını oluşturun { #create-the-callback-path-operation } + +Callback *path operation*'ını oluşturmak için, yukarıda oluşturduğunuz aynı `APIRouter`'ı kullanın. + +Normal bir FastAPI *path operation*'ı gibi görünmelidir: + +* Muhtemelen alması gereken body'nin bir deklarasyonu olmalı, örn. `body: InvoiceEvent`. +* Ayrıca döndürmesi gereken response'un deklarasyonu da olabilir, örn. `response_model=InvoiceEventReceived`. + +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *} + +Normal bir *path operation*'dan 2 temel farkı vardır: + +* Gerçek bir koda ihtiyaç duymaz; çünkü uygulamanız bu kodu asla çağırmayacak. Bu yalnızca *external API*'yi dokümante etmek için kullanılır. Yani fonksiyon sadece `pass` içerebilir. +* *path*, bir OpenAPI 3 expression (aşağıda daha fazlası) içerebilir; böylece parametreler ve *sizin API'nize* gönderilen orijinal request'in bazı parçalarıyla değişkenler kullanılabilir. + +### Callback path ifadesi { #the-callback-path-expression } + +Callback *path*'i, *sizin API'nize* gönderilen orijinal request'in bazı parçalarını içerebilen bir OpenAPI 3 expression barındırabilir. + +Bu örnekte, bu bir `str`: + +```Python +"{$callback_url}/invoices/{$request.body.id}" +``` + +Yani API'nizin kullanıcısı (external geliştirici) *sizin API'nize* şu adrese bir request gönderirse: + +``` +https://yourapi.com/invoices/?callback_url=https://www.external.org/events +``` + +ve JSON body şu şekilde olursa: + +```JSON +{ + "id": "2expen51ve", + "customer": "Mr. Richie Rich", + "total": "9999" +} +``` + +o zaman *sizin API'niz* faturayı işleyecek ve daha sonra bir noktada `callback_url`'ye (yani *external API*'ye) bir callback request gönderecek: + +``` +https://www.external.org/events/invoices/2expen51ve +``` + +ve JSON body yaklaşık şöyle bir şey içerecek: + +```JSON +{ + "description": "Payment celebration", + "paid": true +} +``` + +ve o *external API*'den şu gibi bir JSON body içeren response bekleyecek: + +```JSON +{ + "ok": true +} +``` + +/// tip | İpucu + +Callback URL'sinin, `callback_url` içindeki query parametresi olarak alınan URL'yi (`https://www.external.org/events`) ve ayrıca JSON body'nin içindeki fatura `id`'sini (`2expen51ve`) birlikte kullandığına dikkat edin. + +/// + +### Callback router'ını ekleyin { #add-the-callback-router } + +Bu noktada, yukarıda oluşturduğunuz callback router'ında gerekli callback *path operation*'ları (external geliştiricinin *external API*'de implemente etmesi gerekenler) hazır. + +Şimdi *sizin API'nizin path operation decorator*'ında `callbacks` parametresini kullanarak, callback router'ının `.routes` attribute'unu (bu aslında route/*path operation*'lardan oluşan bir `list`) geçin: + +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} + +/// tip | İpucu + +`callback=` içine router'ın kendisini (`invoices_callback_router`) değil, `invoices_callback_router.routes` şeklinde `.routes` attribute'unu verdiğinize dikkat edin. + +/// + +### Dokümanları kontrol edin { #check-the-docs } + +Artık uygulamanızı başlatıp http://127.0.0.1:8000/docs adresine gidebilirsiniz. + +*Path operation*'ınız için, *external API*'nin nasıl görünmesi gerektiğini gösteren bir "Callbacks" bölümünü içeren dokümanları göreceksiniz: + + diff --git a/docs/tr/docs/advanced/openapi-webhooks.md b/docs/tr/docs/advanced/openapi-webhooks.md new file mode 100644 index 000000000..dd9e9bbe7 --- /dev/null +++ b/docs/tr/docs/advanced/openapi-webhooks.md @@ -0,0 +1,55 @@ +# OpenAPI Webhook'lar { #openapi-webhooks } + +Bazı durumlarda, API'nizi kullanan **kullanıcılara** uygulamanızın *onların* uygulamasını (request göndererek) bazı verilerle çağırabileceğini; genellikle bir tür **event** hakkında **bildirim** yapmak için kullanacağını söylemek istersiniz. + +Bu da şunu ifade eder: Kullanıcılarınızın API'nize request göndermesi şeklindeki normal akış yerine, request'i **sizin API'niz** (veya uygulamanız) **onların sistemine** (onların API'sine, onların uygulamasına) **gönderebilir**. + +Buna genellikle **webhook** denir. + +## Webhook adımları { #webhooks-steps } + +Süreç genellikle şöyledir: Kodunuzda göndereceğiniz mesajın ne olduğunu, yani request'in **body**'sini **siz tanımlarsınız**. + +Ayrıca uygulamanızın bu request'leri veya event'leri hangi **anlarda** göndereceğini de bir şekilde tanımlarsınız. + +Ve **kullanıcılarınız** da bir şekilde (örneğin bir web dashboard üzerinden) uygulamanızın bu request'leri göndermesi gereken **URL**'yi tanımlar. + +Webhook'lar için URL'lerin nasıl kaydedileceğine dair tüm **mantık** ve bu request'leri gerçekten gönderen kod tamamen size bağlıdır. Bunu **kendi kodunuzda** istediğiniz gibi yazarsınız. + +## **FastAPI** ve OpenAPI ile webhook'ları dokümante etmek { #documenting-webhooks-with-fastapi-and-openapi } + +**FastAPI** ile OpenAPI kullanarak bu webhook'ların adlarını, uygulamanızın gönderebileceği HTTP operation türlerini (örn. `POST`, `PUT`, vb.) ve uygulamanızın göndereceği request **body**'lerini tanımlayabilirsiniz. + +Bu, kullanıcılarınızın **webhook** request'lerinizi alacak şekilde **API'lerini implement etmesini** çok daha kolaylaştırabilir; hatta kendi API kodlarının bir kısmını otomatik üretebilirler. + +/// info | Bilgi + +Webhook'lar OpenAPI 3.1.0 ve üzeri sürümlerde mevcuttur; FastAPI `0.99.0` ve üzeri tarafından desteklenir. + +/// + +## Webhook'ları olan bir uygulama { #an-app-with-webhooks } + +Bir **FastAPI** uygulaması oluşturduğunuzda, *webhook*'ları tanımlamak için kullanabileceğiniz bir `webhooks` attribute'u vardır; *path operation* tanımlar gibi, örneğin `@app.webhooks.post()` ile. + +{* ../../docs_src/openapi_webhooks/tutorial001_py39.py hl[9:13,36:53] *} + +Tanımladığınız webhook'lar **OpenAPI** şemasında ve otomatik **docs UI**'da yer alır. + +/// info | Bilgi + +`app.webhooks` nesnesi aslında sadece bir `APIRouter`'dır; uygulamanızı birden fazla dosya ile yapılandırırken kullanacağınız türün aynısıdır. + +/// + +Dikkat edin: Webhook'larda aslında bir *path* (ör. `/items/`) deklare etmiyorsunuz; oraya verdiğiniz metin sadece webhook'un bir **identifier**'ıdır (event'in adı). Örneğin `@app.webhooks.post("new-subscription")` içinde webhook adı `new-subscription`'dır. + +Bunun nedeni, webhook request'ini almak istedikleri gerçek **URL path**'i **kullanıcılarınızın** başka bir şekilde (örn. bir web dashboard üzerinden) tanımlamasının beklenmesidir. + +### Dokümanları kontrol edin { #check-the-docs } + +Şimdi uygulamanızı başlatıp http://127.0.0.1:8000/docs adresine gidin. + +Dokümanlarınızda normal *path operation*'ları ve artık bazı **webhook**'ları da göreceksiniz: + + diff --git a/docs/tr/docs/advanced/path-operation-advanced-configuration.md b/docs/tr/docs/advanced/path-operation-advanced-configuration.md new file mode 100644 index 000000000..e326842d6 --- /dev/null +++ b/docs/tr/docs/advanced/path-operation-advanced-configuration.md @@ -0,0 +1,172 @@ +# Path Operation İleri Düzey Yapılandırma { #path-operation-advanced-configuration } + +## OpenAPI operationId { #openapi-operationid } + +/// warning | Uyarı + +OpenAPI konusunda "uzman" değilseniz, muhtemelen buna ihtiyacınız yok. + +/// + +*path operation*’ınızda kullanılacak OpenAPI `operationId` değerini `operation_id` parametresiyle ayarlayabilirsiniz. + +Bunun her operation için benzersiz olduğundan emin olmanız gerekir. + +{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *} + +### operationId olarak *path operation function* adını kullanma { #using-the-path-operation-function-name-as-the-operationid } + +API’lerinizin function adlarını `operationId` olarak kullanmak istiyorsanız, hepsini dolaşıp her *path operation*’ın `operation_id` değerini `APIRoute.name` ile override edebilirsiniz. + +Bunu, tüm *path operation*’ları ekledikten sonra yapmalısınız. + +{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *} + +/// tip | İpucu + +`app.openapi()` fonksiyonunu manuel olarak çağırıyorsanız, bunu yapmadan önce `operationId`’leri güncellemelisiniz. + +/// + +/// warning | Uyarı + +Bunu yaparsanız, her bir *path operation function*’ın adının benzersiz olduğundan emin olmanız gerekir. + +Farklı modüllerde (Python dosyalarında) olsalar bile. + +/// + +## OpenAPI’den Hariç Tutma { #exclude-from-openapi } + +Bir *path operation*’ı üretilen OpenAPI şemasından (dolayısıyla otomatik dokümantasyon sistemlerinden) hariç tutmak için `include_in_schema` parametresini kullanın ve `False` yapın: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *} + +## Docstring’den İleri Düzey Açıklama { #advanced-description-from-docstring } + +OpenAPI için, bir *path operation function*’ın docstring’inden kullanılacak satırları sınırlandırabilirsiniz. + +Bir `\f` (escape edilmiş "form feed" karakteri) eklerseniz, **FastAPI** OpenAPI için kullanılan çıktıyı bu noktada **keser**. + +Dokümantasyonda görünmez, ancak diğer araçlar (Sphinx gibi) geri kalan kısmı kullanabilir. + +{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} + +## Ek Responses { #additional-responses } + +Muhtemelen bir *path operation* için `response_model` ve `status_code` tanımlamayı görmüşsünüzdür. + +Bu, bir *path operation*’ın ana response’u ile ilgili metadata’yı tanımlar. + +Ek response’ları; modelleri, status code’ları vb. ile birlikte ayrıca da tanımlayabilirsiniz. + +Dokümantasyonda bununla ilgili ayrı bir bölüm var; [OpenAPI’de Ek Responses](additional-responses.md){.internal-link target=_blank} sayfasından okuyabilirsiniz. + +## OpenAPI Extra { #openapi-extra } + +Uygulamanızda bir *path operation* tanımladığınızda, **FastAPI** OpenAPI şemasına dahil edilmek üzere o *path operation* ile ilgili metadata’yı otomatik olarak üretir. + +/// note | Teknik Detaylar + +OpenAPI spesifikasyonunda buna Operation Object denir. + +/// + +Bu, *path operation* hakkında tüm bilgileri içerir ve otomatik dokümantasyonu üretmek için kullanılır. + +`tags`, `parameters`, `requestBody`, `responses` vb. alanları içerir. + +Bu *path operation*’a özel OpenAPI şeması normalde **FastAPI** tarafından otomatik üretilir; ancak siz bunu genişletebilirsiniz. + +/// tip | İpucu + +Bu, düşük seviyeli bir genişletme noktasıdır. + +Yalnızca ek response’lar tanımlamanız gerekiyorsa, bunu yapmanın daha pratik yolu [OpenAPI’de Ek Responses](additional-responses.md){.internal-link target=_blank} kullanmaktır. + +/// + +Bir *path operation* için OpenAPI şemasını `openapi_extra` parametresiyle genişletebilirsiniz. + +### OpenAPI Extensions { #openapi-extensions } + +Örneğin bu `openapi_extra`, [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions) tanımlamak için faydalı olabilir: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *} + +Otomatik API dokümanlarını açtığınızda, extension’ınız ilgili *path operation*’ın en altında görünür. + + + +Ayrıca ortaya çıkan OpenAPI’yi (API’nizde `/openapi.json`) görüntülerseniz, extension’ınızı ilgili *path operation*’ın bir parçası olarak orada da görürsünüz: + +```JSON hl_lines="22" +{ + "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": {} + } + } + } + }, + "x-aperture-labs-portal": "blue" + } + } + } +} +``` + +### Özel OpenAPI *path operation* şeması { #custom-openapi-path-operation-schema } + +`openapi_extra` içindeki dictionary, *path operation* için otomatik üretilen OpenAPI şemasıyla derinlemesine (deep) birleştirilir. + +Böylece otomatik üretilen şemaya ek veri ekleyebilirsiniz. + +Örneğin, Pydantic ile FastAPI’nin otomatik özelliklerini kullanmadan request’i kendi kodunuzla okuyup doğrulamaya karar verebilirsiniz; ancak yine de OpenAPI şemasında request’i tanımlamak isteyebilirsiniz. + +Bunu `openapi_extra` ile yapabilirsiniz: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *} + +Bu örnekte herhangi bir Pydantic model tanımlamadık. Hatta request body JSON olarak parsed bile edilmiyor; doğrudan `bytes` olarak okunuyor ve `magic_data_reader()` fonksiyonu bunu bir şekilde parse etmekten sorumlu oluyor. + +Buna rağmen, request body için beklenen şemayı tanımlayabiliriz. + +### Özel OpenAPI content type { #custom-openapi-content-type } + +Aynı yöntemi kullanarak, Pydantic model ile JSON Schema’yı tanımlayıp bunu *path operation* için özel OpenAPI şeması bölümüne dahil edebilirsiniz. + +Ve bunu, request içindeki veri tipi JSON olmasa bile yapabilirsiniz. + +Örneğin bu uygulamada, FastAPI’nin Pydantic modellerinden JSON Schema çıkarmaya yönelik entegre işlevselliğini ve JSON için otomatik doğrulamayı kullanmıyoruz. Hatta request content type’ını JSON değil, YAML olarak tanımlıyoruz: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} + +Buna rağmen, varsayılan entegre işlevselliği kullanmasak da, YAML olarak almak istediğimiz veri için JSON Schema’yı manuel üretmek üzere bir Pydantic model kullanmaya devam ediyoruz. + +Ardından request’i doğrudan kullanıp body’yi `bytes` olarak çıkarıyoruz. Bu da FastAPI’nin request payload’ını JSON olarak parse etmeye çalışmayacağı anlamına gelir. + +Sonrasında kodumuzda bu YAML içeriğini doğrudan parse ediyor, ardından YAML içeriğini doğrulamak için yine aynı Pydantic modeli kullanıyoruz: + +{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} + +/// tip | İpucu + +Burada aynı Pydantic modeli tekrar kullanıyoruz. + +Aynı şekilde, başka bir yöntemle de doğrulama yapabilirdik. + +/// diff --git a/docs/tr/docs/advanced/response-change-status-code.md b/docs/tr/docs/advanced/response-change-status-code.md new file mode 100644 index 000000000..239c0dddd --- /dev/null +++ b/docs/tr/docs/advanced/response-change-status-code.md @@ -0,0 +1,31 @@ +# Response - Status Code Değiştirme { #response-change-status-code } + +Muhtemelen daha önce varsayılan bir [Response Status Code](../tutorial/response-status-code.md){.internal-link target=_blank} ayarlayabileceğinizi okumuşsunuzdur. + +Ancak bazı durumlarda, varsayılandan farklı bir status code döndürmeniz gerekir. + +## Kullanım senaryosu { #use-case } + +Örneğin, varsayılan olarak "OK" `200` HTTP status code'u döndürmek istediğinizi düşünün. + +Ama veri mevcut değilse onu oluşturmak ve "CREATED" `201` HTTP status code'u döndürmek istiyorsunuz. + +Aynı zamanda, döndürdüğünüz veriyi bir `response_model` ile filtreleyip dönüştürebilmeyi de sürdürmek istiyorsunuz. + +Bu tür durumlarda bir `Response` parametresi kullanabilirsiniz. + +## Bir `Response` parametresi kullanın { #use-a-response-parameter } + +*Path operation function* içinde `Response` tipinde bir parametre tanımlayabilirsiniz (cookie ve header'lar için yapabildiğiniz gibi). + +Ardından bu *geçici (temporal)* `Response` nesnesi üzerinde `status_code` değerini ayarlayabilirsiniz. + +{* ../../docs_src/response_change_status_code/tutorial001_py39.py hl[1,9,12] *} + +Sonrasında, normalde yaptığınız gibi ihtiyacınız olan herhangi bir nesneyi döndürebilirsiniz (`dict`, bir veritabanı modeli, vb.). + +Ve eğer bir `response_model` tanımladıysanız, döndürdüğünüz nesneyi filtrelemek ve dönüştürmek için yine kullanılacaktır. + +**FastAPI**, status code'u (ayrıca cookie ve header'ları) bu *geçici (temporal)* response'tan alır ve `response_model` ile filtrelenmiş, sizin döndürdüğünüz değeri içeren nihai response'a yerleştirir. + +Ayrıca `Response` parametresini dependency'lerde de tanımlayıp status code'u orada ayarlayabilirsiniz. Ancak unutmayın, en son ayarlanan değer geçerli olur. diff --git a/docs/tr/docs/advanced/response-cookies.md b/docs/tr/docs/advanced/response-cookies.md new file mode 100644 index 000000000..d00bfc4cd --- /dev/null +++ b/docs/tr/docs/advanced/response-cookies.md @@ -0,0 +1,51 @@ +# Response Cookie'leri { #response-cookies } + +## Bir `Response` parametresi kullanın { #use-a-response-parameter } + +*Path operation function* içinde `Response` tipinde bir parametre tanımlayabilirsiniz. + +Ardından bu *geçici* response nesnesi üzerinde cookie'leri set edebilirsiniz. + +{* ../../docs_src/response_cookies/tutorial002_py39.py hl[1, 8:9] *} + +Sonrasında normalde yaptığınız gibi ihtiyaç duyduğunuz herhangi bir nesneyi döndürebilirsiniz (bir `dict`, bir veritabanı modeli vb.). + +Ayrıca bir `response_model` tanımladıysanız, döndürdüğünüz nesneyi filtrelemek ve dönüştürmek için yine kullanılacaktır. + +**FastAPI**, bu *geçici* response'u cookie'leri (ayrıca header'ları ve status code'u) çıkarmak için kullanır ve bunları, döndürdüğünüz değeri içeren nihai response'a ekler. Döndürdüğünüz değer, varsa `response_model` ile filtrelenmiş olur. + +`Response` parametresini dependency'lerde de tanımlayıp, onların içinde cookie (ve header) set edebilirsiniz. + +## Doğrudan bir `Response` döndürün { #return-a-response-directly } + +Kodunuzda doğrudan bir `Response` döndürürken de cookie oluşturabilirsiniz. + +Bunu yapmak için, [Doğrudan Response Döndürme](response-directly.md){.internal-link target=_blank} bölümünde anlatıldığı gibi bir response oluşturabilirsiniz. + +Sonra bunun içinde Cookie'leri set edin ve response'u döndürün: + +{* ../../docs_src/response_cookies/tutorial001_py39.py hl[10:12] *} + +/// tip | İpucu + +`Response` parametresini kullanmak yerine doğrudan bir response döndürürseniz, FastAPI onu olduğu gibi (doğrudan) döndürür. + +Bu yüzden, verinizin doğru tipte olduğundan emin olmanız gerekir. Örneğin `JSONResponse` döndürüyorsanız, verinin JSON ile uyumlu olması gerekir. + +Ayrıca `response_model` tarafından filtrelenmesi gereken bir veriyi göndermediğinizden de emin olun. + +/// + +### Daha fazla bilgi { #more-info } + +/// note | Teknik Detaylar + +`from starlette.responses import Response` veya `from starlette.responses import JSONResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olması için `fastapi.responses` içinde `starlette.responses` ile aynı response sınıflarını sunar. Ancak mevcut response'ların büyük kısmı doğrudan Starlette'ten gelir. + +Ve `Response`, header ve cookie set etmek için sık kullanıldığından, **FastAPI** bunu `fastapi.Response` olarak da sağlar. + +/// + +Mevcut tüm parametreleri ve seçenekleri görmek için Starlette dokümantasyonuna bakın. diff --git a/docs/tr/docs/advanced/response-directly.md b/docs/tr/docs/advanced/response-directly.md new file mode 100644 index 000000000..332f1224f --- /dev/null +++ b/docs/tr/docs/advanced/response-directly.md @@ -0,0 +1,65 @@ +# Doğrudan Bir Response Döndürme { #return-a-response-directly } + +**FastAPI** ile bir *path operation* oluşturduğunuzda, normalde ondan herhangi bir veri döndürebilirsiniz: bir `dict`, bir `list`, bir Pydantic model, bir veritabanı modeli vb. + +Varsayılan olarak **FastAPI**, döndürdüğünüz bu değeri [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} bölümünde anlatılan `jsonable_encoder` ile otomatik olarak JSON'a çevirir. + +Ardından perde arkasında, JSON-uyumlu bu veriyi (ör. bir `dict`) client'a response göndermek için kullanılacak bir `JSONResponse` içine yerleştirir. + +Ancak *path operation*'larınızdan doğrudan bir `JSONResponse` döndürebilirsiniz. + +Bu, örneğin özel header'lar veya cookie'ler döndürmek istediğinizde faydalı olabilir. + +## Bir `Response` Döndürme { #return-a-response } + +Aslında herhangi bir `Response` veya onun herhangi bir alt sınıfını döndürebilirsiniz. + +/// tip | İpucu + +`JSONResponse` zaten `Response`'un bir alt sınıfıdır. + +/// + +Bir `Response` döndürdüğünüzde, **FastAPI** bunu olduğu gibi doğrudan iletir. + +Pydantic model'leriyle herhangi bir veri dönüşümü yapmaz, içeriği başka bir tipe çevirmez vb. + +Bu size ciddi bir esneklik sağlar. Herhangi bir veri türü döndürebilir, herhangi bir veri deklarasyonunu veya validasyonunu override edebilirsiniz. + +## Bir `Response` İçinde `jsonable_encoder` Kullanma { #using-the-jsonable-encoder-in-a-response } + +**FastAPI**, sizin döndürdüğünüz `Response` üzerinde hiçbir değişiklik yapmadığı için, içeriğinin gönderilmeye hazır olduğundan emin olmanız gerekir. + +Örneğin, bir Pydantic model'i, önce JSON-uyumlu tiplere çevrilmeden (`datetime`, `UUID` vb.) doğrudan bir `JSONResponse` içine koyamazsınız. Önce tüm veri tipleri JSON-uyumlu hale gelecek şekilde `dict`'e çevrilmesi gerekir. + +Bu gibi durumlarda, response'a vermeden önce verinizi dönüştürmek için `jsonable_encoder` kullanabilirsiniz: + +{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} + +/// note | Teknik Detaylar + +`from starlette.responses import JSONResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olması için `starlette.responses` içeriğini `fastapi.responses` üzerinden de sunar. Ancak mevcut response'ların çoğu doğrudan Starlette'tan gelir. + +/// + +## Özel Bir `Response` Döndürme { #returning-a-custom-response } + +Yukarıdaki örnek ihtiyaç duyduğunuz tüm parçaları gösteriyor, ancak henüz çok kullanışlı değil. Çünkü `item`'ı zaten doğrudan döndürebilirdiniz ve **FastAPI** varsayılan olarak onu sizin için bir `JSONResponse` içine koyup `dict`'e çevirirdi vb. + +Şimdi bunu kullanarak nasıl özel bir response döndürebileceğinize bakalım. + +Diyelim ki XML response döndürmek istiyorsunuz. + +XML içeriğinizi bir string içine koyabilir, onu bir `Response` içine yerleştirip döndürebilirsiniz: + +{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *} + +## Notlar { #notes } + +Bir `Response`'u doğrudan döndürdüğünüzde, verisi otomatik olarak validate edilmez, dönüştürülmez (serialize edilmez) veya dokümante edilmez. + +Ancak yine de [Additional Responses in OpenAPI](additional-responses.md){.internal-link target=_blank} bölümünde anlatıldığı şekilde dokümante edebilirsiniz. + +İlerleyen bölümlerde, otomatik veri dönüşümü, dokümantasyon vb. özellikleri korurken bu özel `Response`'ları nasıl kullanıp declare edebileceğinizi göreceksiniz. diff --git a/docs/tr/docs/advanced/response-headers.md b/docs/tr/docs/advanced/response-headers.md new file mode 100644 index 000000000..85b0799d3 --- /dev/null +++ b/docs/tr/docs/advanced/response-headers.md @@ -0,0 +1,41 @@ +# Response Header'ları { #response-headers } + +## Bir `Response` parametresi kullanın { #use-a-response-parameter } + +*Path operation function* içinde (cookie'lerde yapabildiğiniz gibi) tipi `Response` olan bir parametre tanımlayabilirsiniz. + +Sonra da bu *geçici* response nesnesi üzerinde header'ları ayarlayabilirsiniz. + +{* ../../docs_src/response_headers/tutorial002_py39.py hl[1, 7:8] *} + +Ardından normalde yaptığınız gibi ihtiyacınız olan herhangi bir nesneyi döndürebilirsiniz (bir `dict`, bir veritabanı modeli vb.). + +Eğer bir `response_model` tanımladıysanız, döndürdüğünüz nesneyi filtrelemek ve dönüştürmek için yine kullanılacaktır. + +**FastAPI**, header'ları (aynı şekilde cookie'leri ve status code'u) bu *geçici* response'dan alır ve döndürdüğünüz değeri (varsa bir `response_model` ile filtrelenmiş hâliyle) içeren nihai response'a ekler. + +`Response` parametresini dependency'lerde de tanımlayıp, onların içinde header (ve cookie) ayarlayabilirsiniz. + +## Doğrudan bir `Response` döndürün { #return-a-response-directly } + +Doğrudan bir `Response` döndürdüğünüzde de header ekleyebilirsiniz. + +[Bir Response'u Doğrudan Döndürün](response-directly.md){.internal-link target=_blank} bölümünde anlatıldığı gibi bir response oluşturun ve header'ları ek bir parametre olarak geçin: + +{* ../../docs_src/response_headers/tutorial001_py39.py hl[10:12] *} + +/// note | Teknik Detaylar + +`from starlette.responses import Response` veya `from starlette.responses import JSONResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.responses` içeriğini `fastapi.responses` olarak da sunar. Ancak mevcut response'ların çoğu doğrudan Starlette'ten gelir. + +Ayrıca `Response` header ve cookie ayarlamak için sık kullanıldığından, **FastAPI** bunu `fastapi.Response` altında da sağlar. + +/// + +## Özel Header'lar { #custom-headers } + +Özel/proprietary header'ların `X-` prefix'i kullanılarak eklenebileceğini unutmayın. + +Ancak tarayıcıdaki bir client'ın görebilmesini istediğiniz özel header'larınız varsa, bunları CORS ayarlarınıza eklemeniz gerekir ([CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank} bölümünde daha fazla bilgi), bunun için Starlette'in CORS dokümanında açıklanan `expose_headers` parametresini kullanın. diff --git a/docs/tr/docs/advanced/security/http-basic-auth.md b/docs/tr/docs/advanced/security/http-basic-auth.md new file mode 100644 index 000000000..b194c763e --- /dev/null +++ b/docs/tr/docs/advanced/security/http-basic-auth.md @@ -0,0 +1,107 @@ +# HTTP Basic Auth { #http-basic-auth } + +En basit senaryolarda HTTP Basic Auth kullanabilirsiniz. + +HTTP Basic Auth’ta uygulama, içinde kullanıcı adı ve şifre bulunan bir header bekler. + +Eğer bunu almazsa HTTP 401 "Unauthorized" hatası döndürür. + +Ayrıca değeri `Basic` olan ve isteğe bağlı `realm` parametresi içerebilen `WWW-Authenticate` header’ını da döndürür. + +Bu da tarayıcıya, kullanıcı adı ve şifre için entegre giriş penceresini göstermesini söyler. + +Ardından kullanıcı adı ve şifreyi yazdığınızda tarayıcı bunları otomatik olarak header içinde gönderir. + +## Basit HTTP Basic Auth { #simple-http-basic-auth } + +* `HTTPBasic` ve `HTTPBasicCredentials` import edin. +* `HTTPBasic` kullanarak bir "`security` scheme" oluşturun. +* *path operation*’ınızda bir dependency ile bu `security`’yi kullanın. +* Bu, `HTTPBasicCredentials` tipinde bir nesne döndürür: + * İçinde gönderilen `username` ve `password` bulunur. + +{* ../../docs_src/security/tutorial006_an_py39.py hl[4,8,12] *} + +URL’yi ilk kez açmaya çalıştığınızda (veya dokümanlardaki "Execute" butonuna tıkladığınızda) tarayıcı sizden kullanıcı adınızı ve şifrenizi ister: + + + +## Kullanıcı adını kontrol edin { #check-the-username } + +Daha kapsamlı bir örneğe bakalım. + +Kullanıcı adı ve şifrenin doğru olup olmadığını kontrol etmek için bir dependency kullanın. + +Bunun için kullanıcı adı ve şifreyi kontrol ederken Python standart modülü olan `secrets`’i kullanın. + +`secrets.compare_digest()`; `bytes` ya da yalnızca ASCII karakterleri (İngilizce’deki karakterler) içeren bir `str` almalıdır. Bu da `Sebastián` içindeki `á` gibi karakterlerle çalışmayacağı anlamına gelir. + +Bunu yönetmek için önce `username` ve `password` değerlerini UTF-8 ile encode ederek `bytes`’a dönüştürürüz. + +Sonra `secrets.compare_digest()` kullanarak `credentials.username`’in `"stanleyjobson"` ve `credentials.password`’ün `"swordfish"` olduğundan emin olabiliriz. + +{* ../../docs_src/security/tutorial007_an_py39.py hl[1,12:24] *} + +Bu, kabaca şuna benzer olurdu: + +```Python +if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): + # Return some error + ... +``` + +Ancak `secrets.compare_digest()` kullanarak, "timing attacks" denilen bir saldırı türüne karşı güvenli olursunuz. + +### Timing Attacks { #timing-attacks } + +Peki "timing attack" nedir? + +Bazı saldırganların kullanıcı adı ve şifreyi tahmin etmeye çalıştığını düşünelim. + +Ve `johndoe` kullanıcı adı ve `love123` şifresi ile bir request gönderiyorlar. + +Uygulamanızdaki Python kodu o zaman kabaca şuna denk olur: + +```Python +if "johndoe" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +Ancak Python, `johndoe` içindeki ilk `j` ile `stanleyjobson` içindeki ilk `s`’i karşılaştırdığı anda `False` döndürür; çünkü iki string’in aynı olmadığını zaten anlar ve "kalan harfleri karşılaştırmak için daha fazla hesaplama yapmaya gerek yok" diye düşünür. Uygulamanız da "Incorrect username or password" der. + +Sonra saldırganlar bu sefer `stanleyjobsox` kullanıcı adı ve `love123` şifresi ile dener. + +Uygulama kodunuz da şuna benzer bir şey yapar: + +```Python +if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +Bu kez Python, iki string’in aynı olmadığını fark etmeden önce hem `stanleyjobsox` hem de `stanleyjobson` içinde `stanleyjobso` kısmının tamamını karşılaştırmak zorunda kalır. Bu nedenle "Incorrect username or password" yanıtını vermesi birkaç mikro saniye daha uzun sürer. + +#### Yanıt süresi saldırganlara yardımcı olur { #the-time-to-answer-helps-the-attackers } + +Bu noktada saldırganlar, server’ın "Incorrect username or password" response’unu göndermesinin birkaç mikro saniye daha uzun sürdüğünü fark ederek _bir şeyleri_ doğru yaptıklarını anlar; yani başlangıçtaki bazı harfler doğrudur. + +Sonra tekrar denerken, bunun `johndoe`’dan ziyade `stanleyjobsox`’a daha yakın bir şey olması gerektiğini bilerek devam edebilirler. + +#### "Profesyonel" bir saldırı { #a-professional-attack } + +Elbette saldırganlar bunu elle tek tek denemez; bunu yapan bir program yazarlar. Muhtemelen saniyede binlerce ya da milyonlarca test yaparlar ve her seferinde yalnızca bir doğru harf daha elde ederler. + +Böylece birkaç dakika ya da birkaç saat içinde doğru kullanıcı adı ve şifreyi, yanıt süresini kullanarak ve uygulamamızın "yardımıyla" tahmin etmiş olurlar. + +#### `secrets.compare_digest()` ile düzeltin { #fix-it-with-secrets-compare-digest } + +Ancak bizim kodumuzda `secrets.compare_digest()` kullanıyoruz. + +Kısacası, `stanleyjobsox` ile `stanleyjobson`’u karşılaştırmak için geçen süre, `johndoe` ile `stanleyjobson`’u karşılaştırmak için geçen süreyle aynı olur. Şifre için de aynı şekilde. + +Bu sayede uygulama kodunuzda `secrets.compare_digest()` kullanarak bu güvenlik saldırıları ailesine karşı güvenli olursunuz. + +### Hatayı döndürün { #return-the-error } + +Credential’ların hatalı olduğunu tespit ettikten sonra, 401 status code ile (credential verilmediğinde dönenle aynı) bir `HTTPException` döndürün ve tarayıcının giriş penceresini yeniden göstermesi için `WWW-Authenticate` header’ını ekleyin: + +{* ../../docs_src/security/tutorial007_an_py39.py hl[26:30] *} diff --git a/docs/tr/docs/advanced/security/oauth2-scopes.md b/docs/tr/docs/advanced/security/oauth2-scopes.md new file mode 100644 index 000000000..ecba7851b --- /dev/null +++ b/docs/tr/docs/advanced/security/oauth2-scopes.md @@ -0,0 +1,274 @@ +# OAuth2 scope'ları { #oauth2-scopes } + +OAuth2 scope'larını **FastAPI** ile doğrudan kullanabilirsiniz; sorunsuz çalışacak şekilde entegre edilmiştir. + +Bu sayede OAuth2 standardını takip eden, daha ince taneli bir izin sistemini OpenAPI uygulamanıza (ve API dokümanlarınıza) entegre edebilirsiniz. + +Scope'lu OAuth2; Facebook, Google, GitHub, Microsoft, X (Twitter) vb. birçok büyük kimlik doğrulama sağlayıcısının kullandığı mekanizmadır. Kullanıcı ve uygulamalara belirli izinler vermek için bunu kullanırlar. + +Facebook, Google, GitHub, Microsoft, X (Twitter) ile "giriş yaptığınızda", o uygulama scope'lu OAuth2 kullanıyor demektir. + +Bu bölümde, **FastAPI** uygulamanızda aynı scope'lu OAuth2 ile authentication ve authorization'ı nasıl yöneteceğinizi göreceksiniz. + +/// warning | Uyarı + +Bu bölüm az çok ileri seviye sayılır. Yeni başlıyorsanız atlayabilirsiniz. + +OAuth2 scope'larına mutlaka ihtiyacınız yok; authentication ve authorization'ı istediğiniz şekilde ele alabilirsiniz. + +Ancak scope'lu OAuth2, API'nize (OpenAPI ile) ve API dokümanlarınıza güzel biçimde entegre edilebilir. + +Buna rağmen, bu scope'ları (veya başka herhangi bir security/authorization gereksinimini) kodunuzda ihtiyaç duyduğunuz şekilde yine siz zorunlu kılarsınız. + +Birçok durumda scope'lu OAuth2 gereğinden fazla (overkill) olabilir. + +Ama ihtiyacınız olduğunu biliyorsanız ya da merak ediyorsanız okumaya devam edin. + +/// + +## OAuth2 scope'ları ve OpenAPI { #oauth2-scopes-and-openapi } + +OAuth2 spesifikasyonu, "scope"ları boşluklarla ayrılmış string'lerden oluşan bir liste olarak tanımlar. + +Bu string'lerin her birinin içeriği herhangi bir formatta olabilir, ancak boşluk içermemelidir. + +Bu scope'lar "izinleri" temsil eder. + +OpenAPI'de (ör. API dokümanlarında) "security scheme" tanımlayabilirsiniz. + +Bu security scheme'lerden biri OAuth2 kullanıyorsa, scope'ları da tanımlayıp kullanabilirsiniz. + +Her bir "scope" sadece bir string'dir (boşluksuz). + +Genellikle belirli güvenlik izinlerini tanımlamak için kullanılır, örneğin: + +* `users:read` veya `users:write` sık görülen örneklerdir. +* `instagram_basic` Facebook / Instagram tarafından kullanılır. +* `https://www.googleapis.com/auth/drive` Google tarafından kullanılır. + +/// info | Bilgi + +OAuth2'de "scope", gereken belirli bir izni bildiren bir string'den ibarettir. + +`:` gibi başka karakterler içermesi ya da bir URL olması önemli değildir. + +Bu detaylar implementasyon'a bağlıdır. + +OAuth2 için bunlar sadece string'dir. + +/// + +## Genel görünüm { #global-view } + +Önce, ana **Tutorial - User Guide** içindeki [Password (ve hashing) ile OAuth2, JWT token'lı Bearer](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank} örneklerinden, OAuth2 scope'larına geçince hangi kısımların değiştiğine hızlıca bakalım: + +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:126,130:136,141,157] *} + +Şimdi bu değişiklikleri adım adım inceleyelim. + +## OAuth2 Security scheme { #oauth2-security-scheme } + +İlk değişiklik, artık OAuth2 security scheme'ini iki adet kullanılabilir scope ile tanımlamamız: `me` ve `items`. + +`scopes` parametresi; her scope'un key, açıklamasının ise value olduğu bir `dict` alır: + +{* ../../docs_src/security/tutorial005_an_py310.py hl[63:66] *} + +Bu scope'ları tanımladığımız için, login/authorize yaptığınızda API dokümanlarında görünecekler. + +Ve hangi scope'lara erişim vermek istediğinizi seçebileceksiniz: `me` ve `items`. + +Bu, Facebook/Google/GitHub vb. ile giriş yaparken izin verdiğinizde kullanılan mekanizmanın aynısıdır: + + + +## Scope'lu JWT token { #jwt-token-with-scopes } + +Şimdi token *path operation*'ını, istenen scope'ları döndürecek şekilde değiştirin. + +Hâlâ aynı `OAuth2PasswordRequestForm` kullanılıyor. Bu form, request'te aldığı her scope için `str`'lerden oluşan bir `list` içeren `scopes` özelliğine sahiptir. + +Ve scope'ları JWT token'ın bir parçası olarak döndürüyoruz. + +/// danger | Uyarı + +Basitlik için burada, gelen scope'ları doğrudan token'a ekliyoruz. + +Ama uygulamanızda güvenlik açısından, yalnızca kullanıcının gerçekten sahip olabileceği scope'ları (veya sizin önceden tanımladıklarınızı) eklediğinizden emin olmalısınız. + +/// + +{* ../../docs_src/security/tutorial005_an_py310.py hl[157] *} + +## *Path operation*'larda ve dependency'lerde scope tanımlama { #declare-scopes-in-path-operations-and-dependencies } + +Artık `/users/me/items/` için olan *path operation*'ın `items` scope'unu gerektirdiğini tanımlıyoruz. + +Bunun için `fastapi` içinden `Security` import edip kullanıyoruz. + +Dependency'leri (`Depends` gibi) tanımlamak için `Security` kullanabilirsiniz; fakat `Security`, ayrıca string'lerden oluşan bir scope listesi alan `scopes` parametresini de alır. + +Bu durumda `Security`'ye dependency fonksiyonu olarak `get_current_active_user` veriyoruz (`Depends` ile yaptığımız gibi). + +Ama ayrıca bir `list` olarak scope'ları da veriyoruz; burada tek bir scope var: `items` (daha fazla da olabilir). + +Ve `get_current_active_user` dependency fonksiyonu, sadece `Depends` ile değil `Security` ile de alt-dependency'ler tanımlayabilir. Kendi alt-dependency fonksiyonunu (`get_current_user`) ve daha fazla scope gereksinimini tanımlar. + +Bu örnekte `me` scope'unu gerektiriyor (birden fazla scope da isteyebilirdi). + +/// note | Not + +Farklı yerlerde farklı scope'lar eklemek zorunda değilsiniz. + +Burada, **FastAPI**'nin farklı seviyelerde tanımlanan scope'ları nasıl ele aldığını göstermek için böyle yapıyoruz. + +/// + +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,172] *} + +/// info | Teknik Detaylar + +`Security` aslında `Depends`'in bir alt sınıfıdır ve sadece birazdan göreceğimiz bir ek parametreye sahiptir. + +Ancak `Depends` yerine `Security` kullanınca **FastAPI**, security scope'larının tanımlanabileceğini bilir, bunları içeride kullanır ve API'yi OpenAPI ile dokümante eder. + +Fakat `fastapi` içinden `Query`, `Path`, `Depends`, `Security` vb. import ettiğiniz şeyler, aslında özel sınıflar döndüren fonksiyonlardır. + +/// + +## `SecurityScopes` kullanımı { #use-securityscopes } + +Şimdi `get_current_user` dependency'sini güncelleyelim. + +Bu fonksiyon, yukarıdaki dependency'ler tarafından kullanılıyor. + +Burada, daha önce oluşturduğumuz aynı OAuth2 scheme'i dependency olarak tanımlıyoruz: `oauth2_scheme`. + +Bu dependency fonksiyonunun kendi içinde bir scope gereksinimi olmadığı için, `oauth2_scheme` ile `Depends` kullanabiliriz; security scope'larını belirtmemiz gerekmiyorsa `Security` kullanmak zorunda değiliz. + +Ayrıca `fastapi.security` içinden import edilen, `SecurityScopes` tipinde özel bir parametre tanımlıyoruz. + +Bu `SecurityScopes` sınıfı, `Request`'e benzer (`Request`, request nesnesini doğrudan almak için kullanılmıştı). + +{* ../../docs_src/security/tutorial005_an_py310.py hl[9,106] *} + +## `scopes`'ları kullanma { #use-the-scopes } + +`security_scopes` parametresi `SecurityScopes` tipinde olacaktır. + +Bu nesnenin `scopes` adlı bir özelliği vardır; bu liste, kendisinin ve bunu alt-dependency olarak kullanan tüm dependency'lerin gerektirdiği tüm scope'ları içerir. Yani tüm "dependant"lar... kafa karıştırıcı gelebilir; aşağıda tekrar açıklanıyor. + +`security_scopes` nesnesi (`SecurityScopes` sınıfından) ayrıca, bu scope'ları boşluklarla ayrılmış tek bir string olarak veren `scope_str` attribute'una sahiptir (bunu kullanacağız). + +Sonrasında birkaç farklı noktada tekrar kullanabileceğimiz (`raise` edebileceğimiz) bir `HTTPException` oluşturuyoruz. + +Bu exception içinde, gerekiyorsa, gerekli scope'ları boşlukla ayrılmış bir string olarak (`scope_str` ile) ekliyoruz. Bu scope'ları içeren string'i `WWW-Authenticate` header'ına koyuyoruz (spesifikasyonun bir parçası). + +{* ../../docs_src/security/tutorial005_an_py310.py hl[106,108:116] *} + +## `username` ve veri şeklinin doğrulanması { #verify-the-username-and-data-shape } + +Bir `username` aldığımızı doğruluyoruz ve scope'ları çıkarıyoruz. + +Ardından bu veriyi Pydantic model'i ile doğruluyoruz (`ValidationError` exception'ını yakalayarak). JWT token'ı okurken veya Pydantic ile veriyi doğrularken bir hata olursa, daha önce oluşturduğumuz `HTTPException`'ı fırlatıyoruz. + +Bunun için Pydantic model'i `TokenData`'yı, `scopes` adlı yeni bir özellik ekleyerek güncelliyoruz. + +Veriyi Pydantic ile doğrulayarak örneğin scope'ların tam olarak `str`'lerden oluşan bir `list` olduğunu ve `username`'in bir `str` olduğunu garanti edebiliriz. + +Aksi halde, örneğin bir `dict` veya başka bir şey gelebilir; bu da daha sonra uygulamanın bir yerinde kırılmaya yol açıp güvenlik riski oluşturabilir. + +Ayrıca bu `username` ile bir kullanıcı olduğunu doğruluyoruz; yoksa yine aynı exception'ı fırlatıyoruz. + +{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:129] *} + +## `scopes`'ların doğrulanması { #verify-the-scopes } + +Şimdi bu dependency'nin ve tüm dependant'ların ( *path operation*'lar dahil) gerektirdiği tüm scope'ların, alınan token'da sağlanan scope'lar içinde olup olmadığını doğruluyoruz; değilse `HTTPException` fırlatıyoruz. + +Bunun için, tüm bu scope'ları `str` olarak içeren bir `list` olan `security_scopes.scopes` kullanılır. + +{* ../../docs_src/security/tutorial005_an_py310.py hl[130:136] *} + +## Dependency ağacı ve scope'lar { #dependency-tree-and-scopes } + +Bu dependency ağacını ve scope'ları tekrar gözden geçirelim. + +`get_current_active_user` dependency'si, alt-dependency olarak `get_current_user`'ı kullandığı için, `get_current_active_user` üzerinde tanımlanan `"me"` scope'u, `get_current_user`'a geçirilen `security_scopes.scopes` içindeki gerekli scope listesine dahil edilir. + +*Path operation*'ın kendisi de `"items"` scope'unu tanımlar; bu da `get_current_user`'a geçirilen `security_scopes.scopes` listesinde yer alır. + +Dependency'lerin ve scope'ların hiyerarşisi şöyle görünür: + +* *Path operation* `read_own_items` şunlara sahiptir: + * Dependency ile gerekli scope'lar `["items"]`: + * `get_current_active_user`: + * `get_current_active_user` dependency fonksiyonu şunlara sahiptir: + * Dependency ile gerekli scope'lar `["me"]`: + * `get_current_user`: + * `get_current_user` dependency fonksiyonu şunlara sahiptir: + * Kendisinin gerektirdiği scope yok. + * `oauth2_scheme` kullanan bir dependency. + * `SecurityScopes` tipinde bir `security_scopes` parametresi: + * Bu `security_scopes` parametresinin `scopes` adlı bir özelliği vardır ve yukarıda tanımlanan tüm scope'ları içeren bir `list` taşır, yani: + * *Path operation* `read_own_items` için `security_scopes.scopes` `["me", "items"]` içerir. + * *Path operation* `read_users_me` için `security_scopes.scopes` `["me"]` içerir; çünkü bu scope `get_current_active_user` dependency'sinde tanımlanmıştır. + * *Path operation* `read_system_status` için `security_scopes.scopes` `[]` (boş) olur; çünkü herhangi bir `Security` ile `scopes` tanımlamamıştır ve dependency'si olan `get_current_user` da `scopes` tanımlamaz. + +/// tip | İpucu + +Buradaki önemli ve "sihirli" nokta şu: `get_current_user`, her *path operation* için kontrol etmesi gereken farklı bir `scopes` listesi alır. + +Bu, belirli bir *path operation* için dependency ağacındaki her *path operation* ve her dependency üzerinde tanımlanan `scopes`'lara bağlıdır. + +/// + +## `SecurityScopes` hakkında daha fazla detay { #more-details-about-securityscopes } + +`SecurityScopes`'u herhangi bir noktada ve birden fazla yerde kullanabilirsiniz; mutlaka "kök" dependency'de olmak zorunda değildir. + +Her zaman, **o spesifik** *path operation* ve **o spesifik** dependency ağacı için, mevcut `Security` dependency'lerinde ve tüm dependant'larda tanımlanan security scope'larını içerir. + +`SecurityScopes`, dependant'ların tanımladığı tüm scope'ları barındırdığı için, gereken scope'ların token'da olup olmadığını merkezi bir dependency fonksiyonunda doğrulayıp, farklı *path operation*'larda farklı scope gereksinimleri tanımlayabilirsiniz. + +Bu kontroller her *path operation* için bağımsız yapılır. + +## Deneyin { #check-it } + +API dokümanlarını açarsanız, authenticate olup hangi scope'ları authorize etmek istediğinizi seçebilirsiniz. + + + +Hiç scope seçmezseniz "authenticated" olursunuz; ancak `/users/me/` veya `/users/me/items/`'e erişmeye çalıştığınızda, yeterli izniniz olmadığını söyleyen bir hata alırsınız. Yine de `/status/`'a erişebilirsiniz. + +`me` scope'unu seçip `items` scope'unu seçmezseniz `/users/me/`'a erişebilirsiniz ama `/users/me/items/`'e erişemezsiniz. + +Bu, bir üçüncü taraf uygulamanın, bir kullanıcı tarafından sağlanan token ile bu *path operation*'lardan birine erişmeye çalıştığında; kullanıcının uygulamaya kaç izin verdiğine bağlı olarak yaşayacağı durumdur. + +## Üçüncü taraf entegrasyonları hakkında { #about-third-party-integrations } + +Bu örnekte OAuth2 "password" flow'unu kullanıyoruz. + +Bu, kendi uygulamamıza giriş yaptığımız durumlar için uygundur; muhtemelen kendi frontend'imiz vardır. + +Çünkü `username` ve `password` alacağını bildiğimiz frontend'i biz kontrol ediyoruz, dolayısıyla güvenebiliriz. + +Ancak başkalarının bağlanacağı bir OAuth2 uygulaması geliştiriyorsanız (yani Facebook, Google, GitHub vb. gibi bir authentication provider muadili geliştiriyorsanız) diğer flow'lardan birini kullanmalısınız. + +En yaygını implicit flow'dur. + +En güvenlisi code flow'dur; ancak daha fazla adım gerektirdiği için implementasyonu daha karmaşıktır. Daha karmaşık olduğundan, birçok sağlayıcı implicit flow'yu önermeye yönelir. + +/// note | Not + +Her authentication provider'ın flow'ları markasının bir parçası yapmak için farklı şekilde adlandırması yaygındır. + +Ama sonuçta aynı OAuth2 standardını implement ediyorlar. + +/// + +**FastAPI**, bu OAuth2 authentication flow'larının tamamı için `fastapi.security.oauth2` içinde yardımcı araçlar sunar. + +## Decorator `dependencies` içinde `Security` { #security-in-decorator-dependencies } + +Decorator'ın `dependencies` parametresinde bir `list` `Depends` tanımlayabildiğiniz gibi ( [Path operation decorator'larında Dependencies](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank} bölümünde açıklandığı üzere), burada `scopes` ile birlikte `Security` de kullanabilirsiniz. diff --git a/docs/tr/docs/advanced/settings.md b/docs/tr/docs/advanced/settings.md new file mode 100644 index 000000000..e3bcaac61 --- /dev/null +++ b/docs/tr/docs/advanced/settings.md @@ -0,0 +1,302 @@ +# Ayarlar ve Ortam Değişkenleri { #settings-and-environment-variables } + +Birçok durumda uygulamanızın bazı harici ayarlara veya konfigürasyonlara ihtiyacı olabilir; örneğin secret key'ler, veritabanı kimlik bilgileri, e-posta servisleri için kimlik bilgileri vb. + +Bu ayarların çoğu değişkendir (değişebilir); örneğin veritabanı URL'leri. Ayrıca birçoğu hassas olabilir; örneğin secret'lar. + +Bu nedenle bunları, uygulama tarafından okunan environment variable'lar ile sağlamak yaygındır. + +/// tip | İpucu + +Environment variable'ları anlamak için [Environment Variables](../environment-variables.md){.internal-link target=_blank} dokümanını okuyabilirsiniz. + +/// + +## Tipler ve doğrulama { #types-and-validation } + +Bu environment variable'lar yalnızca metin (string) taşıyabilir; çünkü Python'ın dışındadırlar ve diğer programlarla ve sistemin geri kalanıyla uyumlu olmaları gerekir (hatta Linux, Windows, macOS gibi farklı işletim sistemleriyle de). + +Bu da, Python içinde bir environment variable'dan okunan herhangi bir değerin `str` olacağı anlamına gelir; farklı bir tipe dönüştürme veya herhangi bir doğrulama işlemi kod içinde yapılmalıdır. + +## Pydantic `Settings` { #pydantic-settings } + +Neyse ki Pydantic, environment variable'lardan gelen bu ayarları yönetmek için Pydantic: Settings management ile çok iyi bir yardımcı araç sunar. + +### `pydantic-settings`'i kurun { #install-pydantic-settings } + +Önce, [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, aktive ettiğinizden emin olun ve ardından `pydantic-settings` paketini kurun: + +
+ +```console +$ pip install pydantic-settings +---> 100% +``` + +
+ +Ayrıca `all` extras'ını şu şekilde kurduğunuzda da dahil gelir: + +
+ +```console +$ pip install "fastapi[all]" +---> 100% +``` + +
+ +### `Settings` nesnesini oluşturun { #create-the-settings-object } + +Pydantic'ten `BaseSettings` import edin ve bir alt sınıf (sub-class) oluşturun; tıpkı bir Pydantic model'inde olduğu gibi. + +Pydantic model'lerinde olduğu gibi, type annotation'larla (ve gerekirse default değerlerle) class attribute'ları tanımlarsınız. + +Pydantic model'lerinde kullandığınız aynı doğrulama özelliklerini ve araçlarını burada da kullanabilirsiniz; örneğin farklı veri tipleri ve `Field()` ile ek doğrulamalar. + +{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} + +/// tip | İpucu + +Hızlıca kopyalayıp yapıştırmak istiyorsanız bu örneği kullanmayın; aşağıdaki son örneği kullanın. + +/// + +Ardından, bu `Settings` sınıfının bir instance'ını oluşturduğunuzda (bu örnekte `settings` nesnesi), Pydantic environment variable'ları büyük/küçük harfe duyarsız şekilde okur; yani büyük harfli `APP_NAME` değişkeni, yine de `app_name` attribute'u için okunur. + +Sonrasında veriyi dönüştürür ve doğrular. Böylece `settings` nesnesini kullandığınızda, tanımladığınız tiplerde verilere sahip olursunuz (örn. `items_per_user` bir `int` olur). + +### `settings`'i kullanın { #use-the-settings } + +Daha sonra uygulamanızda yeni `settings` nesnesini kullanabilirsiniz: + +{* ../../docs_src/settings/tutorial001_py39.py hl[18:20] *} + +### Server'ı çalıştırın { #run-the-server } + +Sonraki adımda server'ı çalıştırırken konfigürasyonları environment variable olarak geçersiniz; örneğin `ADMIN_EMAIL` ve `APP_NAME` şu şekilde ayarlanabilir: + +
+ +```console +$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +/// tip | İpucu + +Tek bir komut için birden fazla env var ayarlamak istiyorsanız aralarına boşluk koyun ve hepsini komuttan önce yazın. + +/// + +Böylece `admin_email` ayarı `"deadpool@example.com"` olur. + +`app_name` `"ChimichangApp"` olur. + +`items_per_user` ise default değeri olan `50` olarak kalır. + +## Ayarları başka bir module'de tutma { #settings-in-another-module } + +[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank} bölümünde gördüğünüz gibi, bu ayarları başka bir module dosyasına koyabilirsiniz. + +Örneğin `config.py` adında bir dosyanız şu şekilde olabilir: + +{* ../../docs_src/settings/app01_py39/config.py *} + +Ve ardından bunu `main.py` dosyasında kullanabilirsiniz: + +{* ../../docs_src/settings/app01_py39/main.py hl[3,11:13] *} + +/// tip | İpucu + +[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank} bölümünde gördüğünüz gibi, ayrıca bir `__init__.py` dosyasına da ihtiyacınız olacak. + +/// + +## Dependency içinde ayarlar { #settings-in-a-dependency } + +Bazı durumlarda, her yerde kullanılan global bir `settings` nesnesi yerine ayarları bir dependency üzerinden sağlamak faydalı olabilir. + +Bu özellikle test sırasında çok işe yarar; çünkü bir dependency'yi kendi özel ayarlarınızla override etmek çok kolaydır. + +### Config dosyası { #the-config-file } + +Bir önceki örnekten devam edersek, `config.py` dosyanız şöyle görünebilir: + +{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *} + +Dikkat edin, artık default bir instance `settings = Settings()` oluşturmuyoruz. + +### Ana uygulama dosyası { #the-main-app-file } + +Şimdi, yeni bir `config.Settings()` döndüren bir dependency oluşturuyoruz. + +{* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *} + +/// tip | İpucu + +`@lru_cache` konusunu birazdan ele alacağız. + +Şimdilik `get_settings()`'in normal bir fonksiyon olduğunu varsayabilirsiniz. + +/// + +Sonra bunu dependency olarak *path operation function*'dan talep edebilir ve ihtiyaç duyduğumuz her yerde kullanabiliriz. + +{* ../../docs_src/settings/app02_an_py39/main.py hl[17,19:21] *} + +### Ayarlar ve test { #settings-and-testing } + +Ardından, `get_settings` için bir dependency override oluşturarak test sırasında farklı bir settings nesnesi sağlamak çok kolay olur: + +{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *} + +Dependency override içinde, yeni `Settings` nesnesini oluştururken `admin_email` için yeni bir değer ayarlarız ve sonra bu yeni nesneyi döndürürüz. + +Sonrasında bunun kullanıldığını test edebiliriz. + +## `.env` dosyası okuma { #reading-a-env-file } + +Çok sayıda ayarınız varsa ve bunlar farklı ortamlarda sık sık değişiyorsa, bunları bir dosyaya koyup, sanki environment variable'mış gibi o dosyadan okumak faydalı olabilir. + +Bu yaklaşım oldukça yaygındır ve bir adı vardır: Bu environment variable'lar genellikle `.env` adlı bir dosyaya konur ve bu dosyaya "dotenv" denir. + +/// tip | İpucu + +Nokta (`.`) ile başlayan dosyalar, Linux ve macOS gibi Unix-benzeri sistemlerde gizli dosyadır. + +Ancak dotenv dosyasının mutlaka bu dosya adına sahip olması gerekmez. + +/// + +Pydantic, harici bir kütüphane kullanarak bu tür dosyalardan okuma desteğine sahiptir. Daha fazlası için: Pydantic Settings: Dotenv (.env) support. + +/// tip | İpucu + +Bunun çalışması için `pip install python-dotenv` yapmanız gerekir. + +/// + +### `.env` dosyası { #the-env-file } + +Şöyle bir `.env` dosyanız olabilir: + +```bash +ADMIN_EMAIL="deadpool@example.com" +APP_NAME="ChimichangApp" +``` + +### Ayarları `.env`'den okuyun { #read-settings-from-env } + +Ardından `config.py` dosyanızı şöyle güncelleyin: + +{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} + +/// tip | İpucu + +`model_config` attribute'u yalnızca Pydantic konfigürasyonu içindir. Daha fazlası için Pydantic: Concepts: Configuration. + +/// + +Burada, Pydantic `Settings` sınıfınızın içinde `env_file` konfigürasyonunu tanımlar ve değer olarak kullanmak istediğimiz dotenv dosyasının dosya adını veririz. + +### `lru_cache` ile `Settings`'i yalnızca bir kez oluşturma { #creating-the-settings-only-once-with-lru-cache } + +Diskten dosya okumak normalde maliyetli (yavaş) bir işlemdir; bu yüzden muhtemelen bunu yalnızca bir kez yapıp aynı settings nesnesini tekrar kullanmak istersiniz. Her request için yeniden okumak istemezsiniz. + +Ancak her seferinde şunu yaptığımızda: + +```Python +Settings() +``` + +yeni bir `Settings` nesnesi oluşturulur ve oluşturulurken `.env` dosyasını yeniden okur. + +Dependency fonksiyonu sadece şöyle olsaydı: + +```Python +def get_settings(): + return Settings() +``` + +bu nesneyi her request için oluştururduk ve `.env` dosyasını her request'te okurduk. ⚠️ + +Fakat en üstte `@lru_cache` decorator'ünü kullandığımız için `Settings` nesnesi yalnızca bir kez, ilk çağrıldığı anda oluşturulur. ✔️ + +{* ../../docs_src/settings/app03_an_py39/main.py hl[1,11] *} + +Sonraki request'lerde dependency'ler içinden `get_settings()` çağrıldığında, `get_settings()`'in iç kodu tekrar çalıştırılıp yeni bir `Settings` nesnesi yaratılmak yerine, ilk çağrıda döndürülen aynı nesne tekrar tekrar döndürülür. + +#### `lru_cache` Teknik Detayları { #lru-cache-technical-details } + +`@lru_cache`, decorator olarak uygulandığı fonksiyonu, her seferinde tekrar hesaplamak yerine ilk seferde döndürdüğü değeri döndürecek şekilde değiştirir; yani fonksiyon kodunu her çağrıda yeniden çalıştırmaz. + +Bu nedenle altındaki fonksiyon, argüman kombinasyonlarının her biri için bir kez çalıştırılır. Sonra bu argüman kombinasyonlarının her biri için döndürülmüş değerler, fonksiyon aynı argüman kombinasyonuyla çağrıldıkça tekrar tekrar kullanılır. + +Örneğin, şöyle bir fonksiyonunuz varsa: + +```Python +@lru_cache +def say_hi(name: str, salutation: str = "Ms."): + return f"Hello {salutation} {name}" +``` + +programınız şu şekilde çalışabilir: + +```mermaid +sequenceDiagram + +participant code as Code +participant function as say_hi() +participant execute as Execute function + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Camila") + function ->> execute: execute function code + execute ->> code: return the result + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Camila") + function ->> code: return stored result + end + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Rick") + function ->> execute: execute function code + execute ->> code: return the result + end + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Rick", salutation="Mr.") + function ->> execute: execute function code + execute ->> code: return the result + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Rick") + function ->> code: return stored result + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Camila") + function ->> code: return stored result + end +``` + +Bizim `get_settings()` dependency'miz özelinde ise fonksiyon hiç argüman almaz; dolayısıyla her zaman aynı değeri döndürür. + +Bu şekilde, neredeyse global bir değişken gibi davranır. Ancak bir dependency fonksiyonu kullandığı için testte kolayca override edebiliriz. + +`@lru_cache`, Python standart kütüphanesinin bir parçası olan `functools` içindedir. Daha fazla bilgi için: Python docs for `@lru_cache`. + +## Özet { #recap } + +Uygulamanızın ayarlarını veya konfigürasyonlarını yönetmek için, Pydantic model'lerinin tüm gücüyle birlikte Pydantic Settings'i kullanabilirsiniz. + +* Dependency kullanarak test etmeyi basitleştirebilirsiniz. +* Bununla `.env` dosyalarını kullanabilirsiniz. +* `@lru_cache` kullanmak, dotenv dosyasını her request için tekrar tekrar okumayı engellerken, test sırasında override etmenize de izin verir. diff --git a/docs/tr/docs/advanced/sub-applications.md b/docs/tr/docs/advanced/sub-applications.md new file mode 100644 index 000000000..4773ba200 --- /dev/null +++ b/docs/tr/docs/advanced/sub-applications.md @@ -0,0 +1,67 @@ +# Alt Uygulamalar - Mount İşlemi { #sub-applications-mounts } + +Kendi bağımsız OpenAPI şemaları ve kendi dokümantasyon arayüzleri olan iki bağımsız FastAPI uygulamasına ihtiyacınız varsa, bir ana uygulama oluşturup bir (veya daha fazla) alt uygulamayı "mount" edebilirsiniz. + +## Bir **FastAPI** uygulamasını mount etmek { #mounting-a-fastapi-application } + +"Mount" etmek, belirli bir path altında tamamen "bağımsız" bir uygulamayı eklemek anlamına gelir. Ardından o path’in altındaki her şeyi, alt uygulamada tanımlanan _path operation_’lar ile o alt uygulama yönetir. + +### Üst seviye uygulama { #top-level-application } + +Önce ana, üst seviye **FastAPI** uygulamasını ve onun *path operation*’larını oluşturun: + +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[3, 6:8] *} + +### Alt uygulama { #sub-application } + +Sonra alt uygulamanızı ve onun *path operation*’larını oluşturun. + +Bu alt uygulama da standart bir FastAPI uygulamasıdır; ancak "mount" edilecek olan budur: + +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 14:16] *} + +### Alt uygulamayı mount edin { #mount-the-sub-application } + +Üst seviye uygulamanızda (`app`), alt uygulama `subapi`’yi mount edin. + +Bu örnekte `/subapi` path’ine mount edilecektir: + +{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 19] *} + +### Otomatik API dokümanlarını kontrol edin { #check-the-automatic-api-docs } + +Şimdi dosyanızla birlikte `fastapi` komutunu çalıştırın: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Ardından http://127.0.0.1:8000/docs adresinden dokümanları açın. + +Ana uygulama için otomatik API dokümanlarını göreceksiniz; yalnızca onun kendi _path operation_’larını içerir: + + + +Sonra alt uygulamanın dokümanlarını http://127.0.0.1:8000/subapi/docs adresinden açın. + +Alt uygulama için otomatik API dokümanlarını göreceksiniz; yalnızca onun kendi _path operation_’larını içerir ve hepsi doğru alt-path öneki `/subapi` altında yer alır: + + + +İki arayüzden herhangi biriyle etkileşime girmeyi denerseniz doğru şekilde çalıştıklarını görürsünüz; çünkü tarayıcı her bir uygulama ya da alt uygulama ile ayrı ayrı iletişim kurabilir. + +### Teknik Detaylar: `root_path` { #technical-details-root-path } + +Yukarıda anlatıldığı gibi bir alt uygulamayı mount ettiğinizde FastAPI, ASGI spesifikasyonundaki `root_path` adlı bir mekanizmayı kullanarak alt uygulamaya mount path’ini iletmeyi otomatik olarak yönetir. + +Bu sayede alt uygulama, dokümantasyon arayüzü için o path önekini kullanması gerektiğini bilir. + +Ayrıca alt uygulamanın kendi mount edilmiş alt uygulamaları da olabilir; FastAPI tüm bu `root_path`’leri otomatik olarak yönettiği için her şey doğru şekilde çalışır. + +`root_path` hakkında daha fazlasını ve bunu açıkça nasıl kullanacağınızı [Behind a Proxy](behind-a-proxy.md){.internal-link target=_blank} bölümünde öğreneceksiniz. diff --git a/docs/tr/docs/advanced/templates.md b/docs/tr/docs/advanced/templates.md new file mode 100644 index 000000000..b91e0a2a8 --- /dev/null +++ b/docs/tr/docs/advanced/templates.md @@ -0,0 +1,126 @@ +# Şablonlar { #templates } + +**FastAPI** ile istediğiniz herhangi bir template engine'i kullanabilirsiniz. + +Yaygın bir tercih, Flask ve diğer araçların da kullandığı Jinja2'dir. + +Bunu kolayca yapılandırmak için, doğrudan **FastAPI** uygulamanızda kullanabileceğiniz yardımcı araçlar vardır (Starlette tarafından sağlanır). + +## Bağımlılıkları Yükleme { #install-dependencies } + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, etkinleştirdiğinizden ve `jinja2`'yi yüklediğinizden emin olun: + +
+ +```console +$ pip install jinja2 + +---> 100% +``` + +
+ +## `Jinja2Templates` Kullanımı { #using-jinja2templates } + +* `Jinja2Templates`'ı içe aktarın. +* Daha sonra tekrar kullanabileceğiniz bir `templates` nesnesi oluşturun. +* Template döndürecek *path operation* içinde bir `Request` parametresi tanımlayın. +* Oluşturduğunuz `templates` nesnesini kullanarak bir `TemplateResponse` render edip döndürün; template'in adını, request nesnesini ve Jinja2 template'i içinde kullanılacak anahtar-değer çiftlerini içeren bir "context" sözlüğünü (dict) iletin. + +{* ../../docs_src/templates/tutorial001_py39.py hl[4,11,15:18] *} + +/// note | Not + +FastAPI 0.108.0 ve Starlette 0.29.0 öncesinde, ilk parametre `name` idi. + +Ayrıca, daha önceki sürümlerde `request` nesnesi, Jinja2 için context içindeki anahtar-değer çiftlerinin bir parçası olarak geçirilirdi. + +/// + +/// tip | İpucu + +`response_class=HTMLResponse` olarak tanımlarsanız doküman arayüzü (docs UI) response'un HTML olacağını anlayabilir. + +/// + +/// note | Teknik Detaylar + +`from starlette.templating import Jinja2Templates` da kullanabilirsiniz. + +**FastAPI**, geliştirici için kolaylık olması adına `starlette.templating` içeriğini `fastapi.templating` olarak da sunar. Ancak mevcut response'ların çoğu doğrudan Starlette'ten gelir. `Request` ve `StaticFiles` için de aynı durum geçerlidir. + +/// + +## Template Yazma { #writing-templates } + +Ardından örneğin `templates/item.html` konumunda bir template yazabilirsiniz: + +```jinja hl_lines="7" +{!../../docs_src/templates/templates/item.html!} +``` + +### Template Context Değerleri { #template-context-values } + +Şu HTML içeriğinde: + +{% raw %} + +```jinja +Item ID: {{ id }} +``` + +{% endraw %} + +...gösterilecek olan `id`, sizin "context" olarak ilettiğiniz `dict` içinden alınır: + +```Python +{"id": id} +``` + +Örneğin ID değeri `42` ise, şu şekilde render edilir: + +```html +Item ID: 42 +``` + +### Template `url_for` Argümanları { #template-url-for-arguments } + +Template içinde `url_for()` da kullanabilirsiniz; argüman olarak, *path operation function*'ınızın kullandığı argümanların aynısını alır. + +Dolayısıyla şu bölüm: + +{% raw %} + +```jinja + +``` + +{% endraw %} + +...*path operation function* olan `read_item(id=id)` tarafından handle edilecek URL'nin aynısına bir link üretir. + +Örneğin ID değeri `42` ise, şu şekilde render edilir: + +```html + +``` + +## Template'ler ve statik dosyalar { #templates-and-static-files } + +Template içinde `url_for()` kullanabilir ve örneğin `name="static"` ile mount ettiğiniz `StaticFiles` ile birlikte kullanabilirsiniz. + +```jinja hl_lines="4" +{!../../docs_src/templates/templates/item.html!} +``` + +Bu örnekte, şu şekilde `static/styles.css` konumundaki bir CSS dosyasına link verir: + +```CSS hl_lines="4" +{!../../docs_src/templates/static/styles.css!} +``` + +Ve `StaticFiles` kullandığınız için, bu CSS dosyası **FastAPI** uygulamanız tarafından `/static/styles.css` URL'sinde otomatik olarak servis edilir. + +## Daha fazla detay { #more-details } + +Template'leri nasıl test edeceğiniz dahil daha fazla detay için Starlette'in template dokümantasyonuna bakın. diff --git a/docs/tr/docs/advanced/testing-dependencies.md b/docs/tr/docs/advanced/testing-dependencies.md new file mode 100644 index 000000000..3cad63776 --- /dev/null +++ b/docs/tr/docs/advanced/testing-dependencies.md @@ -0,0 +1,53 @@ +# Override Kullanarak Dependency'leri Test Etme { #testing-dependencies-with-overrides } + +## Test Sırasında Dependency Override Etme { #overriding-dependencies-during-testing } + +Test yazarken bazı durumlarda bir dependency'yi override etmek isteyebilirsiniz. + +Orijinal dependency'nin (ve varsa tüm alt dependency'lerinin) çalışmasını istemezsiniz. + +Bunun yerine, yalnızca testler sırasında (hatta belki sadece belirli bazı testlerde) kullanılacak farklı bir dependency sağlarsınız; böylece orijinal dependency'nin ürettiği değerin kullanıldığı yerde, test için üretilen değeri kullanabilirsiniz. + +### Kullanım Senaryoları: Harici Servis { #use-cases-external-service } + +Örneğin, çağırmanız gereken harici bir authentication provider'ınız olabilir. + +Ona bir token gönderirsiniz ve o da authenticated bir user döndürür. + +Bu provider request başına ücret alıyor olabilir ve onu çağırmak, testlerde sabit bir mock user kullanmaya kıyasla daha fazla zaman alabilir. + +Muhtemelen harici provider'ı bir kez test etmek istersiniz; ancak çalışan her testte onu çağırmanız şart değildir. + +Bu durumda, o provider'ı çağıran dependency'yi override edebilir ve yalnızca testleriniz için mock user döndüren özel bir dependency kullanabilirsiniz. + +### `app.dependency_overrides` Attribute'ünü Kullanın { #use-the-app-dependency-overrides-attribute } + +Bu tür durumlar için **FastAPI** uygulamanızda `app.dependency_overrides` adında bir attribute bulunur; bu basit bir `dict`'tir. + +Test için bir dependency'yi override etmek istediğinizde, key olarak orijinal dependency'yi (bir function), value olarak da override edecek dependency'nizi (başka bir function) verirsiniz. + +Böylece **FastAPI**, orijinal dependency yerine bu override'ı çağırır. + +{* ../../docs_src/dependency_testing/tutorial001_an_py310.py hl[26:27,30] *} + +/// tip | İpucu + +**FastAPI** uygulamanızın herhangi bir yerinde kullanılan bir dependency için override tanımlayabilirsiniz. + +Orijinal dependency bir *path operation function* içinde, bir *path operation decorator* içinde (return value kullanmadığınız durumlarda), bir `.include_router()` çağrısında, vb. kullanılıyor olabilir. + +FastAPI yine de onu override edebilir. + +/// + +Sonrasında override'larınızı (yani kaldırıp sıfırlamayı) `app.dependency_overrides` değerini boş bir `dict` yaparak gerçekleştirebilirsiniz: + +```Python +app.dependency_overrides = {} +``` + +/// tip | İpucu + +Bir dependency'yi yalnızca bazı testler sırasında override etmek istiyorsanız, override'ı testin başında (test function'ının içinde) ayarlayıp testin sonunda (yine test function'ının sonunda) sıfırlayabilirsiniz. + +/// diff --git a/docs/tr/docs/advanced/testing-events.md b/docs/tr/docs/advanced/testing-events.md new file mode 100644 index 000000000..f12aef988 --- /dev/null +++ b/docs/tr/docs/advanced/testing-events.md @@ -0,0 +1,12 @@ +# Event'leri Test Etme: lifespan ve startup - shutdown { #testing-events-lifespan-and-startup-shutdown } + +Test'lerinizde `lifespan`'ın çalışması gerektiğinde, `TestClient`'ı bir `with` ifadesiyle kullanabilirsiniz: + +{* ../../docs_src/app_testing/tutorial004_py39.py hl[9:15,18,27:28,30:32,41:43] *} + + +Bu konuda daha fazla ayrıntıyı resmi Starlette dokümantasyon sitesindeki ["Running lifespan in tests in the official Starlette documentation site."](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) bölümünde okuyabilirsiniz. + +Kullanımdan kaldırılmış `startup` ve `shutdown` event'leri için ise `TestClient`'ı aşağıdaki gibi kullanabilirsiniz: + +{* ../../docs_src/app_testing/tutorial003_py39.py hl[9:12,20:24] *} diff --git a/docs/tr/docs/advanced/using-request-directly.md b/docs/tr/docs/advanced/using-request-directly.md new file mode 100644 index 000000000..3efdafb03 --- /dev/null +++ b/docs/tr/docs/advanced/using-request-directly.md @@ -0,0 +1,56 @@ +# Request'i Doğrudan Kullanmak { #using-the-request-directly } + +Şu ana kadar, ihtiyacınız olan request parçalarını tipleriyle birlikte tanımlıyordunuz. + +Verileri şuradan alarak: + +* path'ten parameter olarak. +* Header'lardan. +* Cookie'lerden. +* vb. + +Bunu yaptığınızda **FastAPI**, bu verileri doğrular (validate eder), dönüştürür ve API'niz için dokümantasyonu otomatik olarak üretir. + +Ancak bazı durumlarda `Request` nesnesine doğrudan erişmeniz gerekebilir. + +## `Request` nesnesi hakkında detaylar { #details-about-the-request-object } + +**FastAPI** aslında altta **Starlette** çalıştırır ve üstüne çeşitli araçlardan oluşan bir katman ekler. Bu yüzden gerektiğinde Starlette'in `Request` nesnesini doğrudan kullanabilirsiniz. + +Bu ayrıca şu anlama gelir: `Request` nesnesinden veriyi doğrudan alırsanız (örneğin body'yi okursanız) FastAPI bu veriyi doğrulamaz, dönüştürmez veya dokümante etmez (otomatik API arayüzü için OpenAPI ile). + +Buna rağmen normal şekilde tanımladığınız diğer herhangi bir parameter (örneğin Pydantic model ile body) yine doğrulanır, dönüştürülür, annotate edilir, vb. + +Ama bazı özel durumlarda `Request` nesnesini almak faydalıdır. + +## `Request` nesnesini doğrudan kullanın { #use-the-request-object-directly } + +*Path operation function* içinde client'ın IP adresini/host'unu almak istediğinizi düşünelim. + +Bunun için request'e doğrudan erişmeniz gerekir. + +{* ../../docs_src/using_request_directly/tutorial001_py39.py hl[1,7:8] *} + +Tipi `Request` olan bir *path operation function* parameter'ı tanımladığınızda **FastAPI**, o parameter'a `Request` nesnesini geçmesi gerektiğini anlar. + +/// tip | İpucu + +Bu örnekte, request parameter'ının yanında bir path parameter'ı da tanımladığımıza dikkat edin. + +Dolayısıyla path parameter'ı çıkarılır, doğrulanır, belirtilen tipe dönüştürülür ve OpenAPI ile annotate edilir. + +Aynı şekilde, diğer parameter'ları normal biçimde tanımlamaya devam edip buna ek olarak `Request` de alabilirsiniz. + +/// + +## `Request` dokümantasyonu { #request-documentation } + +Resmi Starlette dokümantasyon sitesinde `Request` nesnesiyle ilgili daha fazla detayı okuyabilirsiniz. + +/// note | Teknik Detaylar + +`from starlette.requests import Request` de kullanabilirsiniz. + +**FastAPI** bunu size (geliştiriciye) kolaylık olsun diye doğrudan sunar. Ancak kendisi doğrudan Starlette'ten gelir. + +/// diff --git a/docs/tr/docs/advanced/websockets.md b/docs/tr/docs/advanced/websockets.md new file mode 100644 index 000000000..775d7cc25 --- /dev/null +++ b/docs/tr/docs/advanced/websockets.md @@ -0,0 +1,186 @@ +# WebSockets { #websockets } + +**FastAPI** ile WebSockets kullanabilirsiniz. + +## `websockets` Kurulumu { #install-websockets } + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu aktive ettiğinizden ve `websockets`'i ("WebSocket" protokolünü kullanmayı kolaylaştıran bir Python kütüphanesi) kurduğunuzdan emin olun: + +
+ +```console +$ pip install websockets + +---> 100% +``` + +
+ +## WebSockets client { #websockets-client } + +### Production'da { #in-production } + +Production sisteminizde muhtemelen React, Vue.js veya Angular gibi modern bir framework ile oluşturulmuş bir frontend vardır. + +WebSockets kullanarak backend'inizle iletişim kurmak için de büyük ihtimalle frontend'inizin sağladığı yardımcı araçları kullanırsınız. + +Ya da native kod ile doğrudan WebSocket backend'inizle iletişim kuran native bir mobil uygulamanız olabilir. + +Veya WebSocket endpoint'i ile iletişim kurmak için başka herhangi bir yönteminizi de kullanıyor olabilirsiniz. + +--- + +Ancak bu örnek için, tamamı uzun bir string içinde olacak şekilde biraz JavaScript içeren çok basit bir HTML dokümanı kullanacağız. + +Elbette bu optimal değil ve production için kullanmazsınız. + +Production'da yukarıdaki seçeneklerden birini kullanırsınız. + +Ama WebSockets'in server tarafına odaklanmak ve çalışan bir örnek görmek için en basit yol bu: + +{* ../../docs_src/websockets/tutorial001_py39.py hl[2,6:38,41:43] *} + +## Bir `websocket` Oluşturun { #create-a-websocket } + +**FastAPI** uygulamanızda bir `websocket` oluşturun: + +{* ../../docs_src/websockets/tutorial001_py39.py hl[1,46:47] *} + +/// note | Teknik Detaylar + +`from starlette.websockets import WebSocket` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak işinizi kolaylaştırmak için aynı `WebSocket`'i doğrudan sağlar. Ancak aslında doğrudan Starlette'ten gelir. + +/// + +## Mesajları `await` Edin ve Mesaj Gönderin { #await-for-messages-and-send-messages } + +WebSocket route'unuzda mesajları `await` edebilir ve mesaj gönderebilirsiniz. + +{* ../../docs_src/websockets/tutorial001_py39.py hl[48:52] *} + +Binary, text ve JSON verisi alıp gönderebilirsiniz. + +## Deneyin { #try-it } + +Dosyanızın adı `main.py` ise uygulamanızı şu şekilde çalıştırın: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Tarayıcınızda http://127.0.0.1:8000 adresini açın. + +Şuna benzer basit bir sayfa göreceksiniz: + + + +Input kutusuna mesaj yazıp gönderebilirsiniz: + + + +Ve WebSockets kullanan **FastAPI** uygulamanız yanıt döndürecektir: + + + +Birçok mesaj gönderebilir (ve alabilirsiniz): + + + +Ve hepsinde aynı WebSocket bağlantısı kullanılacaktır. + +## `Depends` ve Diğerlerini Kullanma { #using-depends-and-others } + +WebSocket endpoint'lerinde `fastapi` içinden import edip şunları kullanabilirsiniz: + +* `Depends` +* `Security` +* `Cookie` +* `Header` +* `Path` +* `Query` + +Diğer FastAPI endpoint'leri/*path operations* ile aynı şekilde çalışırlar: + +{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} + +/// info | Bilgi + +Bu bir WebSocket olduğu için `HTTPException` raise etmek pek anlamlı değildir; bunun yerine `WebSocketException` raise ederiz. + +Spesifikasyonda tanımlanan geçerli kodlar arasından bir kapatma kodu kullanabilirsiniz. + +/// + +### Dependency'lerle WebSockets'i Deneyin { #try-the-websockets-with-dependencies } + +Dosyanızın adı `main.py` ise uygulamanızı şu şekilde çalıştırın: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Tarayıcınızda http://127.0.0.1:8000 adresini açın. + +Burada şunları ayarlayabilirsiniz: + +* path'te kullanılan "Item ID". +* query parametresi olarak kullanılan "Token". + +/// tip | İpucu + +query'deki `token` değerinin bir dependency tarafından ele alınacağına dikkat edin. + +/// + +Bununla WebSocket'e bağlanabilir, ardından mesaj gönderip alabilirsiniz: + + + +## Bağlantı Kopmalarını ve Birden Fazla Client'ı Yönetme { #handling-disconnections-and-multiple-clients } + +Bir WebSocket bağlantısı kapandığında, `await websocket.receive_text()` bir `WebSocketDisconnect` exception'ı raise eder; ardından bunu bu örnekteki gibi yakalayıp (catch) yönetebilirsiniz. + +{* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *} + +Denemek için: + +* Uygulamayı birden fazla tarayıcı sekmesiyle açın. +* Bu sekmelerden mesaj yazın. +* Sonra sekmelerden birini kapatın. + +Bu, `WebSocketDisconnect` exception'ını raise eder ve diğer tüm client'lar şuna benzer bir mesaj alır: + +``` +Client #1596980209979 left the chat +``` + +/// tip | İpucu + +Yukarıdaki uygulama, birden fazla WebSocket bağlantısına mesajları nasıl yönetip broadcast edeceğinizi göstermek için minimal ve basit bir örnektir. + +Ancak her şey memory'de, tek bir list içinde yönetildiği için yalnızca process çalıştığı sürece ve yalnızca tek bir process ile çalışacaktır. + +FastAPI ile kolay entegre olan ama Redis, PostgreSQL vb. tarafından desteklenen daha sağlam bir şeye ihtiyacınız varsa encode/broadcaster'a göz atın. + +/// + +## Daha Fazla Bilgi { #more-info } + +Seçenekler hakkında daha fazlasını öğrenmek için Starlette dokümantasyonunda şunlara bakın: + +* `WebSocket` class'ı. +* Class tabanlı WebSocket yönetimi. diff --git a/docs/tr/docs/deployment/concepts.md b/docs/tr/docs/deployment/concepts.md new file mode 100644 index 000000000..d0f568146 --- /dev/null +++ b/docs/tr/docs/deployment/concepts.md @@ -0,0 +1,321 @@ +# Deployment Kavramları { #deployments-concepts } + +Bir **FastAPI** uygulamasını (hatta genel olarak herhangi bir web API'yi) deploy ederken, muhtemelen önemseyeceğiniz bazı kavramlar vardır. Bu kavramları kullanarak, **uygulamanızı deploy etmek** için **en uygun** yöntemi bulabilirsiniz. + +Önemli kavramlardan bazıları şunlardır: + +* Güvenlik - HTTPS +* Startup'ta çalıştırma +* Yeniden başlatmalar +* Replikasyon (çalışan process sayısı) +* Bellek +* Başlatmadan önceki adımlar + +Bunların **deployment**'ları nasıl etkilediğine bakalım. + +Nihai hedef, **API client**'larınıza **güvenli** bir şekilde hizmet verebilmek, **kesintileri** önlemek ve **hesaplama kaynaklarını** (ör. uzak server'lar/sanal makineler) olabildiğince verimli kullanmaktır. + +Burada bu **kavramlar** hakkında biraz daha bilgi vereceğim. Böylece, çok farklı ortamlarda—hatta bugün var olmayan **gelecekteki** ortamlarda bile—API'nizi nasıl deploy edeceğinize karar verirken ihtiyaç duyacağınız **sezgiyi** kazanmış olursunuz. + +Bu kavramları dikkate alarak, **kendi API**'leriniz için en iyi deployment yaklaşımını **değerlendirebilir ve tasarlayabilirsiniz**. + +Sonraki bölümlerde, FastAPI uygulamalarını deploy etmek için daha **somut tarifler** (recipes) paylaşacağım. + +Ama şimdilik, bu önemli **kavramsal fikirleri** inceleyelim. Bu kavramlar diğer tüm web API türleri için de geçerlidir. + +## Güvenlik - HTTPS { #security-https } + +[HTTPS hakkındaki önceki bölümde](https.md){.internal-link target=_blank} HTTPS'in API'niz için nasıl şifreleme sağladığını öğrenmiştik. + +Ayrıca HTTPS'in genellikle uygulama server'ınızın **dışında** yer alan bir bileşen tarafından sağlandığını, yani bir **TLS Termination Proxy** ile yapıldığını da görmüştük. + +Ve **HTTPS sertifikalarını yenilemekten** sorumlu bir şey olmalıdır; bu aynı bileşen olabileceği gibi farklı bir bileşen de olabilir. + +### HTTPS için Örnek Araçlar { #example-tools-for-https } + +TLS Termination Proxy olarak kullanabileceğiniz bazı araçlar: + +* Traefik + * Sertifika yenilemelerini otomatik yönetir +* Caddy + * Sertifika yenilemelerini otomatik yönetir +* Nginx + * Sertifika yenilemeleri için Certbot gibi harici bir bileşenle +* HAProxy + * Sertifika yenilemeleri için Certbot gibi harici bir bileşenle +* Nginx gibi bir Ingress Controller ile Kubernetes + * Sertifika yenilemeleri için cert-manager gibi harici bir bileşenle +* Bir cloud provider tarafından servislerinin parçası olarak içeride yönetilmesi (aşağıyı okuyun) + +Bir diğer seçenek de, HTTPS kurulumunu da dahil olmak üzere işin daha büyük kısmını yapan bir **cloud service** kullanmaktır. Bunun bazı kısıtları olabilir veya daha pahalı olabilir vb. Ancak bu durumda TLS Termination Proxy'yi kendiniz kurmak zorunda kalmazsınız. + +Sonraki bölümlerde bazı somut örnekler göstereceğim. + +--- + +Sonraki kavramlar, gerçek API'nizi çalıştıran programla (ör. Uvicorn) ilgilidir. + +## Program ve Process { #program-and-process } + +Çalışan "**process**" hakkında çok konuşacağız. Bu yüzden ne anlama geldiğini ve "**program**" kelimesinden farkının ne olduğunu netleştirmek faydalı. + +### Program Nedir { #what-is-a-program } + +**Program** kelimesi günlük kullanımda birçok şeyi anlatmak için kullanılır: + +* Yazdığınız **code**, yani **Python dosyaları**. +* İşletim sistemi tarafından **çalıştırılabilen** **dosya**, örn: `python`, `python.exe` veya `uvicorn`. +* İşletim sistemi üzerinde **çalışır durumdayken** CPU kullanan ve bellekte veri tutan belirli bir program. Buna **process** de denir. + +### Process Nedir { #what-is-a-process } + +**Process** kelimesi genellikle daha spesifik kullanılır; yalnızca işletim sistemi üzerinde çalışan şeye (yukarıdaki son madde gibi) işaret eder: + +* İşletim sistemi üzerinde **çalışır durumda** olan belirli bir program. + * Bu; dosyayı ya da code'u değil, işletim sistemi tarafından **çalıştırılan** ve yönetilen şeyi ifade eder. +* Herhangi bir program, herhangi bir code, **yalnızca çalıştırılırken** bir şey yapabilir. Yani bir **process çalışıyorken**. +* Process siz tarafından veya işletim sistemi tarafından **sonlandırılabilir** (ya da "killed" edilebilir). O anda çalışması/çalıştırılması durur ve artık **hiçbir şey yapamaz**. +* Bilgisayarınızda çalışan her uygulamanın arkasında bir process vardır; çalışan her program, her pencere vb. Bilgisayar açıkken normalde **aynı anda** birçok process çalışır. +* Aynı anda **aynı programın birden fazla process**'i çalışabilir. + +İşletim sisteminizdeki "task manager" veya "system monitor" (ya da benzeri araçlar) ile bu process'lerin birçoğunu çalışır halde görebilirsiniz. + +Örneğin muhtemelen aynı browser programını (Firefox, Chrome, Edge vb.) çalıştıran birden fazla process göreceksiniz. Genelde her tab için bir process, üstüne bazı ek process'ler çalıştırırlar. + + + +--- + +Artık **process** ve **program** arasındaki farkı bildiğimize göre, deployment konusuna devam edelim. + +## Startup'ta Çalıştırma { #running-on-startup } + +Çoğu durumda bir web API oluşturduğunuzda, client'larınızın her zaman erişebilmesi için API'nizin kesintisiz şekilde **sürekli çalışıyor** olmasını istersiniz. Elbette sadece belirli durumlarda çalışmasını istemenizin özel bir sebebi olabilir; ancak çoğunlukla onu sürekli açık ve **kullanılabilir** halde tutarsınız. + +### Uzak Bir Server'da { #in-a-remote-server } + +Uzak bir server (cloud server, sanal makine vb.) kurduğunuzda, yapabileceğiniz en basit şey; local geliştirme sırasında yaptığınız gibi, manuel olarak `fastapi run` (Uvicorn'u kullanır) veya benzeri bir komutla çalıştırmaktır. + +Bu yöntem çalışır ve **geliştirme sırasında** faydalıdır. + +Ancak server'a olan bağlantınız koparsa, **çalışan process** muhtemelen ölür. + +Ve server yeniden başlatılırsa (örneğin update'lerden sonra ya da cloud provider'ın migration'larından sonra) bunu muhtemelen **fark etmezsiniz**. Dolayısıyla process'i manuel yeniden başlatmanız gerektiğini de bilmezsiniz. Sonuçta API'niz ölü kalır. + +### Startup'ta Otomatik Çalıştırma { #run-automatically-on-startup } + +Genellikle server programının (ör. Uvicorn) server açılışında otomatik başlamasını ve herhangi bir **insan müdahalesi** gerektirmeden API'nizi çalıştıran bir process'in sürekli ayakta olmasını istersiniz (ör. FastAPI uygulamanızı çalıştıran Uvicorn). + +### Ayrı Bir Program { #separate-program } + +Bunu sağlamak için genellikle startup'ta uygulamanızın çalıştığından emin olacak **ayrı bir program** kullanırsınız. Pek çok durumda bu program, örneğin bir veritabanı gibi diğer bileşenlerin/uygulamaların da çalıştığından emin olur. + +### Startup'ta Çalıştırmak için Örnek Araçlar { #example-tools-to-run-at-startup } + +Bu işi yapabilen araçlara örnekler: + +* Docker +* Kubernetes +* Docker Compose +* Docker in Swarm Mode +* Systemd +* Supervisor +* Bir cloud provider tarafından servislerinin parçası olarak içeride yönetilmesi +* Diğerleri... + +Sonraki bölümlerde daha somut örnekler vereceğim. + +## Yeniden Başlatmalar { #restarts } + +Uygulamanızın startup'ta çalıştığından emin olmaya benzer şekilde, hatalardan sonra **yeniden başlatıldığından** da emin olmak istersiniz. + +### Hata Yaparız { #we-make-mistakes } + +Biz insanlar sürekli **hata** yaparız. Yazılımın neredeyse *her zaman* farklı yerlerinde gizli **bug**'lar vardır. + +Ve biz geliştiriciler bu bug'ları buldukça ve yeni özellikler ekledikçe code'u iyileştiririz (muhtemelen yeni bug'lar da ekleyerek). + +### Küçük Hatalar Otomatik Yönetilir { #small-errors-automatically-handled } + +FastAPI ile web API geliştirirken, code'umuzda bir hata olursa FastAPI genellikle bunu hatayı tetikleyen tek request ile sınırlar. + +Client o request için **500 Internal Server Error** alır; ancak uygulama tamamen çöküp durmak yerine sonraki request'ler için çalışmaya devam eder. + +### Daha Büyük Hatalar - Çökmeler { #bigger-errors-crashes } + +Yine de bazı durumlarda, yazdığımız bir code **tüm uygulamayı çökertip** Uvicorn ve Python'ın crash olmasına neden olabilir. + +Böyle bir durumda, tek bir noktadaki hata yüzünden uygulamanın ölü kalmasını istemezsiniz; bozuk olmayan *path operations* en azından çalışmaya devam etsin istersiniz. + +### Crash Sonrası Yeniden Başlatma { #restart-after-crash } + +Ancak çalışan **process**'i çökerten gerçekten kötü hatalarda, process'i **yeniden başlatmaktan** sorumlu harici bir bileşen istersiniz; en azından birkaç kez... + +/// tip | İpucu + +...Yine de uygulama **hemen crash oluyorsa**, onu sonsuza kadar yeniden başlatmaya çalışmanın pek anlamı yoktur. Böyle durumları büyük ihtimalle geliştirme sırasında ya da en geç deploy'dan hemen sonra fark edersiniz. + +O yüzden ana senaryoya odaklanalım: Gelecekte bazı özel durumlarda tamamen çökebilir ve yine de yeniden başlatmak mantıklıdır. + +/// + +Uygulamanızı yeniden başlatmakla görevli bileşenin **harici bir bileşen** olmasını istersiniz. Çünkü o noktada Uvicorn ve Python ile birlikte aynı uygulama zaten crash olmuştur; aynı app'in içindeki aynı code'un bunu düzeltmek için yapabileceği bir şey kalmaz. + +### Otomatik Yeniden Başlatma için Örnek Araçlar { #example-tools-to-restart-automatically } + +Çoğu durumda, **startup'ta programı çalıştırmak** için kullanılan aracın aynısı otomatik **restart**'ları yönetmek için de kullanılır. + +Örneğin bu şunlarla yönetilebilir: + +* Docker +* Kubernetes +* Docker Compose +* Docker in Swarm Mode +* Systemd +* Supervisor +* Bir cloud provider tarafından servislerinin parçası olarak içeride yönetilmesi +* Diğerleri... + +## Replikasyon - Process'ler ve Bellek { #replication-processes-and-memory } + +FastAPI uygulamasında, Uvicorn'u çalıştıran `fastapi` komutu gibi bir server programı kullanırken, uygulamayı **tek bir process** içinde bir kez çalıştırmak bile aynı anda birden fazla client'a hizmet verebilir. + +Ancak birçok durumda, aynı anda birden fazla worker process çalıştırmak istersiniz. + +### Birden Fazla Process - Worker'lar { #multiple-processes-workers } + +Tek bir process'in karşılayabileceğinden daha fazla client'ınız varsa (örneğin sanal makine çok büyük değilse) ve server CPU'sunda **birden fazla core** varsa, aynı uygulamayla **birden fazla process** çalıştırıp tüm request'leri bunlara dağıtabilirsiniz. + +Aynı API programının **birden fazla process**'ini çalıştırdığınızda, bunlara genellikle **worker** denir. + +### Worker Process'ler ve Port'lar { #worker-processes-and-ports } + +[HTTPS hakkındaki dokümanda](https.md){.internal-link target=_blank} bir server'da aynı port ve IP adresi kombinasyonunu yalnızca tek bir process'in dinleyebileceğini hatırlıyor musunuz? + +Bu hâlâ geçerli. + +Dolayısıyla **aynı anda birden fazla process** çalıştırabilmek için, **port** üzerinde dinleyen **tek bir process** olmalı ve bu process iletişimi bir şekilde worker process'lere aktarmalıdır. + +### Process Başına Bellek { #memory-per-process } + +Program belleğe bir şeyler yüklediğinde—örneğin bir değişkende bir machine learning modelini veya büyük bir dosyanın içeriğini tutmak gibi—bunların hepsi server'ın **belleğini (RAM)** tüketir. + +Ve birden fazla process normalde **belleği paylaşmaz**. Yani her çalışan process'in kendi verileri, değişkenleri ve belleği vardır. Code'unuz çok bellek tüketiyorsa, **her process** buna denk bir miktar bellek tüketir. + +### Server Belleği { #server-memory } + +Örneğin code'unuz **1 GB** boyutunda bir Machine Learning modelini yüklüyorsa, API'niz tek process ile çalışırken en az 1 GB RAM tüketir. **4 process** (4 worker) başlatırsanız her biri 1 GB RAM tüketir. Yani toplamda API'niz **4 GB RAM** tüketir. + +Uzak server'ınız veya sanal makineniz yalnızca 3 GB RAM'e sahipse, 4 GB'tan fazla RAM yüklemeye çalışmak sorun çıkarır. + +### Birden Fazla Process - Bir Örnek { #multiple-processes-an-example } + +Bu örnekte, iki adet **Worker Process** başlatıp kontrol eden bir **Manager Process** vardır. + +Bu Manager Process büyük ihtimalle IP üzerindeki **port**'u dinleyen süreçtir ve tüm iletişimi worker process'lere aktarır. + +Worker process'ler uygulamanızı çalıştıran process'lerdir; bir **request** alıp bir **response** döndürmek için asıl hesaplamaları yaparlar ve sizin RAM'de değişkenlere koyduğunuz her şeyi yüklerler. + + + +Elbette aynı makinede, uygulamanız dışında da muhtemelen **başka process**'ler çalışır. + +İlginç bir detay: Her process'in kullandığı **CPU** yüzdesi zaman içinde çok **değişken** olabilir; ancak **bellek (RAM)** genellikle az çok **stabil** kalır. + +Eğer API'niz her seferinde benzer miktarda hesaplama yapıyorsa ve çok sayıda client'ınız varsa, **CPU kullanımı** da muhtemelen *stabil olur* (hızlı hızlı sürekli yükselip alçalmak yerine). + +### Replikasyon Araçları ve Stratejileri Örnekleri { #examples-of-replication-tools-and-strategies } + +Bunu başarmak için farklı yaklaşımlar olabilir. Sonraki bölümlerde, örneğin Docker ve container'lar konuşurken, belirli stratejileri daha detaylı anlatacağım. + +Dikkate almanız gereken ana kısıt şudur: **public IP** üzerindeki **port**'u yöneten **tek** bir bileşen olmalı. Sonrasında bu bileşenin, replikasyonla çoğaltılmış **process/worker**'lara iletişimi **aktarmanın** bir yoluna sahip olması gerekir. + +Olası kombinasyonlar ve stratejiler: + +* `--workers` ile **Uvicorn** + * Bir Uvicorn **process manager** **IP** ve **port** üzerinde dinler ve **birden fazla Uvicorn worker process** başlatır. +* **Kubernetes** ve diğer dağıtık **container sistemleri** + * **Kubernetes** katmanında bir şey **IP** ve **port** üzerinde dinler. Replikasyon, her birinde **tek bir Uvicorn process** çalışan **birden fazla container** ile yapılır. +* Bunu sizin yerinize yapan **cloud service**'ler + * Cloud service muhtemelen **replikasyonu sizin yerinize yönetir**. Size çalıştırılacak **bir process** veya kullanılacak bir **container image** tanımlama imkânı verebilir; her durumda büyük ihtimalle **tek bir Uvicorn process** olur ve bunu çoğaltmaktan cloud service sorumlu olur. + +/// tip | İpucu + +**Container**, Docker veya Kubernetes ile ilgili bazı maddeler şimdilik çok anlamlı gelmiyorsa dert etmeyin. + +Container image'ları, Docker, Kubernetes vb. konuları ilerideki bir bölümde daha detaylı anlatacağım: [Container'larda FastAPI - Docker](docker.md){.internal-link target=_blank}. + +/// + +## Başlatmadan Önceki Adımlar { #previous-steps-before-starting } + +Uygulamanızı **başlatmadan önce** bazı adımlar yapmak isteyeceğiniz birçok durum vardır. + +Örneğin **database migrations** çalıştırmak isteyebilirsiniz. + +Ancak çoğu durumda, bu adımları yalnızca **bir kez** çalıştırmak istersiniz. + +Bu yüzden, uygulamayı başlatmadan önce bu **ön adımları** çalıştıracak **tek bir process** olmasını istersiniz. + +Ve daha sonra uygulamanın kendisi için **birden fazla process** (birden fazla worker) başlatsanız bile, bu ön adımları çalıştıranın *yine* tek process olduğundan emin olmalısınız. Bu adımlar **birden fazla process** tarafından çalıştırılsaydı, işi **paralel** şekilde tekrarlarlardı. Adımlar database migration gibi hassas bir şeyse, birbirleriyle çakışıp çatışma çıkarabilirler. + +Elbette bazı durumlarda ön adımları birden fazla kez çalıştırmak sorun değildir; bu durumda yönetmesi çok daha kolay olur. + +/// tip | İpucu + +Ayrıca, kurulumunuza bağlı olarak bazı durumlarda uygulamanızı başlatmadan önce **hiç ön adıma ihtiyaç duymayabilirsiniz**. + +Bu durumda bunların hiçbirini düşünmeniz gerekmez. + +/// + +### Ön Adımlar için Strateji Örnekleri { #examples-of-previous-steps-strategies } + +Bu konu, **sisteminizi nasıl deploy ettiğinize** çok bağlıdır ve muhtemelen programları nasıl başlattığınız, restart'ları nasıl yönettiğiniz vb. ile bağlantılıdır. + +Bazı olası fikirler: + +* Kubernetes'te, app container'ınızdan önce çalışan bir "Init Container" +* Ön adımları çalıştırıp sonra uygulamanızı başlatan bir bash script + * Yine de o bash script'i başlatmak/restart etmek, hataları tespit etmek vb. için bir mekanizmaya ihtiyacınız olur. + +/// tip | İpucu + +Bunu container'larla nasıl yapabileceğinize dair daha somut örnekleri ilerideki bir bölümde anlatacağım: [Container'larda FastAPI - Docker](docker.md){.internal-link target=_blank}. + +/// + +## Kaynak Kullanımı { #resource-utilization } + +Server(lar)ınız bir **kaynaktır**. Programlarınızla CPU'lardaki hesaplama zamanını ve mevcut RAM belleğini tüketebilir veya **kullanabilirsiniz**. + +Sistem kaynaklarının ne kadarını tüketmek/kullanmak istersiniz? "Az" demek kolaydır; ancak pratikte hedef genellikle **çökmeden mümkün olduğunca fazla** kullanmaktır. + +3 server için para ödüyor ama onların RAM ve CPU'sunun yalnızca küçük bir kısmını kullanıyorsanız, muhtemelen **para israf ediyorsunuz** ve muhtemelen **elektrik tüketimini** de gereksiz yere artırıyorsunuz vb. + +Bu durumda 2 server ile devam edip onların kaynaklarını (CPU, bellek, disk, ağ bant genişliği vb.) daha yüksek oranlarda kullanmak daha iyi olabilir. + +Öte yandan, 2 server'ınız var ve CPU ile RAM'in **%100**'ünü kullanıyorsanız, bir noktada bir process daha fazla bellek ister; server diski "bellek" gibi kullanmak zorunda kalır (binlerce kat daha yavaş olabilir) ya da hatta **crash** edebilir. Ya da bir process bir hesaplama yapmak ister ve CPU tekrar boşalana kadar beklemek zorunda kalır. + +Bu senaryoda **bir server daha** eklemek ve bazı process'leri orada çalıştırmak daha iyi olur; böylece hepsinin **yeterli RAM'i ve CPU zamanı** olur. + +Ayrıca, herhangi bir sebeple API'nizde bir kullanım **spike**'ı olma ihtimali de vardır. Belki viral olur, belki başka servisler veya bot'lar kullanmaya başlar. Bu durumlarda güvende olmak için ekstra kaynak isteyebilirsiniz. + +Hedef olarak **keyfi bir sayı** belirleyebilirsiniz; örneğin kaynak kullanımını **%50 ile %90 arasında** tutmak gibi. Önemli olan, bunların muhtemelen ölçmek isteyeceğiniz ve deployment'larınızı ayarlamak için kullanacağınız ana metrikler olmasıdır. + +Server'ınızda CPU ve RAM kullanımını veya her process'in ne kadar kullandığını görmek için `htop` gibi basit araçları kullanabilirsiniz. Ya da server'lar arasında dağıtık çalışan daha karmaşık monitoring araçları kullanabilirsiniz. + +## Özet { #recap } + +Uygulamanızı nasıl deploy edeceğinize karar verirken aklınızda tutmanız gereken ana kavramların bazılarını okudunuz: + +* Güvenlik - HTTPS +* Startup'ta çalıştırma +* Yeniden başlatmalar +* Replikasyon (çalışan process sayısı) +* Bellek +* Başlatmadan önceki adımlar + +Bu fikirleri ve nasıl uygulayacağınızı anlamak, deployment'larınızı yapılandırırken ve ince ayar yaparken ihtiyaç duyacağınız sezgiyi kazanmanızı sağlamalıdır. + +Sonraki bölümlerde, izleyebileceğiniz stratejilere dair daha somut örnekler paylaşacağım. diff --git a/docs/tr/docs/deployment/docker.md b/docs/tr/docs/deployment/docker.md new file mode 100644 index 000000000..b48d2ddf5 --- /dev/null +++ b/docs/tr/docs/deployment/docker.md @@ -0,0 +1,620 @@ +# Container'larda FastAPI - Docker { #fastapi-in-containers-docker } + +FastAPI uygulamalarını deploy ederken yaygın bir yaklaşım, bir **Linux container image** oluşturmaktır. Bu genellikle **Docker** kullanılarak yapılır. Ardından bu container image'ı birkaç farklı yöntemden biriyle deploy edebilirsiniz. + +Linux container'ları kullanmanın **güvenlik**, **tekrarlanabilirlik**, **basitlik** gibi birçok avantajı vardır. + +/// tip | İpucu + +Aceleniz var ve bunları zaten biliyor musunuz? Aşağıdaki [`Dockerfile`'a atlayın 👇](#build-a-docker-image-for-fastapi). + +/// + +
+Dockerfile Önizleme 👀 + +```Dockerfile +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app + +CMD ["fastapi", "run", "app/main.py", "--port", "80"] + +# If running behind a proxy like Nginx or Traefik add --proxy-headers +# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"] +``` + +
+ +## Container Nedir { #what-is-a-container } + +Container'lar (özellikle Linux container'ları), bir uygulamayı tüm bağımlılıkları ve gerekli dosyalarıyla birlikte paketlemenin, aynı sistemdeki diğer container'lardan (diğer uygulama ya da bileşenlerden) izole tutarken yapılan, çok **hafif** bir yoludur. + +Linux container'ları, host'un (makine, sanal makine, cloud server vb.) aynı Linux kernel'ini kullanarak çalışır. Bu da, tüm bir işletim sistemini emüle eden tam sanal makinelere kıyasla çok daha hafif oldukları anlamına gelir. + +Bu sayede container'lar **az kaynak** tüketir; süreçleri doğrudan çalıştırmaya benzer bir seviyede (bir sanal makine çok daha fazla tüketirdi). + +Container'ların ayrıca kendi **izole** çalışan process'leri (çoğunlukla tek bir process), dosya sistemi ve ağı vardır. Bu da deployment, güvenlik, geliştirme vb. süreçleri kolaylaştırır. + +## Container Image Nedir { #what-is-a-container-image } + +Bir **container**, bir **container image**'dan çalıştırılır. + +Container image; bir container içinde bulunması gereken tüm dosyaların, environment variable'ların ve varsayılan komut/programın **statik** bir sürümüdür. Buradaki **statik**, container **image**'ının çalışmadığı, execute edilmediği; sadece paketlenmiş dosyalar ve metadata olduğu anlamına gelir. + +Depolanmış statik içerik olan "**container image**"ın aksine, "**container**" normalde çalışan instance'ı, yani **execute edilen** şeyi ifade eder. + +**Container** başlatılıp çalıştığında (bir **container image**'dan başlatılır), dosyalar oluşturabilir/değiştirebilir, environment variable'ları değiştirebilir vb. Bu değişiklikler sadece o container içinde kalır; alttaki container image'da kalıcı olmaz (diske kaydedilmez). + +Bir container image, **program** dosyası ve içeriklerine benzetilebilir; örn. `python` ve `main.py` gibi bir dosya. + +Ve **container**'ın kendisi (container image'a karşıt olarak) image'ın gerçek çalışan instance'ıdır; bir **process**'e benzer. Hatta bir container, yalnızca içinde **çalışan bir process** varken çalışır (ve genelde tek process olur). İçinde çalışan process kalmayınca container durur. + +## Container Image'lar { #container-images } + +Docker, **container image** ve **container** oluşturup yönetmek için kullanılan başlıca araçlardan biri olmuştur. + +Ayrıca birçok araç, ortam, veritabanı ve uygulama için önceden hazırlanmış **resmi container image**'ların bulunduğu herkese açık bir Docker Hub vardır. + +Örneğin, resmi bir Python Image bulunur. + +Ve veritabanları gibi farklı şeyler için de birçok image vardır; örneğin: + +* PostgreSQL +* MySQL +* MongoDB +* Redis, vb. + +Hazır bir container image kullanarak farklı araçları **birleştirmek** ve birlikte kullanmak çok kolaydır. Örneğin yeni bir veritabanını denemek için. Çoğu durumda **resmi image**'ları kullanıp sadece environment variable'lar ile yapılandırmanız yeterlidir. + +Bu şekilde, çoğu zaman container'lar ve Docker hakkında öğrendiklerinizi farklı araç ve bileşenlerde tekrar kullanabilirsiniz. + +Dolayısıyla; veritabanı, Python uygulaması, React frontend uygulaması olan bir web server gibi farklı şeyler için **birden fazla container** çalıştırır ve bunları internal network üzerinden birbirine bağlarsınız. + +Docker veya Kubernetes gibi tüm container yönetim sistemlerinde bu ağ özellikleri entegre olarak bulunur. + +## Container'lar ve Process'ler { #containers-and-processes } + +Bir **container image** normalde metadata içinde, **container** başlatıldığında çalıştırılacak varsayılan program/komutu ve o programa geçirilecek parametreleri içerir. Bu, komut satırında yazacağınız şeye çok benzer. + +Bir **container** başlatıldığında bu komutu/programı çalıştırır (ancak isterseniz bunu override edip başka bir komut/program çalıştırabilirsiniz). + +Bir container, **ana process** (komut/program) çalıştığı sürece çalışır. + +Container'larda normalde **tek bir process** olur. Ancak ana process içinden subprocess'ler başlatmak da mümkündür; böylece aynı container içinde **birden fazla process** olur. + +Ama **en az bir çalışan process olmadan** çalışan bir container olamaz. Ana process durursa container da durur. + +## FastAPI için Docker Image Oluşturalım { #build-a-docker-image-for-fastapi } + +Tamam, şimdi bir şeyler inşa edelim! 🚀 + +Resmi **Python** image'ını temel alarak, FastAPI için **sıfırdan** bir **Docker image** nasıl oluşturulur göstereceğim. + +Bu, örneğin şu durumlarda **çoğu zaman** yapmak isteyeceğiniz şeydir: + +* **Kubernetes** veya benzeri araçlar kullanırken +* **Raspberry Pi** üzerinde çalıştırırken +* Container image'ınızı sizin için çalıştıran bir cloud servisi kullanırken, vb. + +### Paket Gereksinimleri { #package-requirements } + +Uygulamanızın **paket gereksinimleri** genelde bir dosyada yer alır. + +Bu, gereksinimleri **yüklemek** için kullandığınız araca göre değişir. + +En yaygın yöntem, paket adları ve versiyonlarının satır satır yazıldığı bir `requirements.txt` dosyasına sahip olmaktır. + +Versiyon aralıklarını belirlemek için elbette [FastAPI sürümleri hakkında](versions.md){.internal-link target=_blank} bölümünde okuduğunuz fikirleri kullanırsınız. + +Örneğin `requirements.txt` şöyle görünebilir: + +``` +fastapi[standard]>=0.113.0,<0.114.0 +pydantic>=2.7.0,<3.0.0 +``` + +Ve bu bağımlılıkları normalde `pip` ile yüklersiniz, örneğin: + +
+ +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic +``` + +
+ +/// info | Bilgi + +Paket bağımlılıklarını tanımlamak ve yüklemek için başka formatlar ve araçlar da vardır. + +/// + +### **FastAPI** Kodunu Oluşturun { #create-the-fastapi-code } + +* Bir `app` dizini oluşturun ve içine girin. +* Boş bir `__init__.py` dosyası oluşturun. +* Aşağıdakilerle bir `main.py` dosyası oluşturun: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +### Dockerfile { #dockerfile } + +Şimdi aynı proje dizininde `Dockerfile` adlı bir dosya oluşturun ve içine şunları yazın: + +```{ .dockerfile .annotate } +# (1)! +FROM python:3.9 + +# (2)! +WORKDIR /code + +# (3)! +COPY ./requirements.txt /code/requirements.txt + +# (4)! +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (5)! +COPY ./app /code/app + +# (6)! +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +1. Resmi Python base image'ından başlayın. + +2. Geçerli çalışma dizinini `/code` olarak ayarlayın. + + `requirements.txt` dosyasını ve `app` dizinini buraya koyacağız. + +3. Gereksinimleri içeren dosyayı `/code` dizinine kopyalayın. + + Önce kodun tamamını değil, **sadece** gereksinim dosyasını kopyalayın. + + Bu dosya **çok sık değişmediği** için Docker bunu tespit eder ve bu adımda **cache** kullanır; böylece bir sonraki adım için de cache devreye girer. + +4. Gereksinim dosyasındaki paket bağımlılıklarını yükleyin. + + `--no-cache-dir` seçeneği, indirilen paketlerin yerel olarak kaydedilmemesini `pip`'e söyler. Bu kayıt, `pip` aynı paketleri tekrar yüklemek için yeniden çalıştırılacaksa işe yarar; ancak container'larla çalışırken genelde bu durum geçerli değildir. + + /// note | Not + + `--no-cache-dir` yalnızca `pip` ile ilgilidir; Docker veya container'larla ilgili değildir. + + /// + + `--upgrade` seçeneği, paketler zaten yüklüyse `pip`'e onları yükseltmesini söyler. + + Bir önceki adım (dosyayı kopyalama) **Docker cache** tarafından tespit edilebildiği için, bu adım da uygun olduğunda **Docker cache'i kullanır**. + + Bu adımda cache kullanmak, geliştirme sırasında image'ı tekrar tekrar build ederken size çok **zaman** kazandırır; her seferinde bağımlılıkları **indirip yüklemek** zorunda kalmazsınız. + +5. `./app` dizinini `/code` dizininin içine kopyalayın. + + Burada en sık değişen şey olan kodun tamamı bulunduğundan, bu adım (ve genelde bundan sonraki adımlar) için Docker **cache**'i kolay kolay kullanılamaz. + + Bu yüzden, container image build sürelerini optimize etmek için bunu `Dockerfile`'ın **sonlarına yakın** koymak önemlidir. + +6. Altta Uvicorn kullanan `fastapi run` komutunu **command** olarak ayarlayın. + + `CMD` bir string listesi alır; bu string'lerin her biri komut satırında boşlukla ayrılmış şekilde yazacağınız parçaları temsil eder. + + Bu komut, yukarıda `WORKDIR /code` ile ayarladığınız `/code` dizininden çalıştırılır. + +/// tip | İpucu + +Kod içindeki her numara balonuna tıklayarak her satırın ne yaptığını gözden geçirin. 👆 + +/// + +/// warning | Uyarı + +Aşağıda açıklandığı gibi `CMD` talimatının **her zaman** **exec form**'unu kullandığınızdan emin olun. + +/// + +#### `CMD` Kullanımı - Exec Form { #use-cmd-exec-form } + +`CMD` Docker talimatı iki formda yazılabilir: + +✅ **Exec** form: + +```Dockerfile +# ✅ Do this +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +⛔️ **Shell** form: + +```Dockerfile +# ⛔️ Don't do this +CMD fastapi run app/main.py --port 80 +``` + +FastAPI'nin düzgün şekilde kapanabilmesi ve [lifespan event](../advanced/events.md){.internal-link target=_blank}'lerinin tetiklenmesi için her zaman **exec** formunu kullanın. + +Detaylar için shell ve exec form için Docker dokümanlarına bakabilirsiniz. + +Bu durum `docker compose` kullanırken oldukça belirgin olabilir. Daha teknik detaylar için şu Docker Compose FAQ bölümüne bakın: Why do my services take 10 seconds to recreate or stop?. + +#### Dizin Yapısı { #directory-structure } + +Artık dizin yapınız şöyle olmalı: + +``` +. +├── app +│   ├── __init__.py +│ └── main.py +├── Dockerfile +└── requirements.txt +``` + +#### TLS Termination Proxy Arkasında { #behind-a-tls-termination-proxy } + +Container'ınızı Nginx veya Traefik gibi bir TLS Termination Proxy (load balancer) arkasında çalıştırıyorsanız `--proxy-headers` seçeneğini ekleyin. Bu, Uvicorn'a (FastAPI CLI üzerinden) uygulamanın HTTPS arkasında çalıştığını söyleyen proxy header'larına güvenmesini söyler. + +```Dockerfile +CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] +``` + +#### Docker Cache { #docker-cache } + +Bu `Dockerfile` içinde önemli bir numara var: önce kodun geri kalanını değil, **sadece bağımlılık dosyasını** kopyalıyoruz. Nedenini anlatayım. + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +Docker ve benzeri araçlar bu container image'larını **artımlı (incremental)** olarak **build** eder; `Dockerfile`'ın en üstünden başlayıp her talimatın oluşturduğu dosyaları ekleyerek **katman katman (layer)** ilerler. + +Docker ve benzeri araçlar image build ederken ayrıca bir **internal cache** kullanır. Son build'den beri bir dosya değişmediyse, dosyayı tekrar kopyalayıp sıfırdan yeni bir layer oluşturmak yerine, daha önce oluşturulan **aynı layer**'ı yeniden kullanır. + +Sadece dosya kopyalamayı azaltmak her zaman büyük fark yaratmaz. Ancak o adımda cache kullanıldığı için, **bir sonraki adımda da cache kullanılabilir**. Örneğin bağımlılıkları yükleyen şu talimat için: + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +Paket gereksinimleri dosyası **sık sık değişmez**. Bu yüzden sadece bu dosyayı kopyalayınca, Docker bu adımda **cache** kullanabilir. + +Sonra Docker, bağımlılıkları indirip yükleyen **bir sonraki adımda** da cache kullanabilir. Asıl **çok zaman kazandığımız** yer de burasıdır. ✨ ...ve beklerken sıkılmayı engeller. 😪😆 + +Bağımlılıkları indirip yüklemek **dakikalar sürebilir**, fakat **cache** kullanmak en fazla **saniyeler** alır. + +Geliştirme sırasında kod değişikliklerinizin çalıştığını kontrol etmek için container image'ı tekrar tekrar build edeceğinizden, bu ciddi birikimli zaman kazancı sağlar. + +Sonra `Dockerfile`'ın sonlarına doğru tüm kodu kopyalarız. En sık değişen kısım bu olduğu için sona koyarız; çünkü neredeyse her zaman bu adımdan sonra gelen adımlar cache kullanamaz. + +```Dockerfile +COPY ./app /code/app +``` + +### Docker Image'ını Build Edin { #build-the-docker-image } + +Tüm dosyalar hazır olduğuna göre container image'ı build edelim. + +* Proje dizinine gidin (`Dockerfile`'ınızın olduğu ve `app` dizininizi içeren dizin). +* FastAPI image'ınızı build edin: + +
+ +```console +$ docker build -t myimage . + +---> 100% +``` + +
+ +/// tip | İpucu + +Sondaki `.` ifadesine dikkat edin; `./` ile aynı anlama gelir ve Docker'a container image build etmek için hangi dizini kullanacağını söyler. + +Bu örnekte, mevcut dizindir (`.`). + +/// + +### Docker Container'ını Başlatın { #start-the-docker-container } + +* Image'ınızdan bir container çalıştırın: + +
+ +```console +$ docker run -d --name mycontainer -p 80:80 myimage +``` + +
+ +## Kontrol Edin { #check-it } + +Docker container'ınızın URL'inden kontrol edebilmelisiniz. Örneğin: http://192.168.99.100/items/5?q=somequery veya http://127.0.0.1/items/5?q=somequery (ya da Docker host'unuzu kullanarak eşdeğeri). + +Şuna benzer bir şey görürsünüz: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +## Etkileşimli API Dokümanları { #interactive-api-docs } + +Şimdi http://192.168.99.100/docs veya http://127.0.0.1/docs adresine gidebilirsiniz (ya da Docker host'unuzla eşdeğeri). + +Otomatik etkileşimli API dokümantasyonunu görürsünüz ( Swagger UI tarafından sağlanır): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +## Alternatif API Dokümanları { #alternative-api-docs } + +Ayrıca http://192.168.99.100/redoc veya http://127.0.0.1/redoc adresine de gidebilirsiniz (ya da Docker host'unuzla eşdeğeri). + +Alternatif otomatik dokümantasyonu görürsünüz (ReDoc tarafından sağlanır): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Tek Dosyalık FastAPI ile Docker Image Oluşturma { #build-a-docker-image-with-a-single-file-fastapi } + +FastAPI uygulamanız tek bir dosyaysa; örneğin `./app` dizini olmadan sadece `main.py` varsa, dosya yapınız şöyle olabilir: + +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` + +Bu durumda `Dockerfile` içinde dosyayı kopyaladığınız path'leri buna göre değiştirmeniz yeterlidir: + +```{ .dockerfile .annotate hl_lines="10 13" } +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (1)! +COPY ./main.py /code/ + +# (2)! +CMD ["fastapi", "run", "main.py", "--port", "80"] +``` + +1. `main.py` dosyasını doğrudan `/code` dizinine kopyalayın (herhangi bir `./app` dizini olmadan). + +2. Tek dosya olan `main.py` içindeki uygulamanızı sunmak için `fastapi run` kullanın. + +Dosyayı `fastapi run`'a verdiğinizde, bunun bir package'ın parçası değil tek bir dosya olduğunu otomatik olarak algılar; nasıl import edip FastAPI uygulamanızı nasıl serve edeceğini bilir. 😎 + +## Deployment Kavramları { #deployment-concepts } + +Aynı [Deployment Kavramları](concepts.md){.internal-link target=_blank}nı bu kez container'lar açısından tekrar konuşalım. + +Container'lar, bir uygulamayı **build etme ve deploy etme** sürecini basitleştiren bir araçtır. Ancak bu **deployment kavramları**nı ele almak için belirli bir yaklaşımı zorunlu kılmazlar; birkaç farklı strateji mümkündür. + +**İyi haber** şu: Hangi stratejiyi seçerseniz seçin, deployment kavramlarının tamamını kapsayacak bir yol vardır. 🎉 + +Bu **deployment kavramları**nı container'lar açısından gözden geçirelim: + +* HTTPS +* Startup'ta çalıştırma +* Restart'lar +* Replication (çalışan process sayısı) +* Memory +* Başlatmadan önceki adımlar + +## HTTPS { #https } + +Bir FastAPI uygulamasının sadece **container image**'ına (ve sonra çalışan **container**'a) odaklanırsak, HTTPS genellikle **haricen** başka bir araçla ele alınır. + +Örneğin Traefik kullanan başka bir container olabilir; **HTTPS** ve **sertifika**ların **otomatik** alınmasını o yönetebilir. + +/// tip | İpucu + +Traefik; Docker, Kubernetes ve diğerleriyle entegre çalışır. Bu sayede container'larınız için HTTPS'i kurup yapılandırmak oldukça kolaydır. + +/// + +Alternatif olarak HTTPS, bir cloud provider'ın sunduğu servislerden biri tarafından da yönetilebilir (uygulama yine container içinde çalışırken). + +## Startup'ta Çalıştırma ve Restart'lar { #running-on-startup-and-restarts } + +Container'ınızı **başlatıp çalıştırmaktan** sorumlu genellikle başka bir araç olur. + +Bu; doğrudan **Docker**, **Docker Compose**, **Kubernetes**, bir **cloud service** vb. olabilir. + +Çoğu (veya tüm) durumda, container'ı startup'ta çalıştırmayı ve hata durumlarında restart'ları etkinleştirmeyi sağlayan basit bir seçenek vardır. Örneğin Docker'da bu, `--restart` komut satırı seçeneğidir. + +Container kullanmadan, uygulamaları startup'ta çalıştırmak ve restart mekanizması eklemek zahmetli ve zor olabilir. Ancak **container'larla çalışırken** çoğu zaman bu işlevler varsayılan olarak hazır gelir. ✨ + +## Replication - Process Sayısı { #replication-number-of-processes } + +Kubernetes, Docker Swarm Mode, Nomad veya benzeri, birden fazla makinede dağıtık container'ları yöneten karmaşık bir sistemle kurulmuş bir cluster'ınız varsa, replication'ı her container içinde bir **process manager** (ör. worker'lı Uvicorn) kullanarak yönetmek yerine, muhtemelen **cluster seviyesinde** ele almak istersiniz. + +Kubernetes gibi dağıtık container yönetim sistemleri, gelen request'ler için **load balancing** desteği sunarken aynı zamanda **container replication**'ını yönetmek için entegre mekanizmalara sahiptir. Hepsi **cluster seviyesinde**. + +Bu tür durumlarda, yukarıda [anlatıldığı gibi](#dockerfile) bağımlılıkları yükleyip **sıfırdan bir Docker image** build etmek ve birden fazla Uvicorn worker kullanmak yerine **tek bir Uvicorn process** çalıştırmak istersiniz. + +### Load Balancer { #load-balancer } + +Container'lar kullanırken, genellikle ana port'ta dinleyen bir bileşen olur. Bu, **HTTPS**'i ele almak için bir **TLS Termination Proxy** olan başka bir container da olabilir ya da benzeri bir araç olabilir. + +Bu bileşen request'lerin **yükünü** alıp worker'lar arasında (umarım) **dengeli** şekilde dağıttığı için yaygın olarak **Load Balancer** diye de adlandırılır. + +/// tip | İpucu + +HTTPS için kullanılan aynı **TLS Termination Proxy** bileşeni muhtemelen bir **Load Balancer** olarak da çalışır. + +/// + +Container'larla çalışırken, onları başlatıp yönettiğiniz sistem; bu **load balancer**'dan (aynı zamanda **TLS Termination Proxy** de olabilir) uygulamanızın bulunduğu container(lar)a **network communication**'ı (ör. HTTP request'leri) iletmek için zaten dahili araçlar sunar. + +### Tek Load Balancer - Çoklu Worker Container { #one-load-balancer-multiple-worker-containers } + +**Kubernetes** veya benzeri dağıtık container yönetim sistemleriyle çalışırken, dahili ağ mekanizmaları sayesinde ana **port**'u dinleyen tek bir **load balancer**, uygulamanızı çalıştıran muhtemelen **birden fazla container**'a request'leri iletebilir. + +Uygulamanızı çalıştıran bu container'ların her birinde normalde **tek bir process** olur (ör. FastAPI uygulamanızı çalıştıran bir Uvicorn process). Hepsi aynı şeyi çalıştıran **özdeş container**'lardır; ancak her birinin kendi process'i, memory'si vb. vardır. Böylece CPU'nun **farklı core**'larında, hatta **farklı makinelerde** paralelleştirmeden yararlanırsınız. + +Load balancer'lı dağıtık sistem, request'leri uygulamanızın bulunduğu container'ların her birine sırayla **dağıtır**. Böylece her request, uygulamanızın birden fazla **replicated container**'ından biri tarafından işlenebilir. + +Ve bu **load balancer** normalde cluster'ınızdaki *diğer* uygulamalara giden request'leri de (ör. farklı bir domain ya da farklı bir URL path prefix altında) yönetebilir ve iletişimi o *diğer* uygulamanın doğru container'larına iletir. + +### Container Başına Tek Process { #one-process-per-container } + +Bu senaryoda, replication'ı zaten cluster seviyesinde yaptığınız için, muhtemelen **container başına tek bir (Uvicorn) process** istersiniz. + +Dolayısıyla bu durumda container içinde `--workers` gibi bir komut satırı seçeneğiyle çoklu worker istemezsiniz. Container başına sadece **tek bir Uvicorn process** istersiniz (ama muhtemelen birden fazla container). + +Container içine ekstra bir process manager koymak (çoklu worker gibi) çoğu zaman zaten cluster sisteminizle çözdüğünüz şeye ek **gereksiz karmaşıklık** katar. + +### Birden Fazla Process'li Container'lar ve Özel Durumlar { #containers-with-multiple-processes-and-special-cases } + +Elbette bazı **özel durumlarda** bir container içinde birden fazla **Uvicorn worker process** çalıştırmak isteyebilirsiniz. + +Bu durumlarda çalıştırmak istediğiniz worker sayısını `--workers` komut satırı seçeneğiyle ayarlayabilirsiniz: + +```{ .dockerfile .annotate } +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app + +# (1)! +CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"] +``` + +1. Burada worker sayısını 4 yapmak için `--workers` komut satırı seçeneğini kullanıyoruz. + +Bunun mantıklı olabileceği birkaç örnek: + +#### Basit Bir Uygulama { #a-simple-app } + +Uygulamanız tek bir server üzerinde (cluster değil) çalışacak kadar **basitse**, container içinde bir process manager isteyebilirsiniz. + +#### Docker Compose { #docker-compose } + +**Docker Compose** ile **tek bir server**'a (cluster değil) deploy ediyor olabilirsiniz. Bu durumda, paylaşılan ağı ve **load balancing**'i koruyarak container replication'ını (Docker Compose ile) yönetmenin kolay bir yolu olmayabilir. + +Bu durumda, tek bir container içinde **bir process manager** ile **birden fazla worker process** başlatmak isteyebilirsiniz. + +--- + +Ana fikir şu: Bunların **hiçbiri** körü körüne uymanız gereken **değişmez kurallar** değildir. Bu fikirleri, kendi kullanım senaryonuzu **değerlendirmek** ve sisteminiz için en iyi yaklaşımı seçmek için kullanabilirsiniz. Şu kavramları nasıl yöneteceğinize bakarak karar verin: + +* Güvenlik - HTTPS +* Startup'ta çalıştırma +* Restart'lar +* Replication (çalışan process sayısı) +* Memory +* Başlatmadan önceki adımlar + +## Memory { #memory } + +**Container başına tek process** çalıştırırsanız, her container'ın tüketeceği memory miktarı aşağı yukarı tanımlı, stabil ve sınırlı olur (replication varsa birden fazla container için). + +Sonra aynı memory limit ve gereksinimlerini container yönetim sisteminizin (ör. **Kubernetes**) konfigürasyonlarında belirleyebilirsiniz. Böylece sistem; ihtiyaç duyulan memory miktarını ve cluster'daki makinelerde mevcut memory'yi dikkate alarak **uygun makinelerde container'ları replicate edebilir**. + +Uygulamanız **basitse**, muhtemelen bu **bir sorun olmaz** ve katı memory limitleri belirlemeniz gerekmeyebilir. Ancak **çok memory kullanıyorsanız** (ör. **machine learning** modelleriyle), ne kadar memory tükettiğinizi kontrol edip **her makinede** çalışacak **container sayısını** ayarlamalısınız (ve gerekirse cluster'a daha fazla makine eklemelisiniz). + +**Container başına birden fazla process** çalıştırırsanız, başlatılan process sayısının mevcut olandan **fazla memory tüketmediğinden** emin olmanız gerekir. + +## Başlatmadan Önceki Adımlar ve Container'lar { #previous-steps-before-starting-and-containers } + +Container kullanıyorsanız (örn. Docker, Kubernetes), temelde iki yaklaşım vardır. + +### Birden Fazla Container { #multiple-containers } + +**Birden fazla container**'ınız varsa ve muhtemelen her biri **tek process** çalıştırıyorsa (ör. bir **Kubernetes** cluster'ında), replication yapılan worker container'lar çalışmadan **önce**, **başlatmadan önceki adımlar**ın işini yapan **ayrı bir container** kullanmak isteyebilirsiniz (tek container, tek process). + +/// info | Bilgi + +Kubernetes kullanıyorsanız, bu muhtemelen bir Init Container olur. + +/// + +Kullanım senaryonuzda bu adımları **paralel olarak birden fazla kez** çalıştırmak sorun değilse (örneğin veritabanı migration çalıştırmıyor, sadece veritabanı hazır mı diye kontrol ediyorsanız), o zaman her container'da ana process başlamadan hemen önce de çalıştırabilirsiniz. + +### Tek Container { #single-container } + +Basit bir kurulumda; **tek bir container** olup onun içinde birden fazla **worker process** (ya da sadece bir process) başlatıyorsanız, bu adımları aynı container içinde, uygulama process'ini başlatmadan hemen önce çalıştırabilirsiniz. + +### Base Docker Image { #base-docker-image } + +Eskiden resmi bir FastAPI Docker image'ı vardı: tiangolo/uvicorn-gunicorn-fastapi. Ancak artık kullanımdan kaldırıldı (deprecated). ⛔️ + +Muhtemelen bu base Docker image'ını (veya benzeri başka bir image'ı) kullanmamalısınız. + +**Kubernetes** (veya diğerleri) kullanıyor ve cluster seviyesinde birden fazla **container** ile **replication** ayarlıyorsanız, bu durumda yukarıda anlatıldığı gibi **sıfırdan bir image build etmek** daha iyi olur: [FastAPI için Docker Image Oluşturalım](#build-a-docker-image-for-fastapi). + +Ve birden fazla worker gerekiyorsa, sadece `--workers` komut satırı seçeneğini kullanabilirsiniz. + +/// note | Teknik Detaylar + +Bu Docker image, Uvicorn dead worker'ları yönetmeyi ve yeniden başlatmayı desteklemediği dönemde oluşturulmuştu. Bu yüzden Uvicorn ile birlikte Gunicorn kullanmak gerekiyordu; sırf Gunicorn, Uvicorn worker process'lerini yönetip yeniden başlatsın diye oldukça fazla karmaşıklık ekleniyordu. + +Ancak artık Uvicorn (ve `fastapi` komutu) `--workers` kullanımını desteklediğine göre, kendi image'ınızı build etmek yerine bir base Docker image kullanmanın bir nedeni kalmadı (kod miktarı da hemen hemen aynı 😅). + +/// + +## Container Image'ı Deploy Etme { #deploy-the-container-image } + +Bir Container (Docker) Image'ınız olduktan sonra bunu deploy etmenin birkaç yolu vardır. + +Örneğin: + +* Tek bir server'da **Docker Compose** ile +* Bir **Kubernetes** cluster'ı ile +* Docker Swarm Mode cluster'ı ile +* Nomad gibi başka bir araçla +* Container image'ınızı alıp deploy eden bir cloud servisiyle + +## `uv` ile Docker Image { #docker-image-with-uv } + +Projenizi yüklemek ve yönetmek için uv kullanıyorsanız, onların uv Docker rehberini takip edebilirsiniz. + +## Özet { #recap } + +Container sistemleri (örn. **Docker** ve **Kubernetes** ile) kullanınca tüm **deployment kavramları**nı ele almak oldukça kolaylaşır: + +* HTTPS +* Startup'ta çalıştırma +* Restart'lar +* Replication (çalışan process sayısı) +* Memory +* Başlatmadan önceki adımlar + +Çoğu durumda bir base image kullanmak istemezsiniz; bunun yerine resmi Python Docker image'ını temel alarak **sıfırdan bir container image** build edersiniz. + +`Dockerfile` içindeki talimatların **sırasına** ve **Docker cache**'ine dikkat ederek **build sürelerini minimize edebilir**, üretkenliğinizi artırabilirsiniz (ve beklerken sıkılmayı önlersiniz). 😎 diff --git a/docs/tr/docs/deployment/fastapicloud.md b/docs/tr/docs/deployment/fastapicloud.md new file mode 100644 index 000000000..bb861273b --- /dev/null +++ b/docs/tr/docs/deployment/fastapicloud.md @@ -0,0 +1,65 @@ +# FastAPI Cloud { #fastapi-cloud } + +FastAPI uygulamanızı FastAPI Cloud'a **tek bir komutla** deploy edebilirsiniz. Henüz yapmadıysanız gidip bekleme listesine katılın. 🚀 + +## Giriş Yapma { #login } + +Önceden bir **FastAPI Cloud** hesabınız olduğundan emin olun (sizi bekleme listesinden davet ettik 😉). + +Ardından giriş yapın: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud 🚀 +``` + +
+ +## Deploy { #deploy } + +Şimdi uygulamanızı **tek bir komutla** deploy edin: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +✅ Deployment successful! + +🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +Hepsi bu! Artık uygulamanıza o URL üzerinden erişebilirsiniz. ✨ + +## FastAPI Cloud Hakkında { #about-fastapi-cloud } + +**FastAPI Cloud**, **FastAPI**'nin arkasındaki aynı yazar ve ekip tarafından geliştirilmiştir. + +Bir API'yi minimum eforla **geliştirme**, **deploy etme** ve **erişilebilir kılma** sürecini sadeleştirir. + +FastAPI ile uygulama geliştirirken elde ettiğiniz aynı **developer experience**'ı, onları buluta **deploy etmeye** de taşır. 🎉 + +Ayrıca bir uygulamayı deploy ederken ihtiyaç duyacağınız pek çok şeyi de sizin için halleder; örneğin: + +* HTTPS +* Replication (çoğaltma), request'lere göre autoscaling ile +* vb. + +FastAPI Cloud, *FastAPI and friends* açık kaynak projelerinin birincil sponsoru ve finansman sağlayıcısıdır. ✨ + +## Diğer cloud sağlayıcılarına deploy etme { #deploy-to-other-cloud-providers } + +FastAPI açık kaynaklıdır ve standartlara dayanır. FastAPI uygulamalarını seçtiğiniz herhangi bir cloud sağlayıcısına deploy edebilirsiniz. + +FastAPI uygulamalarını deploy etmek için cloud sağlayıcınızın kendi kılavuzlarını takip edin. 🤓 + +## Kendi server'ınıza deploy etme { #deploy-your-own-server } + +Bu **Deployment** kılavuzunun ilerleyen bölümlerinde tüm detayları da ele alacağız; böylece neler olduğunu, nelerin gerçekleşmesi gerektiğini ve FastAPI uygulamalarını kendi başınıza (kendi server'larınızla da) nasıl deploy edebileceğinizi anlayacaksınız. 🤓 diff --git a/docs/tr/docs/deployment/https.md b/docs/tr/docs/deployment/https.md new file mode 100644 index 000000000..bb70883aa --- /dev/null +++ b/docs/tr/docs/deployment/https.md @@ -0,0 +1,231 @@ +# HTTPS Hakkında { #about-https } + +HTTPS’in sadece "açık" ya da "kapalı" olan bir şey olduğunu düşünmek kolaydır. + +Ancak bundan çok daha karmaşıktır. + +/// tip | İpucu + +Aceleniz varsa veya çok da önemsemiyorsanız, her şeyi farklı tekniklerle adım adım kurmak için sonraki bölümlere geçin. + +/// + +Bir kullanıcı gözüyle **HTTPS’in temellerini öğrenmek** için https://howhttps.works/ adresine bakın. + +Şimdi de **geliştirici perspektifinden**, HTTPS hakkında düşünürken akılda tutulması gereken birkaç nokta: + +* HTTPS için **server**’ın, **üçüncü bir taraf** tarafından verilen **"sertifikalara"** sahip olması gerekir. + * Bu sertifikalar aslında üçüncü tarafça "üretilmez", üçüncü taraftan **temin edilir**. +* Sertifikaların bir **geçerlilik süresi** vardır. + * Süresi **dolar**. + * Sonrasında **yenilenmeleri**, üçüncü taraftan **yeniden temin edilmeleri** gerekir. +* Bağlantının şifrelenmesi **TCP seviyesinde** gerçekleşir. + * Bu, **HTTP’nin bir katman altıdır**. + * Dolayısıyla **sertifika ve şifreleme** işlemleri **HTTP’den önce** yapılır. +* **TCP "domain"leri bilmez**. Yalnızca IP adreslerini bilir. + * İstenen **spesifik domain** bilgisi **HTTP verisinin** içindedir. +* **HTTPS sertifikaları** belirli bir **domain**’i "sertifikalandırır"; ancak protokol ve şifreleme TCP seviyesinde, hangi domain ile çalışıldığı **henüz bilinmeden** gerçekleşir. +* **Varsayılan olarak** bu, IP adresi başına yalnızca **bir HTTPS sertifikası** olabileceği anlamına gelir. + * Server’ınız ne kadar büyük olursa olsun ya da üzerindeki her uygulama ne kadar küçük olursa olsun. + * Ancak bunun bir **çözümü** vardır. +* **TLS** protokolüne (TCP seviyesinde, HTTP’den önce şifrelemeyi yapan) eklenen **SNI** adlı bir **extension** vardır. + * Bu SNI extension’ı, tek bir server’ın (tek bir **IP adresiyle**) **birden fazla HTTPS sertifikası** kullanmasına ve **birden fazla HTTPS domain/uygulama** sunmasına izin verir. + * Bunun çalışması için server üzerinde, **public IP adresini** dinleyen tek bir bileşenin (programın) server’daki **tüm HTTPS sertifikalarına** sahip olması gerekir. +* Güvenli bir bağlantı elde edildikten **sonra**, iletişim protokolü **hâlâ HTTP**’dir. + * İçerikler, **HTTP protokolü** ile gönderiliyor olsa bile **şifrelenmiştir**. + +Yaygın yaklaşım, server’da (makine, host vb.) çalışan **tek bir program/HTTP server** bulundurup **HTTPS ile ilgili tüm kısımları** yönetmektir: **şifreli HTTPS request**’leri almak, aynı server’da çalışan gerçek HTTP uygulamasına (bu örnekte **FastAPI** uygulaması) **şifresi çözülmüş HTTP request**’leri iletmek, uygulamadan gelen **HTTP response**’u almak, uygun **HTTPS sertifikası** ile **şifrelemek** ve **HTTPS** ile client’a geri göndermek. Bu server’a çoğu zaman **TLS Termination Proxy** denir. + +TLS Termination Proxy olarak kullanabileceğiniz seçeneklerden bazıları: + +* Traefik (sertifika yenilemelerini de yönetebilir) +* Caddy (sertifika yenilemelerini de yönetebilir) +* Nginx +* HAProxy + +## Let's Encrypt { #lets-encrypt } + +Let's Encrypt’ten önce bu **HTTPS sertifikaları**, güvenilen üçüncü taraflar tarafından satılırdı. + +Bu sertifikalardan birini temin etme süreci zahmetliydi, epey evrak işi gerektirirdi ve sertifikalar oldukça pahalıydı. + +Sonra **Let's Encrypt** ortaya çıktı. + +Linux Foundation’ın bir projesidir. **HTTPS sertifikalarını ücretsiz** ve otomatik bir şekilde sağlar. Bu sertifikalar tüm standart kriptografik güvenliği kullanır ve kısa ömürlüdür (yaklaşık 3 ay). Bu yüzden, ömürleri kısa olduğu için **güvenlik aslında daha iyidir**. + +Domain’ler güvenli şekilde doğrulanır ve sertifikalar otomatik üretilir. Bu sayede sertifikaların yenilenmesini otomatikleştirmek de mümkün olur. + +Amaç, bu sertifikaların temin edilmesi ve yenilenmesini otomatikleştirerek **ücretsiz, kalıcı olarak güvenli HTTPS** sağlamaktır. + +## Geliştiriciler İçin HTTPS { #https-for-developers } + +Burada, bir HTTPS API’nin adım adım nasıl görünebileceğine dair, özellikle geliştiriciler için önemli fikirlere odaklanan bir örnek var. + +### Domain Adı { #domain-name } + +Muhtemelen her şey, bir **domain adı** **temin etmenizle** başlar. Sonra bunu bir DNS server’ında (muhtemelen aynı cloud provider’ınızda) yapılandırırsınız. + +Muhtemelen bir cloud server (virtual machine) ya da benzeri bir şey alırsınız ve bunun fixed bir **public IP adresi** olur. + +DNS server(lar)ında, bir kaydı ("`A record`") **domain**’inizi server’ınızın **public IP adresine** yönlendirecek şekilde yapılandırırsınız. + +Bunu büyük olasılıkla ilk kurulumda, sadece bir kez yaparsınız. + +/// tip | İpucu + +Bu Domain Adı kısmı HTTPS’ten çok daha önce gelir. Ancak her şey domain ve IP adresine bağlı olduğu için burada bahsetmeye değer. + +/// + +### DNS { #dns } + +Şimdi gerçek HTTPS parçalarına odaklanalım. + +Önce tarayıcı, bu örnekte `someapp.example.com` olan domain için **IP**’nin ne olduğunu **DNS server**’larına sorar. + +DNS server’ları tarayıcıya belirli bir **IP adresini** kullanmasını söyler. Bu, DNS server’larında yapılandırdığınız ve server’ınızın kullandığı public IP adresidir. + + + +### TLS Handshake Başlangıcı { #tls-handshake-start } + +Tarayıcı daha sonra bu IP adresiyle **443 portu** (HTTPS portu) üzerinden iletişim kurar. + +İletişimin ilk kısmı, client ile server arasında bağlantıyı kurmak ve hangi kriptografik anahtarların kullanılacağına karar vermek vb. içindir. + + + +Client ile server arasındaki, TLS bağlantısını kurmaya yönelik bu etkileşime **TLS handshake** denir. + +### SNI Extension’ı ile TLS { #tls-with-sni-extension } + +Server’da, belirli bir **IP adresindeki** belirli bir **portu** dinleyen **yalnızca bir process** olabilir. Aynı IP adresinde başka portları dinleyen başka process’ler olabilir, ancak IP+port kombinasyonu başına yalnızca bir tane olur. + +TLS (HTTPS) varsayılan olarak `443` portunu kullanır. Yani ihtiyaç duyacağımız port budur. + +Bu portu yalnızca bir process dinleyebileceği için, bunu yapacak process **TLS Termination Proxy** olur. + +TLS Termination Proxy, bir ya da daha fazla **TLS sertifikasına** (HTTPS sertifikası) erişebilir. + +Yukarıda bahsettiğimiz **SNI extension**’ını kullanarak TLS Termination Proxy, bu bağlantı için elindeki TLS (HTTPS) sertifikalarından hangisini kullanacağını kontrol eder; client’ın beklediği domain ile eşleşen sertifikayı seçer. + +Bu örnekte `someapp.example.com` sertifikasını kullanır. + + + +Client, bu TLS sertifikasını üreten kuruluşa zaten **güvenir** (bu örnekte Let's Encrypt; birazdan ona da geleceğiz). Bu sayede sertifikanın geçerli olduğunu **doğrulayabilir**. + +Ardından client ve TLS Termination Proxy, sertifikayı kullanarak **TCP iletişiminin geri kalanını nasıl şifreleyeceklerine** karar verir. Böylece **TLS Handshake** kısmı tamamlanır. + +Bundan sonra client ve server arasında **şifreli bir TCP bağlantısı** vardır; TLS’in sağladığı şey budur. Sonra bu bağlantıyı kullanarak gerçek **HTTP iletişimini** başlatabilirler. + +Ve **HTTPS** de tam olarak budur: şifrelenmemiş bir TCP bağlantısı yerine, **güvenli bir TLS bağlantısının içinde** düz **HTTP**’dir. + +/// tip | İpucu + +Şifrelemenin HTTP seviyesinde değil, **TCP seviyesinde** gerçekleştiğine dikkat edin. + +/// + +### HTTPS Request { #https-request } + +Artık client ile server (özellikle tarayıcı ile TLS Termination Proxy) arasında **şifreli bir TCP bağlantısı** olduğuna göre, **HTTP iletişimi** başlayabilir. + +Dolayısıyla client bir **HTTPS request** gönderir. Bu, şifreli bir TLS bağlantısı üzerinden giden bir HTTP request’tir. + + + +### Request’in Şifresini Çözme { #decrypt-the-request } + +TLS Termination Proxy, üzerinde anlaşılan şifrelemeyi kullanarak **request’in şifresini çözer** ve **düz (şifresi çözülmüş) HTTP request**’i uygulamayı çalıştıran process’e iletir (ör. FastAPI uygulamasını çalıştıran Uvicorn process’i). + + + +### HTTP Response { #http-response } + +Uygulama request’i işler ve TLS Termination Proxy’ye **düz (şifrelenmemiş) bir HTTP response** gönderir. + + + +### HTTPS Response { #https-response } + +TLS Termination Proxy daha sonra response’u, daha önce üzerinde anlaşılan kriptografi ile (başlangıcı `someapp.example.com` sertifikasına dayanan) **şifreler** ve tarayıcıya geri gönderir. + +Sonrasında tarayıcı response’un geçerli olduğunu ve doğru kriptografik anahtarla şifrelendiğini doğrular vb. Ardından **response’un şifresini çözer** ve işler. + + + +Client (tarayıcı), response’un doğru server’dan geldiğini bilir; çünkü daha önce **HTTPS sertifikası** ile üzerinde anlaştıkları kriptografiyi kullanmaktadır. + +### Birden Fazla Uygulama { #multiple-applications } + +Aynı server’da (veya server’larda) örneğin başka API programları ya da bir veritabanı gibi **birden fazla uygulama** olabilir. + +Belirli IP ve port kombinasyonunu yalnızca bir process yönetebilir (örneğimizde TLS Termination Proxy). Ancak diğer uygulamalar/process’ler, aynı **public IP + port kombinasyonunu** kullanmaya çalışmadıkları sürece server(lar)da çalışabilir. + + + +Bu şekilde TLS Termination Proxy, birden fazla uygulama için **birden fazla domain**’in HTTPS ve sertifika işlerini yönetebilir ve her durumda request’leri doğru uygulamaya iletebilir. + +### Sertifika Yenileme { #certificate-renewal } + +Gelecekte bir noktada, her sertifikanın süresi **dolar** (temin edildikten yaklaşık 3 ay sonra). + +Ardından başka bir program (bazı durumlarda ayrı bir programdır, bazı durumlarda aynı TLS Termination Proxy olabilir) Let's Encrypt ile konuşup sertifika(ları) yeniler. + + + +**TLS sertifikaları** bir IP adresiyle değil, **domain adıyla ilişkilidir**. + +Bu yüzden sertifikaları yenilemek için, yenileme programı otoriteye (Let's Encrypt) gerçekten o domain’i **"sahiplendiğini" ve kontrol ettiğini** **kanıtlamalıdır**. + +Bunu yapmak ve farklı uygulama ihtiyaçlarını karşılamak için birden fazla yöntem vardır. Yaygın yöntemlerden bazıları: + +* Bazı **DNS kayıtlarını değiştirmek**. + * Bunun için yenileme programının DNS provider API’lerini desteklemesi gerekir. Dolayısıyla kullandığınız DNS provider’a bağlı olarak bu seçenek mümkün de olabilir, olmayabilir de. +* Domain ile ilişkili public IP adresinde **server olarak çalışmak** (en azından sertifika temin sürecinde). + * Yukarıda söylediğimiz gibi, belirli bir IP ve portu yalnızca bir process dinleyebilir. + * Bu, aynı TLS Termination Proxy’nin sertifika yenileme sürecini de yönetmesinin neden çok faydalı olduğunun sebeplerinden biridir. + * Aksi halde TLS Termination Proxy’yi kısa süreliğine durdurmanız, sertifikaları temin etmek için yenileme programını başlatmanız, sonra bunları TLS Termination Proxy ile yapılandırmanız ve ardından TLS Termination Proxy’yi tekrar başlatmanız gerekebilir. Bu ideal değildir; çünkü TLS Termination Proxy kapalıyken uygulama(lar)ınıza erişilemez. + +Uygulamayı servis etmeye devam ederken tüm bu yenileme sürecini yönetebilmek, TLS sertifikalarını doğrudan uygulama server’ıyla (örn. Uvicorn) kullanmak yerine, TLS Termination Proxy ile HTTPS’i yönetecek **ayrı bir sistem** istemenizin başlıca nedenlerinden biridir. + +## Proxy Forwarded Headers { #proxy-forwarded-headers } + +HTTPS’i bir proxy ile yönetirken, **application server**’ınız (örneğin FastAPI CLI üzerinden Uvicorn) HTTPS süreci hakkında hiçbir şey bilmez; **TLS Termination Proxy** ile düz HTTP üzerinden iletişim kurar. + +Bu **proxy** normalde request’i **application server**’a iletmeden önce, request’in proxy tarafından **forward** edildiğini application server’a bildirmek için bazı HTTP header’larını anlık olarak ekler. + +/// note | Teknik Detaylar + +Proxy header’ları şunlardır: + +* X-Forwarded-For +* X-Forwarded-Proto +* X-Forwarded-Host + +/// + +Buna rağmen **application server**, güvenilen bir **proxy** arkasında olduğunu bilmediği için varsayılan olarak bu header’lara güvenmez. + +Ancak **application server**’ı, **proxy**’nin gönderdiği *forwarded* header’larına güvenecek şekilde yapılandırabilirsiniz. FastAPI CLI kullanıyorsanız, hangi IP’lerden gelen *forwarded* header’lara güvenmesi gerektiğini söylemek için *CLI Option* `--forwarded-allow-ips` seçeneğini kullanabilirsiniz. + +Örneğin **application server** yalnızca güvenilen **proxy**’den iletişim alıyorsa, yalnızca **proxy**’nin kullandığı IP’den request alacağı için `--forwarded-allow-ips="*"` ayarlayıp gelen tüm IP’lere güvenmesini sağlayabilirsiniz. + +Bu sayede uygulama kendi public URL’inin ne olduğunu, HTTPS kullanıp kullanmadığını, domain’i vb. bilebilir. + +Bu, örneğin redirect’leri doğru şekilde yönetmek için faydalıdır. + +/// tip | İpucu + +Bununla ilgili daha fazlasını [Behind a Proxy - Enable Proxy Forwarded Headers](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} dokümantasyonunda öğrenebilirsiniz. + +/// + +## Özet { #recap } + +**HTTPS** kullanmak çok önemlidir ve çoğu durumda oldukça **kritiktir**. Geliştirici olarak HTTPS etrafında harcadığınız çabanın büyük kısmı, aslında **bu kavramları** ve nasıl çalıştıklarını **anlamaktır**. + +Ancak **geliştiriciler için HTTPS**’in temel bilgilerini öğrendikten sonra, her şeyi basitçe yönetmek için farklı araçları kolayca birleştirip yapılandırabilirsiniz. + +Sonraki bölümlerin bazılarında, **FastAPI** uygulamaları için **HTTPS**’i nasıl kuracağınıza dair birkaç somut örnek göstereceğim. 🔒 diff --git a/docs/tr/docs/deployment/manually.md b/docs/tr/docs/deployment/manually.md new file mode 100644 index 000000000..561ba8677 --- /dev/null +++ b/docs/tr/docs/deployment/manually.md @@ -0,0 +1,157 @@ +# Bir Sunucuyu Manuel Olarak Çalıştırın { #run-a-server-manually } + +## `fastapi run` Komutunu Kullanın { #use-the-fastapi-run-command } + +Kısacası, FastAPI uygulamanızı sunmak için `fastapi run` kullanın: + +
+ +```console +$ fastapi run main.py + + FastAPI Starting production 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://0.0.0.0:8000 + server Documentation at http://0.0.0.0:8000/docs + + Logs: + + INFO Started server process [2306215] + INFO Waiting for application startup. + INFO Application startup complete. + INFO Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C + to quit) +``` + +
+ +Bu, çoğu durumda işinizi görür. 😎 + +Örneğin bu komutu, **FastAPI** app'inizi bir container içinde, bir sunucuda vb. başlatmak için kullanabilirsiniz. + +## ASGI Sunucuları { #asgi-servers } + +Şimdi biraz daha detaya inelim. + +FastAPI, Python web framework'leri ve sunucularını inşa etmek için kullanılan ASGI adlı bir standardı kullanır. FastAPI bir ASGI web framework'üdür. + +Uzak bir sunucu makinesinde **FastAPI** uygulamasını (veya herhangi bir ASGI uygulamasını) çalıştırmak için gereken ana şey, **Uvicorn** gibi bir ASGI server programıdır. `fastapi` komutuyla varsayılan olarak gelen de budur. + +Buna alternatif birkaç seçenek daha vardır, örneğin: + +* Uvicorn: yüksek performanslı bir ASGI server. +* Hypercorn: diğer özelliklerin yanında HTTP/2 ve Trio ile uyumlu bir ASGI server. +* Daphne: Django Channels için geliştirilmiş ASGI server. +* Granian: Python uygulamaları için bir Rust HTTP server. +* NGINX Unit: NGINX Unit, hafif ve çok yönlü bir web uygulaması runtime'ıdır. + +## Sunucu Makinesi ve Sunucu Programı { #server-machine-and-server-program } + +İsimlendirme konusunda akılda tutulması gereken küçük bir detay var. 💡 + +"**server**" kelimesi yaygın olarak hem uzak/bulut bilgisayarı (fiziksel veya sanal makine) hem de o makinede çalışan programı (ör. Uvicorn) ifade etmek için kullanılır. + +Dolayısıyla genel olarak "server" dendiğinde, bu iki şeyden birini kast ediyor olabilir. + +Uzak makineden bahsederken genelde **server** denir; ayrıca **machine**, **VM** (virtual machine), **node** ifadeleri de kullanılır. Bunların hepsi, genellikle Linux çalıştıran ve üzerinde programlarınızı çalıştırdığınız bir tür uzak makineyi ifade eder. + +## Sunucu Programını Yükleyin { #install-the-server-program } + +FastAPI'yi kurduğunuzda, production sunucusu olarak Uvicorn da beraberinde gelir ve bunu `fastapi run` komutuyla başlatabilirsiniz. + +Ancak bir ASGI server'ı manuel olarak da kurabilirsiniz. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, etkinleştirdiğinizden emin olun; ardından server uygulamasını kurabilirsiniz. + +Örneğin Uvicorn'u kurmak için: + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +Benzer bir süreç, diğer ASGI server programlarının tamamı için de geçerlidir. + +/// tip | İpucu + +`standard` eklediğinizde Uvicorn, önerilen bazı ek bağımlılıkları kurar ve kullanır. + +Bunlara, `asyncio` için yüksek performanslı bir drop-in replacement olan ve concurrency performansını ciddi şekilde artıran `uvloop` da dahildir. + +FastAPI'yi `pip install "fastapi[standard]"` gibi bir şekilde kurduğunuzda `uvicorn[standard]` da zaten kurulmuş olur. + +/// + +## Sunucu Programını Çalıştırın { #run-the-server-program } + +Bir ASGI server'ı manuel olarak kurduysanız, FastAPI uygulamanızı import edebilmesi için genellikle özel bir formatta bir import string geçirmeniz gerekir: + +
+ +```console +$ uvicorn main:app --host 0.0.0.0 --port 80 + +INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) +``` + +
+ +/// note | Not + +`uvicorn main:app` komutu şunları ifade eder: + +* `main`: `main.py` dosyası (Python "module"). +* `app`: `main.py` içinde `app = FastAPI()` satırıyla oluşturulan nesne. + +Şununla eşdeğerdir: + +```Python +from main import app +``` + +/// + +Her alternatif ASGI server programı için benzer bir komut bulunur; daha fazlası için ilgili dokümantasyonlarına bakabilirsiniz. + +/// warning | Uyarı + +Uvicorn ve diğer sunucular, geliştirme sırasında faydalı olan `--reload` seçeneğini destekler. + +`--reload` seçeneği çok daha fazla kaynak tüketir, daha kararsızdır vb. + +**Geliştirme** sırasında çok yardımcı olur, ancak **production** ortamında kullanmamalısınız. + +/// + +## Deployment Kavramları { #deployment-concepts } + +Bu örnekler server programını (ör. Uvicorn) çalıştırır; **tek bir process** başlatır, tüm IP'lerde (`0.0.0.0`) ve önceden belirlenmiş bir port'ta (ör. `80`) dinler. + +Temel fikir budur. Ancak muhtemelen şunlar gibi bazı ek konularla da ilgilenmek isteyeceksiniz: + +* Güvenlik - HTTPS +* Açılışta çalıştırma +* Yeniden başlatmalar +* Replikasyon (çalışan process sayısı) +* Bellek +* Başlatmadan önceki adımlar + +Sonraki bölümlerde bu kavramların her birini nasıl düşünmeniz gerektiğini ve bunlarla başa çıkmak için kullanabileceğiniz somut örnekleri/stratejileri anlatacağım. 🚀 diff --git a/docs/tr/docs/deployment/server-workers.md b/docs/tr/docs/deployment/server-workers.md new file mode 100644 index 000000000..faae4ef92 --- /dev/null +++ b/docs/tr/docs/deployment/server-workers.md @@ -0,0 +1,139 @@ +# Server Workers - Worker'larla Uvicorn { #server-workers-uvicorn-with-workers } + +Önceki bölümlerde bahsettiğimiz deployment kavramlarına tekrar bakalım: + +* Güvenlik - HTTPS +* Başlangıçta çalıştırma +* Yeniden başlatmalar +* **Replikasyon (çalışan process sayısı)** +* Bellek +* Başlatmadan önceki adımlar + +Bu noktaya kadar, dokümantasyondaki tüm tutorial'larla muhtemelen bir **server programı** çalıştırıyordunuz; örneğin Uvicorn'u çalıştıran `fastapi` komutunu kullanarak ve **tek bir process** ile. + +Uygulamaları deploy ederken, **çok çekirdekten (multiple cores)** faydalanmak ve daha fazla request'i karşılayabilmek için büyük olasılıkla **process replikasyonu** (birden fazla process) isteyeceksiniz. + +[Daha önceki Deployment Concepts](concepts.md){.internal-link target=_blank} bölümünde gördüğünüz gibi, kullanabileceğiniz birden fazla strateji var. + +Burada, `fastapi` komutunu kullanarak ya da `uvicorn` komutunu doğrudan çalıştırarak **worker process**'lerle **Uvicorn**'u nasıl kullanacağınızı göstereceğim. + +/// info | Bilgi + +Container kullanıyorsanız (örneğin Docker veya Kubernetes ile), bununla ilgili daha fazlasını bir sonraki bölümde anlatacağım: [Container'larda FastAPI - Docker](docker.md){.internal-link target=_blank}. + +Özellikle **Kubernetes** üzerinde çalıştırırken, büyük olasılıkla worker kullanmak **istemeyeceksiniz**; bunun yerine **container başına tek bir Uvicorn process** çalıştırmak daha uygundur. Ancak bunu da o bölümde detaylandıracağım. + +/// + +## Birden Fazla Worker { #multiple-workers } + +Komut satırında `--workers` seçeneğiyle birden fazla worker başlatabilirsiniz: + +//// tab | `fastapi` + +`fastapi` komutunu kullanıyorsanız: + +
+ +```console +$ fastapi run --workers 4 main.py + + FastAPI Starting production 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://0.0.0.0:8000 + server Documentation at http://0.0.0.0:8000/docs + + Logs: + + INFO Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to + quit) + INFO Started parent process [27365] + INFO Started server process [27368] + INFO Started server process [27369] + INFO Started server process [27370] + INFO Started server process [27367] + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Application startup complete. + INFO Application startup complete. + INFO Application startup complete. + INFO Application startup complete. +``` + +
+ +//// + +//// tab | `uvicorn` + +`uvicorn` komutunu doğrudan kullanmayı tercih ederseniz: + +
+ +```console +$ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 +INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit) +INFO: Started parent process [27365] +INFO: Started server process [27368] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27369] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27370] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27367] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +//// + +Buradaki tek yeni seçenek `--workers`; bu seçenek Uvicorn'a 4 adet worker process başlatmasını söyler. + +Ayrıca her process'in **PID**'inin gösterildiğini de görebilirsiniz: parent process için `27365` (bu **process manager**), her worker process için de bir PID: `27368`, `27369`, `27370` ve `27367`. + +## Deployment Kavramları { #deployment-concepts } + +Burada, uygulamanın çalışmasını **paralelleştirmek**, CPU'daki **çok çekirdekten** yararlanmak ve **daha fazla request** karşılayabilmek için birden fazla **worker**'ı nasıl kullanacağınızı gördünüz. + +Yukarıdaki deployment kavramları listesinden, worker kullanımı ağırlıklı olarak **replikasyon** kısmına yardımcı olur, ayrıca **yeniden başlatmalar** konusunda da az da olsa katkı sağlar. Ancak diğerlerini yine sizin yönetmeniz gerekir: + +* **Güvenlik - HTTPS** +* **Başlangıçta çalıştırma** +* ***Yeniden başlatmalar*** +* Replikasyon (çalışan process sayısı) +* **Bellek** +* **Başlatmadan önceki adımlar** + +## Container'lar ve Docker { #containers-and-docker } + +Bir sonraki bölümde, [Container'larda FastAPI - Docker](docker.md){.internal-link target=_blank} üzerinden diğer **deployment kavramlarını** ele almak için kullanabileceğiniz bazı stratejileri anlatacağım. + +Tek bir Uvicorn process çalıştıracak şekilde **sıfırdan kendi image'ınızı oluşturmayı** göstereceğim. Bu oldukça basit bir süreçtir ve **Kubernetes** gibi dağıtık bir container yönetim sistemi kullanırken büyük olasılıkla yapmak isteyeceğiniz şey de budur. + +## Özet { #recap } + +**Çok çekirdekli CPU**'lardan faydalanmak ve **birden fazla process'i paralel** çalıştırmak için `fastapi` veya `uvicorn` komutlarıyla `--workers` CLI seçeneğini kullanarak birden fazla worker process çalıştırabilirsiniz. + +Diğer deployment kavramlarını da kendiniz ele alarak **kendi deployment sisteminizi** kuruyorsanız, bu araçları ve fikirleri kullanabilirsiniz. + +Container'larla (örn. Docker ve Kubernetes) **FastAPI**'yi öğrenmek için bir sonraki bölüme göz atın. Bu araçların, diğer **deployment kavramlarını** çözmek için de basit yöntemleri olduğunu göreceksiniz. ✨ diff --git a/docs/tr/docs/deployment/versions.md b/docs/tr/docs/deployment/versions.md new file mode 100644 index 000000000..c3fb5d9bd --- /dev/null +++ b/docs/tr/docs/deployment/versions.md @@ -0,0 +1,93 @@ +# FastAPI Sürümleri Hakkında { #about-fastapi-versions } + +**FastAPI** hâlihazırda birçok uygulama ve sistemde production ortamında kullanılmaktadır. Ayrıca test kapsamı %100 seviyesinde tutulmaktadır. Ancak geliştirme süreci hâlâ hızlı şekilde ilerlemektedir. + +Yeni özellikler sık sık eklenir, bug'lar düzenli olarak düzeltilir ve kod sürekli iyileştirilmektedir. + +Bu yüzden mevcut sürümler hâlâ `0.x.x` şeklindedir; bu da her sürümde breaking change olma ihtimalini yansıtır. Bu yaklaşım Semantic Versioning kurallarını takip eder. + +Şu anda **FastAPI** ile production uygulamaları geliştirebilirsiniz (muhtemelen bir süredir yapıyorsunuz da); sadece kodunuzun geri kalanıyla doğru çalışan bir sürüm kullandığınızdan emin olmanız gerekir. + +## `fastapi` sürümünü sabitleyin { #pin-your-fastapi-version } + +İlk yapmanız gereken, kullandığınız **FastAPI** sürümünü uygulamanızla doğru çalıştığını bildiğiniz belirli bir güncel sürüme "sabitlemek" (pinlemek) olmalı. + +Örneğin, uygulamanızda `0.112.0` sürümünü kullandığınızı varsayalım. + +`requirements.txt` dosyası kullanıyorsanız sürümü şöyle belirtebilirsiniz: + +```txt +fastapi[standard]==0.112.0 +``` + +Bu, tam olarak `0.112.0` sürümünü kullanacağınız anlamına gelir. + +Ya da şu şekilde de sabitleyebilirsiniz: + +```txt +fastapi[standard]>=0.112.0,<0.113.0 +``` + +Bu da `0.112.0` ve üzeri, ama `0.113.0` altındaki sürümleri kullanacağınız anlamına gelir; örneğin `0.112.2` gibi bir sürüm de kabul edilir. + +Kurulumları yönetmek için `uv`, Poetry, Pipenv gibi başka bir araç (veya benzerleri) kullanıyorsanız, bunların hepsinde paketler için belirli sürümler tanımlamanın bir yolu vardır. + +## Mevcut sürümler { #available-versions } + +Mevcut sürümleri (ör. en güncel son sürümün hangisi olduğunu kontrol etmek için) [Release Notes](../release-notes.md){.internal-link target=_blank} sayfasında görebilirsiniz. + +## Sürümler Hakkında { #about-versions } + +Semantic Versioning kurallarına göre, `1.0.0` altındaki herhangi bir sürüm breaking change içerebilir. + +FastAPI ayrıca "PATCH" sürüm değişikliklerinin bug fix'ler ve breaking olmayan değişiklikler için kullanılması kuralını da takip eder. + +/// tip | İpucu + +"PATCH" son sayıdır. Örneğin `0.2.3` içinde PATCH sürümü `3`'tür. + +/// + +Dolayısıyla şu şekilde bir sürüme sabitleyebilmelisiniz: + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +Breaking change'ler ve yeni özellikler "MINOR" sürümlerde eklenir. + +/// tip | İpucu + +"MINOR" ortadaki sayıdır. Örneğin `0.2.3` içinde MINOR sürümü `2`'dir. + +/// + +## FastAPI Sürümlerini Yükseltme { #upgrading-the-fastapi-versions } + +Uygulamanız için test'ler eklemelisiniz. + +**FastAPI** ile bu çok kolaydır (Starlette sayesinde). Dokümantasyona bakın: [Testing](../tutorial/testing.md){.internal-link target=_blank} + +Test'leriniz olduktan sonra **FastAPI** sürümünü daha yeni bir sürüme yükseltebilir ve test'lerinizi çalıştırarak tüm kodunuzun doğru çalıştığından emin olabilirsiniz. + +Her şey çalışıyorsa (ya da gerekli değişiklikleri yaptıktan sonra) ve tüm test'leriniz geçiyorsa, `fastapi` sürümünü o yeni sürüme sabitleyebilirsiniz. + +## Starlette Hakkında { #about-starlette } + +`starlette` sürümünü sabitlememelisiniz. + +**FastAPI**'nin farklı sürümleri, Starlette'in belirli (daha yeni) bir sürümünü kullanır. + +Bu yüzden **FastAPI**'nin doğru Starlette sürümünü kullanmasına izin verebilirsiniz. + +## Pydantic Hakkında { #about-pydantic } + +Pydantic, **FastAPI** için olan test'leri kendi test'lerinin içine dahil eder; bu yüzden Pydantic'in yeni sürümleri (`1.0.0` üzeri) her zaman FastAPI ile uyumludur. + +Pydantic'i sizin için çalışan `1.0.0` üzerindeki herhangi bir sürüme sabitleyebilirsiniz. + +Örneğin: + +```txt +pydantic>=2.7.0,<3.0.0 +``` diff --git a/docs/tr/docs/environment-variables.md b/docs/tr/docs/environment-variables.md new file mode 100644 index 000000000..e4f769a16 --- /dev/null +++ b/docs/tr/docs/environment-variables.md @@ -0,0 +1,298 @@ +# Ortam Değişkenleri { #environment-variables } + +/// tip | İpucu + +"Ortam değişkenleri"nin ne olduğunu ve nasıl kullanılacağını zaten biliyorsanız, bu bölümü atlayabilirsiniz. + +/// + +Ortam değişkeni (genelde "**env var**" olarak da anılır), Python kodunun **dışında**, **işletim sistemi** seviyesinde bulunan ve Python kodunuz (veya diğer programlar) tarafından okunabilen bir değişkendir. + +Ortam değişkenleri; uygulama **ayarları**nı yönetmek, Python’un **kurulumu**nun bir parçası olarak konfigürasyon yapmak vb. durumlarda işe yarar. + +## Env Var Oluşturma ve Kullanma { #create-and-use-env-vars } + +Python’a ihtiyaç duymadan, **shell (terminal)** içinde ortam değişkenleri **oluşturabilir** ve kullanabilirsiniz: + +//// 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’da env var Okuma { #read-env-vars-in-python } + +Ortam değişkenlerini Python’un **dışında** (terminalde veya başka bir yöntemle) oluşturup daha sonra **Python’da okuyabilirsiniz**. + +Örneğin `main.py` adında bir dosyanız şöyle olabilir: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +/// tip | İpucu + +`os.getenv()` fonksiyonunun ikinci argümanı, bulunamadığında döndürülecek varsayılan (default) değerdir. + +Verilmezse varsayılan olarak `None` olur; burada varsayılan değer olarak `"World"` verdik. + +/// + +Sonrasında bu Python programını çalıştırabilirsiniz: + +//// 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 +``` + +
+ +//// + +Ortam değişkenleri kodun dışında ayarlanabildiği, ama kod tarafından okunabildiği ve dosyalarla birlikte saklanmasının (ör. `git`’e commit edilmesinin) gerekmediği için, konfigürasyon veya **ayarlar** için sıkça kullanılır. + +Ayrıca, bir ortam değişkenini yalnızca **belirli bir program çalıştırımı** için oluşturabilirsiniz; bu değişken sadece o program tarafından, sadece o çalıştırma süresince kullanılabilir. + +Bunu yapmak için, program komutunun hemen öncesinde ve aynı satırda tanımlayın: + +
+ +```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 | İpucu + +Bu konuyla ilgili daha fazlasını The Twelve-Factor App: Config bölümünde okuyabilirsiniz. + +/// + +## Tipler ve Doğrulama { #types-and-validation } + +Bu ortam değişkenleri yalnızca **metin string**’lerini taşıyabilir. Çünkü Python’un dışındadırlar ve diğer programlarla, sistemin geri kalanıyla (hatta Linux, Windows, macOS gibi farklı işletim sistemleriyle) uyumlu olmak zorundadırlar. + +Bu, Python’da bir ortam değişkeninden okunan **her değerin `str` olacağı** anlamına gelir. Farklı bir tipe dönüştürme veya doğrulama işlemleri kod içinde yapılmalıdır. + +Uygulama **ayarları**nı yönetmek için ortam değişkenlerini kullanmayı, [İleri Seviye Kullanıcı Rehberi - Ayarlar ve Ortam Değişkenleri](./advanced/settings.md){.internal-link target=_blank} bölümünde daha detaylı öğreneceksiniz. + +## `PATH` Ortam Değişkeni { #path-environment-variable } + +İşletim sistemlerinin (Linux, macOS, Windows) çalıştırılacak programları bulmak için kullandığı **özel** bir ortam değişkeni vardır: **`PATH`**. + +`PATH` değişkeninin değeri uzun bir string’dir; Linux ve macOS’te dizinler iki nokta üst üste `:` ile, Windows’ta ise noktalı virgül `;` ile ayrılır. + +Örneğin `PATH` ortam değişkeni şöyle görünebilir: + +//// tab | Linux, macOS + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +Bu, sistemin şu dizinlerde program araması gerektiği anlamına gelir: + +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 +``` + +Bu, sistemin şu dizinlerde program araması gerektiği anlamına gelir: + +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` + +//// + +Terminalde bir **komut** yazdığınızda, işletim sistemi `PATH` ortam değişkeninde listelenen **bu dizinlerin her birinde** programı **arar**. + +Örneğin terminalde `python` yazdığınızda, işletim sistemi bu listedeki **ilk dizinde** `python` adlı bir program arar. + +Bulursa **onu kullanır**. Bulamazsa **diğer dizinlerde** aramaya devam eder. + +### Python Kurulumu ve `PATH`’in Güncellenmesi { #installing-python-and-updating-the-path } + +Python’u kurarken, `PATH` ortam değişkenini güncellemek isteyip istemediğiniz sorulabilir. + +//// tab | Linux, macOS + +Diyelim ki Python’u kurdunuz ve `/opt/custompython/bin` dizinine yüklendi. + +`PATH` ortam değişkenini güncellemeyi seçerseniz, kurulum aracı `/opt/custompython/bin` yolunu `PATH` ortam değişkenine ekler. + +Şöyle görünebilir: + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin +``` + +Böylece terminalde `python` yazdığınızda, sistem `/opt/custompython/bin` (son dizin) içindeki Python programını bulur ve onu kullanır. + +//// + +//// tab | Windows + +Diyelim ki Python’u kurdunuz ve `C:\opt\custompython\bin` dizinine yüklendi. + +`PATH` ortam değişkenini güncellemeyi seçerseniz, kurulum aracı `C:\opt\custompython\bin` yolunu `PATH` ortam değişkenine ekler. + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin +``` + +Böylece terminalde `python` yazdığınızda, sistem `C:\opt\custompython\bin` (son dizin) içindeki Python programını bulur ve onu kullanır. + +//// + +Yani şunu yazarsanız: + +
+ +```console +$ python +``` + +
+ +//// tab | Linux, macOS + +Sistem `python` programını `/opt/custompython/bin` içinde **bulur** ve çalıştırır. + +Bu, kabaca şunu yazmaya denktir: + +
+ +```console +$ /opt/custompython/bin/python +``` + +
+ +//// + +//// tab | Windows + +Sistem `python` programını `C:\opt\custompython\bin\python` içinde **bulur** ve çalıştırır. + +Bu, kabaca şunu yazmaya denktir: + +
+ +```console +$ C:\opt\custompython\bin\python +``` + +
+ +//// + +Bu bilgiler, [Virtual Environments](virtual-environments.md){.internal-link target=_blank} konusunu öğrenirken işinize yarayacak. + +## Sonuç { #conclusion } + +Buraya kadar **ortam değişkenleri**nin ne olduğuna ve Python’da nasıl kullanılacağına dair temel bir fikir edinmiş olmalısınız. + +Ayrıca Wikipedia for Environment Variable sayfasından daha fazlasını da okuyabilirsiniz. + +Çoğu zaman ortam değişkenlerinin hemen nasıl işe yarayacağı ilk bakışta çok net olmayabilir. Ancak geliştirme yaparken birçok farklı senaryoda tekrar tekrar karşınıza çıkarlar; bu yüzden bunları bilmek faydalıdır. + +Örneğin bir sonraki bölümde, [Virtual Environments](virtual-environments.md) konusunda bu bilgilere ihtiyaç duyacaksınız. diff --git a/docs/tr/docs/fastapi-cli.md b/docs/tr/docs/fastapi-cli.md new file mode 100644 index 000000000..4680d4bb6 --- /dev/null +++ b/docs/tr/docs/fastapi-cli.md @@ -0,0 +1,75 @@ +# FastAPI CLI { #fastapi-cli } + +**FastAPI CLI**, FastAPI uygulamanızı servis etmek, FastAPI projenizi yönetmek ve daha fazlası için kullanabileceğiniz bir komut satırı programıdır. + +FastAPI'yi kurduğunuzda (ör. `pip install "fastapi[standard]"`), beraberinde `fastapi-cli` adlı bir paket de gelir; bu paket terminalde `fastapi` komutunu sağlar. + +FastAPI uygulamanızı geliştirme için çalıştırmak üzere `fastapi dev` komutunu kullanabilirsiniz: + +
+ +```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. +``` + +
+ +`fastapi` adlı bu komut satırı programı, **FastAPI CLI**'dır. + +FastAPI CLI, Python programınızın path'ini (ör. `main.py`) alır; `FastAPI` instance'ını (genellikle `app` olarak adlandırılır) otomatik olarak tespit eder, doğru import sürecini belirler ve ardından uygulamayı servis eder. + +Production için bunun yerine `fastapi run` kullanırsınız. 🚀 + +İçeride **FastAPI CLI**, yüksek performanslı, production'a hazır bir ASGI server olan Uvicorn'u kullanır. 😎 + +## `fastapi dev` { #fastapi-dev } + +`fastapi dev` çalıştırmak, geliştirme modunu başlatır. + +Varsayılan olarak **auto-reload** etkindir; kodunuzda değişiklik yaptığınızda server'ı otomatik olarak yeniden yükler. Bu, kaynak tüketimi yüksek bir özelliktir ve kapalı olduğuna kıyasla daha az stabil olabilir. Sadece geliştirme sırasında kullanmalısınız. Ayrıca yalnızca `127.0.0.1` IP adresini dinler; bu, makinenizin sadece kendisiyle iletişim kurması için kullanılan IP'dir (`localhost`). + +## `fastapi run` { #fastapi-run } + +`fastapi run` çalıştırmak, varsayılan olarak FastAPI'yi production modunda başlatır. + +Varsayılan olarak **auto-reload** kapalıdır. Ayrıca `0.0.0.0` IP adresini dinler; bu, kullanılabilir tüm IP adresleri anlamına gelir. Böylece makineyle iletişim kurabilen herkes tarafından genel erişime açık olur. Bu, normalde production'da çalıştırma şeklidir; örneğin bir container içinde. + +Çoğu durumda (ve genellikle yapmanız gereken şekilde) üst tarafta sizin yerinize HTTPS'i yöneten bir "termination proxy" bulunur. Bu, uygulamanızı nasıl deploy ettiğinize bağlıdır; sağlayıcınız bunu sizin için yapabilir ya da sizin ayrıca kurmanız gerekebilir. + +/// tip | İpucu + +Bununla ilgili daha fazla bilgiyi [deployment dokümantasyonunda](deployment/index.md){.internal-link target=_blank} bulabilirsiniz. + +/// diff --git a/docs/tr/docs/help-fastapi.md b/docs/tr/docs/help-fastapi.md new file mode 100644 index 000000000..785c0ae0d --- /dev/null +++ b/docs/tr/docs/help-fastapi.md @@ -0,0 +1,256 @@ +# FastAPI'ye Yardım Et - Yardım Al { #help-fastapi-get-help } + +**FastAPI**'yi seviyor musunuz? + +FastAPI'ye, diğer kullanıcılara ve yazara yardım etmek ister misiniz? + +Yoksa **FastAPI** ile ilgili yardım mı almak istiyorsunuz? + +Yardım etmenin çok basit yolları var (bazıları sadece bir-iki tıklama gerektirir). + +Yardım almanın da birkaç yolu var. + +## Bültene abone olun { #subscribe-to-the-newsletter } + +Şunlardan haberdar olmak için (seyrek yayımlanan) [**FastAPI and friends** bültenine](newsletter.md){.internal-link target=_blank} abone olabilirsiniz: + +* FastAPI ve friends ile ilgili haberler 🚀 +* Rehberler 📝 +* Özellikler ✨ +* Geriye dönük uyumsuz değişiklikler 🚨 +* İpuçları ve püf noktaları ✅ + +## X (Twitter) üzerinden FastAPI'yi takip edin { #follow-fastapi-on-x-twitter } + +**FastAPI** ile ilgili en güncel haberleri almak için @fastapi hesabını **X (Twitter)** üzerinde takip edin. 🐦 + +## GitHub'da **FastAPI**'ye yıldız verin { #star-fastapi-in-github } + +GitHub'da FastAPI'ye "star" verebilirsiniz (sağ üstteki yıldız butonuna tıklayarak): https://github.com/fastapi/fastapi. ⭐️ + +Yıldız verince, diğer kullanıcılar projeyi daha kolay bulabilir ve başkaları için de faydalı olduğunu görebilir. + +## GitHub repository'sini release'ler için izleyin { #watch-the-github-repository-for-releases } + +GitHub'da FastAPI'yi "watch" edebilirsiniz (sağ üstteki "watch" butonuna tıklayarak): https://github.com/fastapi/fastapi. 👀 + +Orada "Releases only" seçebilirsiniz. + +Böylece **FastAPI**'nin bug fix'ler ve yeni özelliklerle gelen her yeni release'inde (yeni versiyonunda) email ile bildirim alırsınız. + +## Yazarla bağlantı kurun { #connect-with-the-author } + +Yazar olan benimle (Sebastián Ramírez / `tiangolo`) bağlantı kurabilirsiniz. + +Şunları yapabilirsiniz: + +* Beni **GitHub**'da takip edin. + * Size yardımcı olabilecek oluşturduğum diğer Open Source projelere göz atın. + * Yeni bir Open Source proje oluşturduğumda haberdar olmak için beni takip edin. +* Beni **X (Twitter)** üzerinde veya Mastodon'da takip edin. + * FastAPI'yi nasıl kullandığınızı anlatın (bunu duymayı seviyorum). + * Duyuru yaptığımda veya yeni araçlar yayınladığımda haberdar olun. + * Ayrıca (ayrı bir hesap olan) X (Twitter) üzerinde @fastapi hesabını da takip edebilirsiniz. +* Beni **LinkedIn**'de takip edin. + * Duyuru yaptığımda veya yeni araçlar yayınladığımda haberdar olun (gerçi X (Twitter)'ı daha sık kullanıyorum 🤷‍♂). +* **Dev.to** veya **Medium** üzerinde yazdıklarımı okuyun (ya da beni takip edin). + * Diğer fikirleri, yazıları ve oluşturduğum araçlarla ilgili içerikleri okuyun. + * Yeni bir şey yayınladığımda görmek için beni takip edin. + +## **FastAPI** hakkında tweet atın { #tweet-about-fastapi } + +**FastAPI** hakkında tweet atın ve neden sevdiğinizi bana ve diğerlerine söyleyin. 🎉 + +**FastAPI**'nin nasıl kullanıldığını, nelerini sevdiğinizi, hangi projede/şirkette kullandığınızı vb. duymayı seviyorum. + +## FastAPI için oy verin { #vote-for-fastapi } + +* Slant'ta **FastAPI** için oy verin. +* AlternativeTo'da **FastAPI** için oy verin. +* StackShare'de **FastAPI** kullandığınızı belirtin. + +## GitHub'da sorularla başkalarına yardım edin { #help-others-with-questions-in-github } + +Şuralarda insanların sorularına yardımcı olmayı deneyebilirsiniz: + +* GitHub Discussions +* GitHub Issues + +Birçok durumda bu soruların cevabını zaten biliyor olabilirsiniz. 🤓 + +Eğer insanların sorularına çok yardım ederseniz, resmi bir [FastAPI Expert](fastapi-people.md#fastapi-experts){.internal-link target=_blank} olabilirsiniz. 🎉 + +Şunu unutmayın: en önemli nokta, nazik olmaya çalışmak. İnsanlar çoğu zaman biriken stresle geliyor ve birçok durumda soruyu en iyi şekilde sormuyor; yine de elinizden geldiğince nazik olmaya çalışın. 🤗 + +Amaç, **FastAPI** topluluğunun nazik ve kapsayıcı olması. Aynı zamanda başkalarına zorbalık ya da saygısız davranışları da kabul etmeyin. Birbirimizi kollamalıyız. + +--- + +Sorularda (discussions veya issues içinde) başkalarına yardım etmek için şunları yapabilirsiniz: + +### Soruyu anlayın { #understand-the-question } + +* Soru soran kişinin **amacının** ve kullanım senaryosunun ne olduğunu anlayabiliyor musunuz, kontrol edin. + +* Sonra sorunun (büyük çoğunluğu soru olur) **net** olup olmadığına bakın. + +* Birçok durumda kullanıcı kafasında hayali bir çözüm kurup onu sorar; ancak **daha iyi** bir çözüm olabilir. Problemi ve kullanım senaryosunu daha iyi anladıysanız daha iyi bir **alternatif çözüm** önerebilirsiniz. + +* Soruyu anlayamıyorsanız daha fazla **detay** isteyin. + +### Problemi yeniden üretin { #reproduce-the-problem } + +Çoğu durumda ve çoğu soruda, kişinin **orijinal kodu** ile ilgili bir şey vardır. + +Birçok kişi sadece kodun bir parçasını kopyalar, ama bu **problemi yeniden üretmek** için yeterli olmaz. + +* Çalıştırıp aynı hatayı/davranışı görebileceğiniz veya kullanım senaryosunu daha iyi anlayabileceğiniz, yerelde **kopyala-yapıştır** yaparak çalıştırılabilen bir minimal, reproducible, example paylaşmalarını isteyebilirsiniz. + +* Çok cömert hissediyorsanız, problemi anlatan açıklamadan yola çıkarak kendiniz de böyle bir **örnek oluşturmayı** deneyebilirsiniz. Ancak bunun çok zaman alabileceğini unutmayın; çoğu zaman önce problemi netleştirmelerini istemek daha iyidir. + +### Çözüm önerin { #suggest-solutions } + +* Soruyu anlayabildikten sonra olası bir **cevap** verebilirsiniz. + +* Çoğu durumda, yapmak istediklerinden ziyade alttaki **asıl problemi veya kullanım senaryosunu** anlamak daha iyidir; çünkü denedikleri yöntemden daha iyi bir çözüm yolu olabilir. + +### Kapatılmasını isteyin { #ask-to-close } + +Eğer yanıt verirlerse, büyük ihtimalle problemi çözmüşsünüzdür, tebrikler, **kahramansınız**! 🦸 + +* Eğer çözüm işe yaradıysa şunları yapmalarını isteyebilirsiniz: + + * GitHub Discussions'ta: ilgili yorumu **answer** olarak işaretlemeleri. + * GitHub Issues'ta: issue'yu **close** etmeleri. + +## GitHub repository'sini izleyin { #watch-the-github-repository } + +GitHub'da FastAPI'yi "watch" edebilirsiniz (sağ üstteki "watch" butonuna tıklayarak): https://github.com/fastapi/fastapi. 👀 + +"Releases only" yerine "Watching" seçerseniz biri yeni bir issue veya soru oluşturduğunda bildirim alırsınız. Ayrıca sadece yeni issue'lar, ya da discussions, ya da PR'lar vb. için bildirim almak istediğinizi belirtebilirsiniz. + +Sonra da bu soruları çözmelerine yardımcı olmayı deneyebilirsiniz. + +## Soru Sorun { #ask-questions } + +GitHub repository'sinde örneğin şunlar için yeni bir soru oluşturabilirsiniz: + +* Bir **soru** sorun veya bir **problem** hakkında danışın. +* Yeni bir **feature** önerin. + +**Not**: Bunu yaparsanız, ben de sizden başkalarına yardım etmenizi isteyeceğim. 😉 + +## Pull Request'leri İnceleyin { #review-pull-requests } + +Başkalarının gönderdiği pull request'leri incelememde bana yardımcı olabilirsiniz. + +Yine, lütfen elinizden geldiğince nazik olmaya çalışın. 🤗 + +--- + +Bir pull request'i incelerken akılda tutmanız gerekenler: + +### Problemi anlayın { #understand-the-problem } + +* Önce, pull request'in çözmeye çalıştığı **problemi anladığınızdan** emin olun. GitHub Discussion veya issue içinde daha uzun bir tartışması olabilir. + +* Pull request'in aslında hiç gerekmiyor olma ihtimali de yüksektir; çünkü problem **farklı bir şekilde** çözülebilir. Bu durumda bunu önerebilir veya bununla ilgili soru sorabilirsiniz. + +### Style konusunda çok dert etmeyin { #dont-worry-about-style } + +* Commit message tarzı gibi şeyleri çok dert etmeyin; ben commit'leri manuel olarak düzenleyerek squash and merge yapacağım. + +* Style kuralları için de endişelenmeyin; bunları kontrol eden otomatik araçlar zaten var. + +Ek bir style veya tutarlılık ihtiyacı olursa, bunu doğrudan isterim ya da gerekli değişikliklerle üstüne commit eklerim. + +### Kodu kontrol edin { #check-the-code } + +* Kodu okuyup kontrol edin; mantıklı mı bakın, **yerelde çalıştırın** ve gerçekten problemi çözüyor mu görün. + +* Ardından bunu yaptığınızı belirten bir **yorum** yazın; böylece gerçekten kontrol ettiğinizi anlarım. + +/// info | Bilgi + +Ne yazık ki sadece birkaç onayı olan PR'lara körü körüne güvenemem. + +Defalarca, 3, 5 veya daha fazla onayı olan PR'lar oldu; muhtemelen açıklaması çekici olduğu için onay aldılar. Ama PR'lara baktığımda aslında bozuk olduklarını, bug içerdiğini veya iddia ettikleri problemi çözmediklerini gördüm. 😅 + +Bu yüzden kodu gerçekten okuyup çalıştırmanız ve bunu yorumlarda bana bildirmeniz çok önemli. 🤓 + +/// + +* PR bir şekilde basitleştirilebiliyorsa bunu isteyebilirsiniz. Ancak çok didik didik etmeye gerek yok; konuya göre birçok öznel bakış açısı olabilir (benim de olacaktır 🙈). Bu yüzden temel noktalara odaklanmak daha iyi. + +### Testler { #tests } + +* PR'da **testler** olduğunu kontrol etmemde bana yardımcı olun. + +* PR'dan önce testlerin **fail** ettiğini kontrol edin. 🚨 + +* PR'dan sonra testlerin **pass** ettiğini kontrol edin. ✅ + +* Birçok PR test içermez; test eklemelerini **hatırlatabilirsiniz** veya hatta kendiniz bazı testler **önerebilirsiniz**. Bu, en çok zaman alan işlerden biridir ve burada çok yardımcı olabilirsiniz. + +* Ayrıca neleri denediğinizi yorumlara yazın; böylece kontrol ettiğinizi anlarım. 🤓 + +## Pull Request Oluşturun { #create-a-pull-request } + +Örneğin şunlar için Pull Request'lerle kaynak koda [katkıda bulunabilirsiniz](contributing.md){.internal-link target=_blank}: + +* Dokümantasyonda bulduğunuz bir yazım hatasını düzeltmek. +* FastAPI hakkında oluşturduğunuz veya bulduğunuz bir makaleyi, videoyu ya da podcast'i bu dosyayı düzenleyerek paylaşmak. + * Link'inizi ilgili bölümün başına eklediğinizden emin olun. +* Dokümantasyonu kendi dilinize [çevirmeye yardımcı olmak](contributing.md#translations){.internal-link target=_blank}. + * Başkalarının yaptığı çevirileri gözden geçirmeye de yardımcı olabilirsiniz. +* Yeni dokümantasyon bölümleri önermek. +* Mevcut bir issue/bug'ı düzeltmek. + * Test eklediğinizden emin olun. +* Yeni bir feature eklemek. + * Test eklediğinizden emin olun. + * İlgiliyse dokümantasyon da eklediğinizden emin olun. + +## FastAPI'nin Bakımına Yardım Edin { #help-maintain-fastapi } + +**FastAPI**'nin bakımını yapmama yardımcı olun! 🤓 + +Yapılacak çok iş var ve bunların çoğunu **SİZ** yapabilirsiniz. + +Şu anda yapabileceğiniz ana işler: + +* [GitHub'da sorularla başkalarına yardım edin](#help-others-with-questions-in-github){.internal-link target=_blank} (yukarıdaki bölüme bakın). +* [Pull Request'leri inceleyin](#review-pull-requests){.internal-link target=_blank} (yukarıdaki bölüme bakın). + +Bu iki iş, **en çok zamanı alan** işlerdir. FastAPI bakımının ana yükü buradadır. + +Burada yardımcı olursanız, **FastAPI'nin bakımını yapmama yardım etmiş** ve daha **hızlı ve daha iyi ilerlemesini** sağlamış olursunuz. 🚀 + +## Sohbete katılın { #join-the-chat } + +FastAPI topluluğundan diğer kişilerle takılmak için 👥 Discord chat server 👥 sohbetine katılın. + +/// tip | İpucu + +Sorular için GitHub Discussions'a sorun; [FastAPI Experts](fastapi-people.md#fastapi-experts){.internal-link target=_blank} tarafından yardım alma ihtimaliniz çok daha yüksektir. + +Chat'i sadece genel sohbetler için kullanın. + +/// + +### Sorular için chat'i kullanmayın { #dont-use-the-chat-for-questions } + +Chat sistemleri daha "serbest sohbet"e izin verdiği için, çok genel ve yanıtlaması daha zor sorular sormak kolaylaşır; bu nedenle cevap alamayabilirsiniz. + +GitHub'da ise şablon (template) doğru soruyu yazmanız için sizi yönlendirir; böylece daha kolay iyi bir cevap alabilir, hatta bazen sormadan önce problemi kendiniz çözebilirsiniz. Ayrıca GitHub'da (zaman alsa bile) her şeye mutlaka cevap verdiğimden emin olabilirim. Chat sistemlerinde bunu kişisel olarak yapamam. 😅 + +Chat sistemlerindeki konuşmalar GitHub kadar kolay aranabilir değildir; bu yüzden soru ve cevaplar sohbet içinde kaybolabilir. Ayrıca [FastAPI Expert](fastapi-people.md#fastapi-experts){.internal-link target=_blank} olmak için sadece GitHub'daki katkılar sayılır; dolayısıyla büyük olasılıkla GitHub'da daha fazla ilgi görürsünüz. + +Öte yandan chat sistemlerinde binlerce kullanıcı vardır; bu yüzden neredeyse her zaman konuşacak birini bulma ihtimaliniz yüksektir. 😄 + +## Yazara sponsor olun { #sponsor-the-author } + +Eğer **ürününüz/şirketiniz** **FastAPI**'ye bağlıysa veya onunla ilişkiliyse ve FastAPI kullanıcılarına ulaşmak istiyorsanız, GitHub sponsors üzerinden yazara (bana) sponsor olabilirsiniz. Tier'a göre dokümantasyonda bir rozet gibi ek faydalar elde edebilirsiniz. 🎁 + +--- + +Teşekkürler! 🚀 diff --git a/docs/tr/docs/how-to/authentication-error-status-code.md b/docs/tr/docs/how-to/authentication-error-status-code.md new file mode 100644 index 000000000..579673624 --- /dev/null +++ b/docs/tr/docs/how-to/authentication-error-status-code.md @@ -0,0 +1,17 @@ +# Eski 403 Kimlik Doğrulama Hata Durum Kodlarını Kullanma { #use-old-403-authentication-error-status-codes } + +FastAPI `0.122.0` sürümünden önce, entegre security yardımcı araçları başarısız bir kimlik doğrulama (authentication) sonrasında client'a bir hata döndüğünde HTTP durum kodu olarak `403 Forbidden` kullanıyordu. + +FastAPI `0.122.0` sürümünden itibaren ise daha uygun olan HTTP durum kodu `401 Unauthorized` kullanılmakta ve HTTP spesifikasyonlarına uygun olarak response içinde anlamlı bir `WWW-Authenticate` header'ı döndürülmektedir: RFC 7235, RFC 9110. + +Ancak herhangi bir nedenle client'larınız eski davranışa bağlıysa, security class'larınızda `make_not_authenticated_error` metodunu override ederek eski davranışa geri dönebilirsiniz. + +Örneğin, varsayılan `401 Unauthorized` hatası yerine `403 Forbidden` hatası döndüren bir `HTTPBearer` alt sınıfı oluşturabilirsiniz: + +{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *} + +/// tip | İpucu + +Fonksiyonun exception instance'ını döndürdüğüne dikkat edin; exception'ı raise etmiyor. Raise işlemi internal kodun geri kalan kısmında yapılıyor. + +/// diff --git a/docs/tr/docs/how-to/conditional-openapi.md b/docs/tr/docs/how-to/conditional-openapi.md new file mode 100644 index 000000000..9562637c4 --- /dev/null +++ b/docs/tr/docs/how-to/conditional-openapi.md @@ -0,0 +1,56 @@ +# Koşullu OpenAPI { #conditional-openapi } + +Gerekirse, ayarlar ve environment variable'ları kullanarak OpenAPI'yi ortama göre koşullu şekilde yapılandırabilir, hatta tamamen devre dışı bırakabilirsiniz. + +## Güvenlik, API'ler ve dokümantasyon hakkında { #about-security-apis-and-docs } + +Production ortamında dokümantasyon arayüzlerini gizlemek, API'nizi korumanın yolu *olmamalıdır*. + +Bu, API'nize ekstra bir güvenlik katmanı eklemez; *path operation*'lar bulundukları yerde yine erişilebilir olacaktır. + +Kodunuzda bir güvenlik açığı varsa, o açık yine var olmaya devam eder. + +Dokümantasyonu gizlemek, API'nizle nasıl etkileşime geçileceğini anlamayı zorlaştırır ve production'da debug etmeyi de daha zor hale getirebilir. Bu yaklaşım, basitçe Security through obscurity olarak değerlendirilebilir. + +API'nizi güvence altına almak istiyorsanız, yapabileceğiniz daha iyi birçok şey var; örneğin: + +* request body'leriniz ve response'larınız için iyi tanımlanmış Pydantic model'larına sahip olduğunuzdan emin olun. +* dependencies kullanarak gerekli izinleri ve rolleri yapılandırın. +* Asla düz metin (plaintext) şifre saklamayın, yalnızca password hash'leri saklayın. +* pwdlib ve JWT token'ları gibi, iyi bilinen kriptografik araçları uygulayın ve kullanın. +* Gerektiğinde OAuth2 scope'ları ile daha ayrıntılı izin kontrolleri ekleyin. +* ...vb. + +Yine de, bazı ortamlarda (örn. production) veya environment variable'lardan gelen konfigürasyonlara bağlı olarak API docs'u gerçekten devre dışı bırakmanız gereken çok spesifik bir use case'iniz olabilir. + +## Ayarlar ve env var'lar ile koşullu OpenAPI { #conditional-openapi-from-settings-and-env-vars } + +Üretilen OpenAPI'yi ve docs UI'larını yapılandırmak için aynı Pydantic settings'i kolayca kullanabilirsiniz. + +Örneğin: + +{* ../../docs_src/conditional_openapi/tutorial001_py39.py hl[6,11] *} + +Burada `openapi_url` ayarını, varsayılanı `"/openapi.json"` olacak şekilde tanımlıyoruz. + +Ardından `FastAPI` app'ini oluştururken bunu kullanıyoruz. + +Sonrasında, environment variable `OPENAPI_URL`'i boş string olarak ayarlayarak OpenAPI'yi (UI docs dahil) devre dışı bırakabilirsiniz; örneğin: + +
+ +```console +$ OPENAPI_URL= uvicorn main:app + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Böylece `/openapi.json`, `/docs` veya `/redoc` URL'lerine giderseniz, aşağıdaki gibi bir `404 Not Found` hatası alırsınız: + +```JSON +{ + "detail": "Not Found" +} +``` diff --git a/docs/tr/docs/how-to/configure-swagger-ui.md b/docs/tr/docs/how-to/configure-swagger-ui.md new file mode 100644 index 000000000..6c051a121 --- /dev/null +++ b/docs/tr/docs/how-to/configure-swagger-ui.md @@ -0,0 +1,70 @@ +# Swagger UI'yi Yapılandırın { #configure-swagger-ui } + +Bazı ek Swagger UI parametrelerini yapılandırabilirsiniz. + +Bunları yapılandırmak için, `FastAPI()` uygulama nesnesini oluştururken ya da `get_swagger_ui_html()` fonksiyonuna `swagger_ui_parameters` argümanını verin. + +`swagger_ui_parameters`, Swagger UI'ye doğrudan iletilecek yapılandırmaları içeren bir `dict` alır. + +FastAPI, Swagger UI'nin ihtiyaç duyduğu şekilde JavaScript ile uyumlu olsun diye bu yapılandırmaları **JSON**'a dönüştürür. + +## Syntax Highlighting'i Devre Dışı Bırakın { #disable-syntax-highlighting } + +Örneğin, Swagger UI'de syntax highlighting'i devre dışı bırakabilirsiniz. + +Ayarları değiştirmeden bırakırsanız, syntax highlighting varsayılan olarak etkindir: + + + +Ancak `syntaxHighlight` değerini `False` yaparak devre dışı bırakabilirsiniz: + +{* ../../docs_src/configure_swagger_ui/tutorial001_py39.py hl[3] *} + +...ve ardından Swagger UI artık syntax highlighting'i göstermeyecektir: + + + +## Temayı Değiştirin { #change-the-theme } + +Aynı şekilde, `"syntaxHighlight.theme"` anahtarıyla (ortasında bir nokta olduğuna dikkat edin) syntax highlighting temasını ayarlayabilirsiniz: + +{* ../../docs_src/configure_swagger_ui/tutorial002_py39.py hl[3] *} + +Bu yapılandırma, syntax highlighting renk temasını değiştirir: + + + +## Varsayılan Swagger UI Parametrelerini Değiştirin { #change-default-swagger-ui-parameters } + +FastAPI, çoğu kullanım senaryosu için uygun bazı varsayılan yapılandırma parametreleriyle gelir. + +Şu varsayılan yapılandırmaları içerir: + +{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} + +`swagger_ui_parameters` argümanında farklı bir değer vererek bunların herhangi birini ezebilirsiniz (override). + +Örneğin `deepLinking`'i devre dışı bırakmak için `swagger_ui_parameters`'a şu ayarları geçebilirsiniz: + +{* ../../docs_src/configure_swagger_ui/tutorial003_py39.py hl[3] *} + +## Diğer Swagger UI Parametreleri { #other-swagger-ui-parameters } + +Kullanabileceğiniz diğer tüm olası yapılandırmaları görmek için, resmi Swagger UI parametreleri dokümantasyonunu okuyun. + +## Yalnızca JavaScript ayarları { #javascript-only-settings } + +Swagger UI ayrıca bazı yapılandırmaların **yalnızca JavaScript** nesneleri olmasına izin verir (örneğin JavaScript fonksiyonları). + +FastAPI, bu yalnızca JavaScript olan `presets` ayarlarını da içerir: + +```JavaScript +presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIBundle.SwaggerUIStandalonePreset +] +``` + +Bunlar string değil, **JavaScript** nesneleridir; dolayısıyla bunları Python kodundan doğrudan geçemezsiniz. + +Böyle yalnızca JavaScript yapılandırmalarına ihtiyacınız varsa, yukarıdaki yöntemlerden birini kullanabilirsiniz: Swagger UI'nin tüm *path operation*'larını override edin ve ihtiyaç duyduğunuz JavaScript'i elle yazın. diff --git a/docs/tr/docs/how-to/custom-docs-ui-assets.md b/docs/tr/docs/how-to/custom-docs-ui-assets.md new file mode 100644 index 000000000..bdd2d0244 --- /dev/null +++ b/docs/tr/docs/how-to/custom-docs-ui-assets.md @@ -0,0 +1,185 @@ +# Özel Docs UI Statik Varlıkları (Self-Hosting) { #custom-docs-ui-static-assets-self-hosting } + +API dokümanları **Swagger UI** ve **ReDoc** kullanır ve bunların her biri bazı JavaScript ve CSS dosyalarına ihtiyaç duyar. + +Varsayılan olarak bu dosyalar bir CDN üzerinden servis edilir. + +Ancak bunu özelleştirmek mümkündür; belirli bir CDN ayarlayabilir veya dosyaları kendiniz servis edebilirsiniz. + +## JavaScript ve CSS için Özel CDN { #custom-cdn-for-javascript-and-css } + +Diyelim ki farklı bir CDN kullanmak istiyorsunuz; örneğin `https://unpkg.com/` kullanmak istiyorsunuz. + +Bu, örneğin bazı URL'leri kısıtlayan bir ülkede yaşıyorsanız faydalı olabilir. + +### Otomatik dokümanları devre dışı bırakın { #disable-the-automatic-docs } + +İlk adım, otomatik dokümanları devre dışı bırakmaktır; çünkü varsayılan olarak bunlar varsayılan CDN'i kullanır. + +Bunları devre dışı bırakmak için `FastAPI` uygulamanızı oluştururken URL'lerini `None` olarak ayarlayın: + +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[8] *} + +### Özel dokümanları ekleyin { #include-the-custom-docs } + +Şimdi özel dokümanlar için *path operation*'ları oluşturabilirsiniz. + +Dokümanlar için HTML sayfalarını üretmek üzere FastAPI'nin dahili fonksiyonlarını yeniden kullanabilir ve gerekli argümanları iletebilirsiniz: + +* `openapi_url`: Dokümanların HTML sayfasının API'niz için OpenAPI şemasını alacağı URL. Burada `app.openapi_url` niteliğini kullanabilirsiniz. +* `title`: API'nizin başlığı. +* `oauth2_redirect_url`: varsayılanı kullanmak için burada `app.swagger_ui_oauth2_redirect_url` kullanabilirsiniz. +* `swagger_js_url`: Swagger UI dokümanlarınızın HTML'inin **JavaScript** dosyasını alacağı URL. Bu, özel CDN URL'idir. +* `swagger_css_url`: Swagger UI dokümanlarınızın HTML'inin **CSS** dosyasını alacağı URL. Bu, özel CDN URL'idir. + +ReDoc için de benzer şekilde... + +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[2:6,11:19,22:24,27:33] *} + +/// tip | İpucu + +`swagger_ui_redirect` için olan *path operation*, OAuth2 kullandığınızda yardımcı olması için vardır. + +API'nizi bir OAuth2 sağlayıcısıyla entegre ederseniz kimlik doğrulaması yapabilir, aldığınız kimlik bilgileriyle API dokümanlarına geri dönebilir ve gerçek OAuth2 kimlik doğrulamasını kullanarak onunla etkileşime geçebilirsiniz. + +Swagger UI bunu arka planda sizin için yönetir, ancak bu "redirect" yardımcısına ihtiyaç duyar. + +/// + +### Test etmek için bir *path operation* oluşturun { #create-a-path-operation-to-test-it } + +Şimdi her şeyin çalıştığını test edebilmek için bir *path operation* oluşturun: + +{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[36:38] *} + +### Test edin { #test-it } + +Artık http://127.0.0.1:8000/docs adresinden dokümanlarınıza gidebilmeli ve sayfayı yenilediğinizde bu varlıkların yeni CDN'den yüklendiğini görebilmelisiniz. + +## Dokümanlar için JavaScript ve CSS'i Self-Hosting ile barındırma { #self-hosting-javascript-and-css-for-docs } + +JavaScript ve CSS'i self-hosting ile barındırmak, örneğin uygulamanızın İnternet erişimi olmadan (offline), açık İnternet olmadan veya bir lokal ağ içinde bile çalışmaya devam etmesi gerekiyorsa faydalı olabilir. + +Burada bu dosyaları aynı FastAPI uygulamasında nasıl kendiniz servis edeceğinizi ve dokümanların bunları kullanacak şekilde nasıl yapılandırılacağını göreceksiniz. + +### Proje dosya yapısı { #project-file-structure } + +Diyelim ki projenizin dosya yapısı şöyle: + +``` +. +├── app +│ ├── __init__.py +│ ├── main.py +``` + +Şimdi bu statik dosyaları saklamak için bir dizin oluşturun. + +Yeni dosya yapınız şöyle olabilir: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +└── static/ +``` + +### Dosyaları indirin { #download-the-files } + +Dokümanlar için gereken statik dosyaları indirin ve `static/` dizinine koyun. + +Muhtemelen her bir linke sağ tıklayıp "Save link as..." benzeri bir seçenek seçebilirsiniz. + +**Swagger UI** şu dosyaları kullanır: + +* `swagger-ui-bundle.js` +* `swagger-ui.css` + +**ReDoc** ise şu dosyayı kullanır: + +* `redoc.standalone.js` + +Bundan sonra dosya yapınız şöyle görünebilir: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +└── static + ├── redoc.standalone.js + ├── swagger-ui-bundle.js + └── swagger-ui.css +``` + +### Statik dosyaları servis edin { #serve-the-static-files } + +* `StaticFiles` içe aktarın. +* Belirli bir path'te bir `StaticFiles()` instance'ını "mount" edin. + +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[7,11] *} + +### Statik dosyaları test edin { #test-the-static-files } + +Uygulamanızı başlatın ve http://127.0.0.1:8000/static/redoc.standalone.js adresine gidin. + +**ReDoc** için çok uzun bir JavaScript dosyası görmelisiniz. + +Şuna benzer bir şekilde başlayabilir: + +```JavaScript +/*! For license information please see redoc.standalone.js.LICENSE.txt */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("null")): +... +``` + +Bu, uygulamanızdan statik dosyaları servis edebildiğinizi ve dokümanlar için statik dosyaları doğru yere koyduğunuzu doğrular. + +Şimdi uygulamayı, dokümanlar için bu statik dosyaları kullanacak şekilde yapılandırabiliriz. + +### Statik dosyalar için otomatik dokümanları devre dışı bırakın { #disable-the-automatic-docs-for-static-files } + +Özel CDN kullanırken olduğu gibi, ilk adım otomatik dokümanları devre dışı bırakmaktır; çünkü bunlar varsayılan olarak CDN kullanır. + +Bunları devre dışı bırakmak için `FastAPI` uygulamanızı oluştururken URL'lerini `None` olarak ayarlayın: + +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[9] *} + +### Statik dosyalar için özel dokümanları ekleyin { #include-the-custom-docs-for-static-files } + +Özel CDN'de olduğu gibi, artık özel dokümanlar için *path operation*'ları oluşturabilirsiniz. + +Yine FastAPI'nin dahili fonksiyonlarını kullanarak dokümanlar için HTML sayfalarını oluşturabilir ve gerekli argümanları geçebilirsiniz: + +* `openapi_url`: Dokümanların HTML sayfasının API'niz için OpenAPI şemasını alacağı URL. Burada `app.openapi_url` niteliğini kullanabilirsiniz. +* `title`: API'nizin başlığı. +* `oauth2_redirect_url`: varsayılanı kullanmak için burada `app.swagger_ui_oauth2_redirect_url` kullanabilirsiniz. +* `swagger_js_url`: Swagger UI dokümanlarınızın HTML'inin **JavaScript** dosyasını alacağı URL. **Artık bunu sizin kendi uygulamanız servis ediyor**. +* `swagger_css_url`: Swagger UI dokümanlarınızın HTML'inin **CSS** dosyasını alacağı URL. **Artık bunu sizin kendi uygulamanız servis ediyor**. + +ReDoc için de benzer şekilde... + +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[2:6,14:22,25:27,30:36] *} + +/// tip | İpucu + +`swagger_ui_redirect` için olan *path operation*, OAuth2 kullandığınızda yardımcı olması için vardır. + +API'nizi bir OAuth2 sağlayıcısıyla entegre ederseniz kimlik doğrulaması yapabilir, aldığınız kimlik bilgileriyle API dokümanlarına geri dönebilir ve gerçek OAuth2 kimlik doğrulamasını kullanarak onunla etkileşime geçebilirsiniz. + +Swagger UI bunu arka planda sizin için yönetir, ancak bu "redirect" yardımcısına ihtiyaç duyar. + +/// + +### Statik dosyaları test etmek için bir *path operation* oluşturun { #create-a-path-operation-to-test-static-files } + +Şimdi her şeyin çalıştığını test edebilmek için bir *path operation* oluşturun: + +{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[39:41] *} + +### Statik Dosyalar UI'ını Test Edin { #test-static-files-ui } + +Artık WiFi bağlantınızı kesip http://127.0.0.1:8000/docs adresindeki dokümanlarınıza gidebilmeli ve sayfayı yenileyebilmelisiniz. + +Ve İnternet olmasa bile API dokümanlarınızı görebilir ve onunla etkileşime geçebilirsiniz. diff --git a/docs/tr/docs/how-to/custom-request-and-route.md b/docs/tr/docs/how-to/custom-request-and-route.md new file mode 100644 index 000000000..a4419373f --- /dev/null +++ b/docs/tr/docs/how-to/custom-request-and-route.md @@ -0,0 +1,109 @@ +# Özel Request ve APIRoute sınıfı { #custom-request-and-apiroute-class } + +Bazı durumlarda, `Request` ve `APIRoute` sınıflarının kullandığı mantığı override etmek isteyebilirsiniz. + +Özellikle bu yaklaşım, bir middleware içindeki mantığa iyi bir alternatif olabilir. + +Örneğin, request body uygulamanız tarafından işlenmeden önce okumak veya üzerinde değişiklik yapmak istiyorsanız. + +/// danger | Uyarı + +Bu "ileri seviye" bir özelliktir. + +**FastAPI**'ye yeni başlıyorsanız bu bölümü atlamak isteyebilirsiniz. + +/// + +## Kullanım senaryoları { #use-cases } + +Bazı kullanım senaryoları: + +* JSON olmayan request body'leri JSON'a dönüştürmek (örn. `msgpack`). +* gzip ile sıkıştırılmış request body'leri açmak (decompress). +* Tüm request body'lerini otomatik olarak loglamak. + +## Özel request body encoding'lerini ele alma { #handling-custom-request-body-encodings } + +Gzip request'lerini açmak için özel bir `Request` alt sınıfını nasıl kullanabileceğimize bakalım. + +Ayrıca, o özel request sınıfını kullanmak için bir `APIRoute` alt sınıfı da oluşturacağız. + +### Özel bir `GzipRequest` sınıfı oluşturun { #create-a-custom-gziprequest-class } + +/// tip | İpucu + +Bu, nasıl çalıştığını göstermek için hazırlanmış basit bir örnektir; Gzip desteğine ihtiyacınız varsa sağlanan [`GzipMiddleware`](../advanced/middleware.md#gzipmiddleware){.internal-link target=_blank} bileşenini kullanabilirsiniz. + +/// + +Önce, uygun bir header mevcut olduğunda body'yi açmak için `Request.body()` metodunu overwrite edecek bir `GzipRequest` sınıfı oluşturuyoruz. + +Header'da `gzip` yoksa body'yi açmayı denemez. + +Böylece aynı route sınıfı, gzip ile sıkıştırılmış veya sıkıştırılmamış request'leri handle edebilir. + +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *} + +### Özel bir `GzipRoute` sınıfı oluşturun { #create-a-custom-gziproute-class } + +Sonra, `GzipRequest`'i kullanacak `fastapi.routing.APIRoute` için özel bir alt sınıf oluşturuyoruz. + +Bu kez `APIRoute.get_route_handler()` metodunu overwrite edeceğiz. + +Bu metot bir fonksiyon döndürür. Bu fonksiyon da request'i alır ve response döndürür. + +Burada bu fonksiyonu, orijinal request'ten bir `GzipRequest` oluşturmak için kullanıyoruz. + +{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *} + +/// note | Teknik Detaylar + +Bir `Request`'in, request ile ilgili metadata'yı içeren bir Python `dict` olan `request.scope` attribute'u vardır. + +Bir `Request` ayrıca `request.receive` içerir; bu, request'in body'sini "almak" (receive etmek) için kullanılan bir fonksiyondur. + +`scope` `dict`'i ve `receive` fonksiyonu, ASGI spesifikasyonunun parçalarıdır. + +Ve bu iki şey, `scope` ve `receive`, yeni bir `Request` instance'ı oluşturmak için gerekenlerdir. + +`Request` hakkında daha fazla bilgi için Starlette'ın Request dokümantasyonuna bakın. + +/// + +`GzipRequest.get_route_handler` tarafından döndürülen fonksiyonun farklı yaptığı tek şey, `Request`'i bir `GzipRequest`'e dönüştürmektir. + +Bunu yaptığımızda `GzipRequest`, veriyi (gerekliyse) *path operations*'larımıza geçirmeden önce açma (decompress) işini üstlenir. + +Bundan sonra tüm işleme mantığı aynıdır. + +Ancak `GzipRequest.body` içindeki değişikliklerimiz sayesinde, request body gerektiğinde **FastAPI** tarafından yüklendiğinde otomatik olarak decompress edilir. + +## Bir exception handler içinde request body'ye erişme { #accessing-the-request-body-in-an-exception-handler } + +/// tip | İpucu + +Aynı problemi çözmek için, muhtemelen `RequestValidationError` için özel bir handler içinde `body` kullanmak çok daha kolaydır ([Hataları Ele Alma](../tutorial/handling-errors.md#use-the-requestvalidationerror-body){.internal-link target=_blank}). + +Yine de bu örnek geçerlidir ve dahili bileşenlerle nasıl etkileşime geçileceğini gösterir. + +/// + +Aynı yaklaşımı bir exception handler içinde request body'ye erişmek için de kullanabiliriz. + +Tek yapmamız gereken, request'i bir `try`/`except` bloğu içinde handle etmek: + +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *} + +Bir exception oluşursa, `Request` instance'ı hâlâ scope içinde olacağı için, hatayı handle ederken request body'yi okuyup kullanabiliriz: + +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *} + +## Bir router içinde özel `APIRoute` sınıfı { #custom-apiroute-class-in-a-router } + +Bir `APIRouter` için `route_class` parametresini de ayarlayabilirsiniz: + +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *} + +Bu örnekte, `router` altındaki *path operations*'lar özel `TimedRoute` sınıfını kullanır ve response'u üretmek için geçen süreyi içeren ekstra bir `X-Response-Time` header'ı response'ta bulunur: + +{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *} diff --git a/docs/tr/docs/how-to/extending-openapi.md b/docs/tr/docs/how-to/extending-openapi.md new file mode 100644 index 000000000..99691946c --- /dev/null +++ b/docs/tr/docs/how-to/extending-openapi.md @@ -0,0 +1,80 @@ +# OpenAPI'yi Genişletme { #extending-openapi } + +Oluşturulan OpenAPI şemasını değiştirmeniz gereken bazı durumlar olabilir. + +Bu bölümde bunun nasıl yapılacağını göreceksiniz. + +## Normal süreç { #the-normal-process } + +Normal (varsayılan) süreç şöyledir. + +Bir `FastAPI` uygulamasının (instance) OpenAPI şemasını döndürmesi beklenen bir `.openapi()` metodu vardır. + +Uygulama nesnesi oluşturulurken, `/openapi.json` (ya da `openapi_url` için ne ayarladıysanız o) için bir *path operation* kaydedilir. + +Bu path operation, uygulamanın `.openapi()` metodunun sonucunu içeren bir JSON response döndürür. + +Varsayılan olarak `.openapi()` metodunun yaptığı şey, `.openapi_schema` özelliğinde içerik olup olmadığını kontrol etmek ve varsa onu döndürmektir. + +Eğer yoksa, `fastapi.openapi.utils.get_openapi` konumundaki yardımcı (utility) fonksiyonu kullanarak şemayı üretir. + +Ve `get_openapi()` fonksiyonu şu parametreleri alır: + +* `title`: Dokümanlarda gösterilen OpenAPI başlığı. +* `version`: API'nizin sürümü, örn. `2.5.0`. +* `openapi_version`: Kullanılan OpenAPI specification sürümü. Varsayılan olarak en günceli: `3.1.0`. +* `summary`: API'nin kısa özeti. +* `description`: API'nizin açıklaması; markdown içerebilir ve dokümanlarda gösterilir. +* `routes`: route'ların listesi; bunların her biri kayıtlı *path operations*'lardır. `app.routes` içinden alınırlar. + +/// info | Bilgi + +`summary` parametresi OpenAPI 3.1.0 ve üzeri sürümlerde vardır; FastAPI 0.99.0 ve üzeri tarafından desteklenmektedir. + +/// + +## Varsayılanları ezme { #overriding-the-defaults } + +Yukarıdaki bilgileri kullanarak aynı yardımcı fonksiyonla OpenAPI şemasını üretebilir ve ihtiyacınız olan her parçayı override edebilirsiniz. + +Örneğin, özel bir logo eklemek için ReDoc'un OpenAPI extension'ını ekleyelim. + +### Normal **FastAPI** { #normal-fastapi } + +Önce, tüm **FastAPI** uygulamanızı her zamanki gibi yazın: + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[1,4,7:9] *} + +### OpenAPI şemasını üretme { #generate-the-openapi-schema } + +Ardından, bir `custom_openapi()` fonksiyonunun içinde aynı yardımcı fonksiyonu kullanarak OpenAPI şemasını üretin: + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[2,15:21] *} + +### OpenAPI şemasını değiştirme { #modify-the-openapi-schema } + +Artık OpenAPI şemasındaki `info` "object"'ine özel bir `x-logo` ekleyerek ReDoc extension'ını ekleyebilirsiniz: + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[22:24] *} + +### OpenAPI şemasını cache'leme { #cache-the-openapi-schema } + +Ürettiğiniz şemayı saklamak için `.openapi_schema` özelliğini bir "cache" gibi kullanabilirsiniz. + +Böylece bir kullanıcı API docs'larınızı her açtığında uygulamanız şemayı tekrar tekrar üretmek zorunda kalmaz. + +Şema yalnızca bir kez üretilecektir; sonraki request'ler için de aynı cache'lenmiş şema kullanılacaktır. + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[13:14,25:26] *} + +### Metodu override etme { #override-the-method } + +Şimdi `.openapi()` metodunu yeni fonksiyonunuzla değiştirebilirsiniz. + +{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[29] *} + +### Kontrol edin { #check-it } + +http://127.0.0.1:8000/redoc adresine gittiğinizde, özel logonuzu kullandığınızı göreceksiniz (bu örnekte **FastAPI**'nin logosu): + + diff --git a/docs/tr/docs/how-to/graphql.md b/docs/tr/docs/how-to/graphql.md new file mode 100644 index 000000000..fbf018874 --- /dev/null +++ b/docs/tr/docs/how-to/graphql.md @@ -0,0 +1,60 @@ +# GraphQL { #graphql } + +**FastAPI**, **ASGI** standardını temel aldığı için ASGI ile uyumlu herhangi bir **GraphQL** kütüphanesini entegre etmek oldukça kolaydır. + +Aynı uygulama içinde normal FastAPI *path operation*'larını GraphQL ile birlikte kullanabilirsiniz. + +/// tip | İpucu + +**GraphQL** bazı çok özel kullanım senaryolarını çözer. + +Yaygın **web API**'lerle karşılaştırıldığında **avantajları** ve **dezavantajları** vardır. + +Kendi senaryonuz için **faydaların**, **olumsuz yönleri** telafi edip etmediğini mutlaka değerlendirin. 🤓 + +/// + +## GraphQL Kütüphaneleri { #graphql-libraries } + +Aşağıda **ASGI** desteği olan bazı **GraphQL** kütüphaneleri var. Bunları **FastAPI** ile kullanabilirsiniz: + +* Strawberry 🍓 + * FastAPI dokümantasyonu ile +* Ariadne + * FastAPI dokümantasyonu ile +* Tartiflette + * ASGI entegrasyonu sağlamak için Tartiflette ASGI ile +* Graphene + * starlette-graphene3 ile + +## Strawberry ile GraphQL { #graphql-with-strawberry } + +**GraphQL** ile çalışmanız gerekiyorsa ya da bunu istiyorsanız, **Strawberry** önerilen kütüphanedir; çünkü tasarımı **FastAPI**'nin tasarımına en yakındır ve her şey **type annotation**'lar üzerine kuruludur. + +Kullanım senaryonuza göre farklı bir kütüphaneyi tercih edebilirsiniz; ancak bana sorarsanız muhtemelen **Strawberry**'yi denemenizi önerirdim. + +Strawberry'yi FastAPI ile nasıl entegre edebileceğinize dair küçük bir ön izleme: + +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} + +Strawberry hakkında daha fazlasını Strawberry dokümantasyonunda öğrenebilirsiniz. + +Ayrıca FastAPI ile Strawberry dokümanlarına da göz atın. + +## Starlette'teki Eski `GraphQLApp` { #older-graphqlapp-from-starlette } + +Starlette'in önceki sürümlerinde Graphene ile entegrasyon için bir `GraphQLApp` sınıfı vardı. + +Bu sınıf Starlette'te kullanımdan kaldırıldı (deprecated). Ancak bunu kullanan bir kodunuz varsa, aynı kullanım senaryosunu kapsayan ve **neredeyse aynı bir interface** sağlayan starlette-graphene3'e kolayca **migrate** edebilirsiniz. + +/// tip | İpucu + +GraphQL'e ihtiyacınız varsa, custom class ve type'lar yerine type annotation'lara dayandığı için yine de Strawberry'yi incelemenizi öneririm. + +/// + +## Daha Fazlasını Öğrenin { #learn-more } + +**GraphQL** hakkında daha fazlasını resmi GraphQL dokümantasyonunda öğrenebilirsiniz. + +Ayrıca yukarıda bahsedilen kütüphanelerin her biri hakkında, kendi bağlantılarından daha fazla bilgi okuyabilirsiniz. diff --git a/docs/tr/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/tr/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md new file mode 100644 index 000000000..275ac5fd1 --- /dev/null +++ b/docs/tr/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -0,0 +1,135 @@ +# Pydantic v1'den Pydantic v2'ye Geçiş { #migrate-from-pydantic-v1-to-pydantic-v2 } + +Eski bir FastAPI uygulamanız varsa, Pydantic'in 1. sürümünü kullanıyor olabilirsiniz. + +FastAPI 0.100.0 sürümü, Pydantic v1 veya v2 ile çalışmayı destekliyordu. Hangisi kuruluysa onu kullanıyordu. + +FastAPI 0.119.0 sürümü, v2'ye geçişi kolaylaştırmak için, Pydantic v2’nin içinden Pydantic v1’e (`pydantic.v1` olarak) kısmi destek ekledi. + +FastAPI 0.126.0 sürümü Pydantic v1 desteğini kaldırdı, ancak bir süre daha `pydantic.v1` desteğini sürdürdü. + +/// warning | Uyarı + +Pydantic ekibi, **Python 3.14** ile başlayarak Python'ın en yeni sürümleri için Pydantic v1 desteğini sonlandırdı. + +Buna `pydantic.v1` de dahildir; Python 3.14 ve üzeri sürümlerde artık desteklenmemektedir. + +Python'ın en yeni özelliklerini kullanmak istiyorsanız, Pydantic v2 kullandığınızdan emin olmanız gerekir. + +/// + +Pydantic v1 kullanan eski bir FastAPI uygulamanız varsa, burada onu Pydantic v2'ye nasıl taşıyacağınızı ve kademeli geçişi kolaylaştıran **FastAPI 0.119.0 özelliklerini** göstereceğim. + +## Resmi Kılavuz { #official-guide } + +Pydantic'in v1'den v2'ye resmi bir Migration Guide'ı vardır. + +Ayrıca nelerin değiştiğini, validasyonların artık nasıl daha doğru ve katı olduğunu, olası dikkat edilmesi gereken noktaları (caveat) vb. de içerir. + +Nelerin değiştiğini daha iyi anlamak için okuyabilirsiniz. + +## Testler { #tests } + +Uygulamanız için [testlerinizin](../tutorial/testing.md){.internal-link target=_blank} olduğundan ve bunları continuous integration (CI) üzerinde çalıştırdığınızdan emin olun. + +Bu şekilde yükseltmeyi yapabilir ve her şeyin hâlâ beklendiği gibi çalıştığını doğrulayabilirsiniz. + +## `bump-pydantic` { #bump-pydantic } + +Birçok durumda, özel özelleştirmeler olmadan standart Pydantic modelleri kullanıyorsanız, Pydantic v1'den Pydantic v2'ye geçiş sürecinin büyük kısmını otomatikleştirebilirsiniz. + +Aynı Pydantic ekibinin geliştirdiği `bump-pydantic` aracını kullanabilirsiniz. + +Bu araç, değişmesi gereken kodun büyük bir kısmını otomatik olarak dönüştürmenize yardımcı olur. + +Bundan sonra testleri çalıştırıp her şeyin çalışıp çalışmadığını kontrol edebilirsiniz. Çalışıyorsa işiniz biter. 😎 + +## v2 İçinde Pydantic v1 { #pydantic-v1-in-v2 } + +Pydantic v2, `pydantic.v1` adlı bir alt modül olarak Pydantic v1'in tamamını içerir. Ancak bu yapı, Python 3.13'ün üzerindeki sürümlerde artık desteklenmemektedir. + +Bu da şu anlama gelir: Pydantic v2'nin en güncel sürümünü kurup, bu alt modülden eski Pydantic v1 bileşenlerini import ederek, sanki eski Pydantic v1 kuruluymuş gibi kullanabilirsiniz. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} + +### v2 İçinde Pydantic v1 için FastAPI Desteği { #fastapi-support-for-pydantic-v1-in-v2 } + +FastAPI 0.119.0'dan itibaren, v2'ye geçişi kolaylaştırmak için Pydantic v2’nin içinden Pydantic v1 kullanımına yönelik kısmi destek de vardır. + +Dolayısıyla Pydantic'i en güncel 2 sürümüne yükseltip import'ları `pydantic.v1` alt modülünü kullanacak şekilde değiştirebilirsiniz; çoğu durumda bu doğrudan çalışır. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} + +/// warning | Uyarı + +Pydantic ekibi Python 3.14'ten itibaren yeni Python sürümlerinde Pydantic v1'i artık desteklemediği için, `pydantic.v1` kullanımı da Python 3.14 ve üzeri sürümlerde desteklenmez. + +/// + +### Aynı Uygulamada Pydantic v1 ve v2 { #pydantic-v1-and-v2-on-the-same-app } + +Pydantic açısından, alanları (field) Pydantic v1 modelleriyle tanımlanmış bir Pydantic v2 modeli (ya da tersi) kullanmak **desteklenmez**. + +```mermaid +graph TB + subgraph "❌ Not Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V1Field["Pydantic v1 Model"] + end + subgraph V1["Pydantic v1 Model"] + V2Field["Pydantic v2 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +...ancak aynı uygulamada Pydantic v1 ve v2 kullanarak **ayrı (separated)** modeller tanımlayabilirsiniz. + +```mermaid +graph TB + subgraph "✅ Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V2Field["Pydantic v2 Model"] + end + subgraph V1["Pydantic v1 Model"] + V1Field["Pydantic v1 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +Bazı durumlarda, FastAPI uygulamanızda aynı **path operation** içinde hem Pydantic v1 hem de v2 modellerini kullanmak bile mümkündür: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} + +Yukarıdaki örnekte input modeli bir Pydantic v1 modelidir; output modeli ( `response_model=ItemV2` ile tanımlanan) ise bir Pydantic v2 modelidir. + +### Pydantic v1 Parametreleri { #pydantic-v1-parameters } + +Pydantic v1 modelleriyle `Body`, `Query`, `Form` vb. parametreler için FastAPI'ye özgü bazı araçları kullanmanız gerekiyorsa, Pydantic v2'ye geçişi tamamlayana kadar bunları `fastapi.temp_pydantic_v1_params` içinden import edebilirsiniz: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} + +### Adım Adım Geçiş { #migrate-in-steps } + +/// tip | İpucu + +Önce `bump-pydantic` ile deneyin; testleriniz geçerse ve bu yol çalışırsa tek komutla işi bitirmiş olursunuz. ✨ + +/// + +`bump-pydantic` sizin senaryonuz için uygun değilse, aynı uygulamada hem Pydantic v1 hem de v2 modellerini birlikte kullanma desteğinden yararlanarak Pydantic v2'ye kademeli şekilde geçebilirsiniz. + +Önce Pydantic'i en güncel 2 sürümüne yükseltip tüm modelleriniz için import'ları `pydantic.v1` kullanacak şekilde değiştirebilirsiniz. + +Ardından modellerinizi Pydantic v1'den v2'ye gruplar hâlinde, adım adım taşımaya başlayabilirsiniz. 🚶 diff --git a/docs/tr/docs/how-to/separate-openapi-schemas.md b/docs/tr/docs/how-to/separate-openapi-schemas.md new file mode 100644 index 000000000..c26411d29 --- /dev/null +++ b/docs/tr/docs/how-to/separate-openapi-schemas.md @@ -0,0 +1,102 @@ +# Input ve Output için Ayrı OpenAPI Schema'ları (Ya da Değil) { #separate-openapi-schemas-for-input-and-output-or-not } + +**Pydantic v2** yayınlandığından beri, üretilen OpenAPI eskisine göre biraz daha net ve **doğru**. 😎 + +Hatta bazı durumlarda, aynı Pydantic model için OpenAPI içinde input ve output tarafında, **default değerler** olup olmamasına bağlı olarak **iki farklı JSON Schema** bile görebilirsiniz. + +Bunun nasıl çalıştığına ve gerekirse nasıl değiştirebileceğinize bir bakalım. + +## Input ve Output için Pydantic Modelleri { #pydantic-models-for-input-and-output } + +Default değerleri olan bir Pydantic modeliniz olduğunu varsayalım; örneğin şöyle: + +{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:7] hl[7] *} + +### Input için Model { #model-for-input } + +Bu modeli şöyle input olarak kullanırsanız: + +{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:15] hl[14] *} + +...`description` alanı **zorunlu olmaz**. Çünkü `None` default değerine sahiptir. + +### Dokümanlarda Input Modeli { #input-model-in-docs } + +Bunu dokümanlarda da doğrulayabilirsiniz; `description` alanında **kırmızı yıldız** yoktur, yani required olarak işaretlenmemiştir: + +
+ +
+ +### Output için Model { #model-for-output } + +Ancak aynı modeli output olarak şöyle kullanırsanız: + +{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py hl[19] *} + +...`description` default değere sahip olduğu için, o alan için **hiçbir şey döndürmeseniz** bile yine de **o default değeri** alır. + +### Output Response Verisi için Model { #model-for-output-response-data } + +Dokümanlarla etkileşip response'u kontrol ederseniz, kod `description` alanlarından birine bir şey eklememiş olsa bile, JSON response default değeri (`null`) içerir: + +
+ +
+ +Bu, alanın **her zaman bir değeri olacağı** anlamına gelir; sadece bazen bu değer `None` olabilir (JSON'da `null`). + +Dolayısıyla API'nizi kullanan client'ların bu değerin var olup olmadığını kontrol etmesine gerek yoktur; **alanın her zaman mevcut olacağını varsayabilirler**, sadece bazı durumlarda default değer olan `None` gelecektir. + +Bunu OpenAPI'de ifade etmenin yolu, bu alanı **required** olarak işaretlemektir; çünkü her zaman yer alacaktır. + +Bu nedenle, bir modelin JSON Schema'sı **input mu output mu** kullanıldığına göre farklı olabilir: + +* **input** için `description` **required olmaz** +* **output** için **required olur** (ve `None` olabilir; JSON açısından `null`) + +### Dokümanlarda Output Modeli { #model-for-output-in-docs } + +Dokümanlarda output modelini de kontrol edebilirsiniz; **hem** `name` **hem de** `description` alanları **kırmızı yıldız** ile **required** olarak işaretlenmiştir: + +
+ +
+ +### Dokümanlarda Input ve Output Modelleri { #model-for-input-and-output-in-docs } + +OpenAPI içindeki tüm kullanılabilir Schema'lara (JSON Schema'lara) bakarsanız, iki tane olduğunu göreceksiniz: biri `Item-Input`, diğeri `Item-Output`. + +`Item-Input` için `description` **required değildir**, kırmızı yıldız yoktur. + +Ama `Item-Output` için `description` **required**'dır, kırmızı yıldız vardır. + +
+ +
+ +**Pydantic v2**'nin bu özelliğiyle API dokümantasyonunuz daha **hassas** olur; ayrıca autogenerated client'lar ve SDK'lar kullanıyorsanız, onlar da daha tutarlı ve daha iyi bir **developer experience** ile daha doğru üretilir. 🎉 + +## Schema'ları Ayırma { #do-not-separate-schemas } + +Bazı durumlarda **input ve output için aynı schema'yı** kullanmak isteyebilirsiniz. + +Bunun muhtemelen en yaygın nedeni, halihazırda autogenerated client kodlarınız/SDK'larınızın olması ve henüz bunların hepsini güncellemek istememenizdir. Büyük ihtimalle bir noktada güncellemek isteyeceksiniz, ama belki şu an değil. + +Bu durumda **FastAPI**'de bu özelliği `separate_input_output_schemas=False` parametresiyle kapatabilirsiniz. + +/// info | Bilgi + +`separate_input_output_schemas` desteği FastAPI `0.102.0` sürümünde eklendi. 🤓 + +/// + +{* ../../docs_src/separate_openapi_schemas/tutorial002_py310.py hl[10] *} + +### Dokümanlarda Input ve Output Modelleri için Aynı Schema { #same-schema-for-input-and-output-models-in-docs } + +Artık model için input ve output tarafında tek bir schema olur: sadece `Item`. Ayrıca `description` alanı **required değildir**: + +
+ +
diff --git a/docs/tr/docs/how-to/testing-database.md b/docs/tr/docs/how-to/testing-database.md new file mode 100644 index 000000000..a2062f6c2 --- /dev/null +++ b/docs/tr/docs/how-to/testing-database.md @@ -0,0 +1,7 @@ +# Bir Veritabanını Test Etmek { #testing-a-database } + +Veritabanları, SQL ve SQLModel hakkında SQLModel dokümantasyonundan öğrenebilirsiniz. 🤓 + +Ayrıca SQLModel'i FastAPI ile kullanmaya dair mini bir tutorial da var. ✨ + +Bu tutorial içinde SQL veritabanlarını test etme hakkında bir bölüm de bulunuyor. 😎 diff --git a/docs/tr/docs/tutorial/background-tasks.md b/docs/tr/docs/tutorial/background-tasks.md new file mode 100644 index 000000000..4cb67d822 --- /dev/null +++ b/docs/tr/docs/tutorial/background-tasks.md @@ -0,0 +1,84 @@ +# Arka Plan Görevleri { #background-tasks } + +Response döndürüldükten *sonra* çalıştırılacak arka plan görevleri tanımlayabilirsiniz. + +Bu, request’ten sonra yapılması gereken; ancak client’ın response’u almadan önce tamamlanmasını beklemesine gerek olmayan işlemler için kullanışlıdır. + +Örneğin: + +* Bir işlem gerçekleştirdikten sonra gönderilen email bildirimleri: + * Bir email server’a bağlanmak ve email göndermek genellikle "yavaş" olduğundan (birkaç saniye), response’u hemen döndürüp email bildirimini arka planda gönderebilirsiniz. +* Veri işleme: + * Örneğin, yavaş bir süreçten geçmesi gereken bir dosya aldığınızı düşünün; "Accepted" (HTTP 202) response’u döndürüp dosyayı arka planda işleyebilirsiniz. + +## `BackgroundTasks` Kullanımı { #using-backgroundtasks } + +Önce `BackgroundTasks`’i import edin ve *path operation function*’ınızda `BackgroundTasks` tip bildirimi olan bir parametre tanımlayın: + +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *} + +**FastAPI**, sizin için `BackgroundTasks` tipinde bir obje oluşturur ve onu ilgili parametre olarak geçirir. + +## Bir Görev Fonksiyonu Oluşturun { #create-a-task-function } + +Arka plan görevi olarak çalıştırılacak bir fonksiyon oluşturun. + +Bu, parametre alabilen standart bir fonksiyondur. + +`async def` de olabilir, normal `def` de olabilir; **FastAPI** bunu doğru şekilde nasıl ele alacağını bilir. + +Bu örnekte görev fonksiyonu bir dosyaya yazacaktır (email göndermeyi simüle ediyor). + +Ve yazma işlemi `async` ve `await` kullanmadığı için fonksiyonu normal `def` ile tanımlarız: + +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *} + +## Arka Plan Görevini Ekleyin { #add-the-background-task } + +*Path operation function*’ınızın içinde, görev fonksiyonunuzu `.add_task()` metodu ile *background tasks* objesine ekleyin: + +{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *} + +`.add_task()` şu argümanları alır: + +* Arka planda çalıştırılacak bir görev fonksiyonu (`write_notification`). +* Görev fonksiyonuna sırayla geçirilecek argümanlar (`email`). +* Görev fonksiyonuna geçirilecek keyword argümanlar (`message="some notification"`). + +## Dependency Injection { #dependency-injection } + +`BackgroundTasks` kullanımı dependency injection sistemiyle de çalışır; `BackgroundTasks` tipinde bir parametreyi birden fazla seviyede tanımlayabilirsiniz: bir *path operation function* içinde, bir dependency’de (dependable), bir sub-dependency’de, vb. + +**FastAPI** her durumda ne yapılacağını ve aynı objenin nasıl yeniden kullanılacağını bilir; böylece tüm arka plan görevleri birleştirilir ve sonrasında arka planda çalıştırılır: + +{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} + +Bu örnekte, response gönderildikten *sonra* mesajlar `log.txt` dosyasına yazılacaktır. + +Request’te bir query varsa, log’a bir arka plan göreviyle yazılır. + +Ardından *path operation function* içinde oluşturulan başka bir arka plan görevi, `email` path parametresini kullanarak bir mesaj yazar. + +## Teknik Detaylar { #technical-details } + +`BackgroundTasks` sınıfı doğrudan `starlette.background`’dan gelir. + +`fastapi` üzerinden import edebilmeniz ve yanlışlıkla `starlette.background` içindeki alternatif `BackgroundTask`’i (sonunda `s` olmadan) import etmemeniz için FastAPI’nin içine doğrudan import/eklenmiştir. + +Sadece `BackgroundTasks` (ve `BackgroundTask` değil) kullanarak, bunu bir *path operation function* parametresi olarak kullanmak ve gerisini **FastAPI**’nin sizin için halletmesini sağlamak mümkündür; tıpkı `Request` objesini doğrudan kullanırken olduğu gibi. + +FastAPI’de `BackgroundTask`’i tek başına kullanmak hâlâ mümkündür; ancak bu durumda objeyi kendi kodunuzda oluşturmanız ve onu içeren bir Starlette `Response` döndürmeniz gerekir. + +Daha fazla detayı Starlette’in Background Tasks için resmi dokümantasyonunda görebilirsiniz. + +## Dikkat Edilmesi Gerekenler { #caveat } + +Yoğun arka plan hesaplamaları yapmanız gerekiyorsa ve bunun aynı process tarafından çalıştırılmasına şart yoksa (örneğin memory, değişkenler vb. paylaşmanız gerekmiyorsa), Celery gibi daha büyük araçları kullanmak size fayda sağlayabilir. + +Bunlar genellikle daha karmaşık konfigurasyonlar ve RabbitMQ veya Redis gibi bir mesaj/iş kuyruğu yöneticisi gerektirir; ancak arka plan görevlerini birden fazla process’te ve özellikle birden fazla server’da çalıştırmanıza olanak tanırlar. + +Ancak aynı **FastAPI** app’i içindeki değişkenlere ve objelere erişmeniz gerekiyorsa veya küçük arka plan görevleri (email bildirimi göndermek gibi) yapacaksanız, doğrudan `BackgroundTasks` kullanabilirsiniz. + +## Özet { #recap } + +Arka plan görevleri eklemek için *path operation function*’larda ve dependency’lerde parametre olarak `BackgroundTasks`’i import edip kullanın. diff --git a/docs/tr/docs/tutorial/bigger-applications.md b/docs/tr/docs/tutorial/bigger-applications.md new file mode 100644 index 000000000..d8a4b8208 --- /dev/null +++ b/docs/tr/docs/tutorial/bigger-applications.md @@ -0,0 +1,504 @@ +# Daha Büyük Uygulamalar - Birden Fazla Dosya { #bigger-applications-multiple-files } + +Bir uygulama veya web API geliştirirken, her şeyi tek bir dosyaya sığdırabilmek nadirdir. + +**FastAPI**, tüm esnekliği korurken uygulamanızı yapılandırmanıza yardımcı olan pratik bir araç sunar. + +/// info | Bilgi + +Flask'ten geliyorsanız, bu yapı Flask'in Blueprints'ine denk gelir. + +/// + +## Örnek Bir Dosya Yapısı { #an-example-file-structure } + +Diyelim ki şöyle bir dosya yapınız var: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   ├── dependencies.py +│   └── routers +│   │ ├── __init__.py +│   │ ├── items.py +│   │ └── users.py +│   └── internal +│   ├── __init__.py +│   └── admin.py +``` + +/// tip | İpucu + +Birden fazla `__init__.py` dosyası var: her dizinde veya alt dizinde bir tane. + +Bu sayede bir dosyadaki kodu diğerine import edebilirsiniz. + +Örneğin `app/main.py` içinde şöyle bir satırınız olabilir: + +``` +from app.routers import items +``` + +/// + +* `app` dizini her şeyi içerir. Ayrıca boş bir `app/__init__.py` dosyası olduğu için bir "Python package" (bir "Python module" koleksiyonu) olur: `app`. +* İçinde bir `app/main.py` dosyası vardır. Bir Python package'in (içinde `__init__.py` dosyası olan bir dizinin) içinde olduğundan, o package'in bir "module"’üdür: `app.main`. +* Benzer şekilde `app/dependencies.py` dosyası da bir "module"’dür: `app.dependencies`. +* `app/routers/` adında bir alt dizin vardır ve içinde başka bir `__init__.py` dosyası bulunur; dolayısıyla bu bir "Python subpackage"’dir: `app.routers`. +* `app/routers/items.py` dosyası `app/routers/` package’i içinde olduğundan bir submodule’dür: `app.routers.items`. +* `app/routers/users.py` için de aynı şekilde, başka bir submodule’dür: `app.routers.users`. +* `app/internal/` adında bir alt dizin daha vardır ve içinde başka bir `__init__.py` dosyası bulunur; dolayısıyla bu da bir "Python subpackage"’dir: `app.internal`. +* Ve `app/internal/admin.py` dosyası başka bir submodule’dür: `app.internal.admin`. + + + +Aynı dosya yapısı, yorumlarla birlikte: + +```bash +. +├── app # "app" bir Python package'idir +│   ├── __init__.py # bu dosya, "app"i bir "Python package" yapar +│   ├── main.py # "main" module'ü, örn. import app.main +│   ├── dependencies.py # "dependencies" module'ü, örn. import app.dependencies +│   └── routers # "routers" bir "Python subpackage"idir +│   │ ├── __init__.py # "routers"ı bir "Python subpackage" yapar +│   │ ├── items.py # "items" submodule'ü, örn. import app.routers.items +│   │ └── users.py # "users" submodule'ü, örn. import app.routers.users +│   └── internal # "internal" bir "Python subpackage"idir +│   ├── __init__.py # "internal"ı bir "Python subpackage" yapar +│   └── admin.py # "admin" submodule'ü, örn. import app.internal.admin +``` + +## `APIRouter` { #apirouter } + +Diyelim ki sadece kullanıcıları yönetmeye ayrılmış dosyanız `/app/routers/users.py` içindeki submodule olsun. + +Kullanıcılarla ilgili *path operation*’ları, kodun geri kalanından ayrı tutmak istiyorsunuz; böylece düzenli kalır. + +Ancak bu hâlâ aynı **FastAPI** uygulaması/web API’sinin bir parçasıdır (aynı "Python Package" içinde). + +Bu module için *path operation*’ları `APIRouter` kullanarak oluşturabilirsiniz. + +### `APIRouter` Import Edin { #import-apirouter } + +`FastAPI` class’ında yaptığınız gibi import edip bir "instance" oluşturursunuz: + +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} + +### `APIRouter` ile *Path Operations* { #path-operations-with-apirouter } + +Sonra bunu kullanarak *path operation*’larınızı tanımlarsınız. + +`FastAPI` class’ını nasıl kullanıyorsanız aynı şekilde kullanın: + +{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} + +`APIRouter`’ı "mini bir `FastAPI`" class’ı gibi düşünebilirsiniz. + +Aynı seçeneklerin hepsi desteklenir. + +Aynı `parameters`, `responses`, `dependencies`, `tags`, vb. + +/// tip | İpucu + +Bu örnekte değişkenin adı `router`. Ancak istediğiniz gibi adlandırabilirsiniz. + +/// + +Bu `APIRouter`’ı ana `FastAPI` uygulamasına ekleyeceğiz; ama önce dependency’lere ve bir diğer `APIRouter`’a bakalım. + +## Dependencies { #dependencies } + +Uygulamanın birden fazla yerinde kullanılacak bazı dependency’lere ihtiyacımız olacağını görüyoruz. + +Bu yüzden onları ayrı bir `dependencies` module’üne koyuyoruz (`app/dependencies.py`). + +Şimdi, özel bir `X-Token` header'ını okumak için basit bir dependency kullanalım: + +{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} + +/// tip | İpucu + +Örneği basit tutmak için uydurma bir header kullanıyoruz. + +Ancak gerçek senaryolarda, entegre [Security yardımcı araçlarını](security/index.md){.internal-link target=_blank} kullanarak daha iyi sonuç alırsınız. + +/// + +## `APIRouter` ile Başka Bir Module { #another-module-with-apirouter } + +Diyelim ki uygulamanızdaki "items" ile ilgili endpoint'ler de `app/routers/items.py` module’ünde olsun. + +Şunlar için *path operation*’larınız var: + +* `/items/` +* `/items/{item_id}` + +Bu, `app/routers/users.py` ile aynı yapıdadır. + +Ancak biraz daha akıllı davranıp kodu sadeleştirmek istiyoruz. + +Bu module’deki tüm *path operation*’ların şu ortak özelliklere sahip olduğunu biliyoruz: + +* Path `prefix`: `/items`. +* `tags`: (tek bir tag: `items`). +* Ek `responses`. +* `dependencies`: hepsinin, oluşturduğumuz `X-Token` dependency’sine ihtiyacı var. + +Dolayısıyla bunları her *path operation*’a tek tek eklemek yerine `APIRouter`’a ekleyebiliriz. + +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} + +Her *path operation*’ın path’i aşağıdaki gibi `/` ile başlamak zorunda olduğundan: + +```Python hl_lines="1" +@router.get("/{item_id}") +async def read_item(item_id: str): + ... +``` + +...prefix’in sonunda `/` olmamalıdır. + +Yani bu örnekte prefix `/items` olur. + +Ayrıca, bu router içindeki tüm *path operation*’lara uygulanacak bir `tags` listesi ve ek `responses` da ekleyebiliriz. + +Ve router’daki tüm *path operation*’lara eklenecek, her request için çalıştırılıp çözülecek bir `dependencies` listesi de ekleyebiliriz. + +/// tip | İpucu + +[ *path operation decorator*’larındaki dependency’lerde](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank} olduğu gibi, *path operation function*’ınıza herhangi bir değer aktarılmayacağını unutmayın. + +/// + +Sonuç olarak item path’leri artık: + +* `/items/` +* `/items/{item_id}` + +...tam da istediğimiz gibi olur. + +* Hepsi, içinde tek bir string `"items"` bulunan bir tag listesiyle işaretlenir. + * Bu "tags", özellikle otomatik interaktif dokümantasyon sistemleri (OpenAPI) için çok faydalıdır. +* Hepsi önceden tanımlı `responses`’ları içerir. +* Bu *path operation*’ların hepsinde, öncesinde `dependencies` listesi değerlendirilip çalıştırılır. + * Ayrıca belirli bir *path operation* içinde dependency tanımlarsanız, **onlar da çalıştırılır**. + * Önce router dependency’leri, sonra decorator’daki [`dependencies`](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, sonra da normal parametre dependency’leri çalışır. + * Ayrıca [`scopes` ile `Security` dependency’leri](../advanced/security/oauth2-scopes.md){.internal-link target=_blank} de ekleyebilirsiniz. + +/// tip | İpucu + +`APIRouter` içinde `dependencies` kullanmak, örneğin bir grup *path operation* için kimlik doğrulamayı zorunlu kılmakta kullanılabilir. Dependency’leri tek tek her birine eklemeseniz bile. + +/// + +/// check | Ek bilgi + +`prefix`, `tags`, `responses` ve `dependencies` parametreleri (çoğu başka örnekte olduğu gibi) kod tekrarını önlemenize yardımcı olan, **FastAPI**’nin bir özelliğidir. + +/// + +### Dependency'leri Import Edin { #import-the-dependencies } + +Bu kod `app.routers.items` module’ünde, yani `app/routers/items.py` dosyasında duruyor. + +Dependency function’ını ise `app.dependencies` module’ünden, yani `app/dependencies.py` dosyasından almamız gerekiyor. + +Bu yüzden dependency’ler için `..` ile relative import kullanıyoruz: + +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *} + +#### Relative Import Nasıl Çalışır { #how-relative-imports-work } + +/// tip | İpucu + +Import’ların nasıl çalıştığını çok iyi biliyorsanız, bir sonraki bölüme geçin. + +/// + +Tek bir nokta `.`, örneğin: + +```Python +from .dependencies import get_token_header +``` + +şu anlama gelir: + +* Bu module’ün (yani `app/routers/items.py` dosyasının) bulunduğu package içinden başla ( `app/routers/` dizini)... +* `dependencies` module’ünü bul (`app/routers/dependencies.py` gibi hayali bir dosya)... +* ve oradan `get_token_header` function’ını import et. + +Ama o dosya yok; bizim dependency’lerimiz `app/dependencies.py` dosyasında. + +Uygulama/dosya yapımızın nasıl göründüğünü hatırlayın: + + + +--- + +İki nokta `..`, örneğin: + +```Python +from ..dependencies import get_token_header +``` + +şu anlama gelir: + +* Bu module’ün bulunduğu package içinden başla (`app/routers/` dizini)... +* üst (parent) package’e çık (`app/` dizini)... +* burada `dependencies` module’ünü bul (`app/dependencies.py` dosyası)... +* ve oradan `get_token_header` function’ını import et. + +Bu doğru şekilde çalışır! 🎉 + +--- + +Aynı şekilde, üç nokta `...` kullansaydık: + +```Python +from ...dependencies import get_token_header +``` + +şu anlama gelirdi: + +* Bu module’ün bulunduğu package içinden başla (`app/routers/` dizini)... +* üst package’e çık (`app/` dizini)... +* sonra bir üstüne daha çık (orada bir üst package yok; `app` en üst seviye 😱)... +* ve orada `dependencies` module’ünü bul (`app/dependencies.py` dosyası)... +* ve oradan `get_token_header` function’ını import et. + +Bu, `app/` dizininin üstünde, kendi `__init__.py` dosyası olan başka bir package’e işaret ederdi. Ama bizde böyle bir şey yok. Dolayısıyla bu örnekte hata verirdi. 🚨 + +Artık nasıl çalıştığını bildiğinize göre, uygulamalarınız ne kadar karmaşık olursa olsun relative import’ları kullanabilirsiniz. 🤓 + +### Özel `tags`, `responses` ve `dependencies` Ekleyin { #add-some-custom-tags-responses-and-dependencies } + +`/items` prefix’ini ya da `tags=["items"]` değerini her *path operation*’a tek tek eklemiyoruz; çünkü bunları `APIRouter`’a ekledik. + +Ama yine de belirli bir *path operation*’a uygulanacak _ek_ `tags` tanımlayabilir, ayrıca o *path operation*’a özel `responses` ekleyebiliriz: + +{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *} + +/// tip | İpucu + +Bu son *path operation*’da tag kombinasyonu şöyle olur: `["items", "custom"]`. + +Ayrıca dokümantasyonda iki response da görünür: biri `404`, diğeri `403`. + +/// + +## Ana `FastAPI` { #the-main-fastapi } + +Şimdi `app/main.py` module’üne bakalım. + +Burada `FastAPI` class’ını import edip kullanırsınız. + +Bu dosya, uygulamanızda her şeyi bir araya getiren ana dosya olacak. + +Mantığın büyük kısmı artık kendi module’lerinde yaşayacağı için ana dosya oldukça basit kalır. + +### `FastAPI` Import Edin { #import-fastapi } + +Normal şekilde bir `FastAPI` class’ı oluşturursunuz. + +Hatta her `APIRouter` için olan dependency’lerle birleştirilecek [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank} bile tanımlayabilirsiniz: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *} + +### `APIRouter` Import Edin { #import-the-apirouter } + +Şimdi `APIRouter` içeren diğer submodule’leri import ediyoruz: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *} + +`app/routers/users.py` ve `app/routers/items.py` dosyaları aynı Python package’i olan `app`’in parçası olan submodule’ler olduğu için, onları "relative import" ile tek bir nokta `.` kullanarak import edebiliriz. + +### Import Nasıl Çalışır { #how-the-importing-works } + +Şu bölüm: + +```Python +from .routers import items, users +``` + +şu anlama gelir: + +* Bu module’ün (yani `app/main.py` dosyasının) bulunduğu package içinden başla (`app/` dizini)... +* `routers` subpackage’ini bul (`app/routers/` dizini)... +* ve buradan `items` submodule’ünü (`app/routers/items.py`) ve `users` submodule’ünü (`app/routers/users.py`) import et... + +`items` module’ünün içinde `router` adında bir değişken vardır (`items.router`). Bu, `app/routers/items.py` dosyasında oluşturduğumuz aynı değişkendir; bir `APIRouter` nesnesidir. + +Sonra aynı işlemi `users` module’ü için de yaparız. + +Ayrıca şöyle de import edebilirdik: + +```Python +from app.routers import items, users +``` + +/// info | Bilgi + +İlk sürüm "relative import"tur: + +```Python +from .routers import items, users +``` + +İkinci sürüm "absolute import"tur: + +```Python +from app.routers import items, users +``` + +Python Packages ve Modules hakkında daha fazlası için, Python'ın Modules ile ilgili resmi dokümantasyonunu okuyun. + +/// + +### İsim Çakışmalarını Önleyin { #avoid-name-collisions } + +`items` submodule’ünü doğrudan import ediyoruz; sadece içindeki `router` değişkenini import etmiyoruz. + +Çünkü `users` submodule’ünde de `router` adlı başka bir değişken var. + +Eğer şöyle sırayla import etseydik: + +```Python +from .routers.items import router +from .routers.users import router +``` + +`users` içindeki `router`, `items` içindeki `router`’ın üstüne yazardı ve ikisini aynı anda kullanamazdık. + +Bu yüzden ikisini de aynı dosyada kullanabilmek için submodule’leri doğrudan import ediyoruz: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *} + +### `users` ve `items` için `APIRouter`’ları Dahil Edin { #include-the-apirouters-for-users-and-items } + +Şimdi `users` ve `items` submodule’lerindeki `router`’ları dahil edelim: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *} + +/// info | Bilgi + +`users.router`, `app/routers/users.py` dosyasının içindeki `APIRouter`’ı içerir. + +`items.router` ise `app/routers/items.py` dosyasının içindeki `APIRouter`’ı içerir. + +/// + +`app.include_router()` ile her bir `APIRouter`’ı ana `FastAPI` uygulamasına ekleyebiliriz. + +Böylece o router içindeki tüm route’lar uygulamanın bir parçası olarak dahil edilir. + +/// note | Teknik Detaylar + +Aslında içeride, `APIRouter` içinde tanımlanan her *path operation* için bir *path operation* oluşturur. + +Yani perde arkasında, her şey tek bir uygulamaymış gibi çalışır. + +/// + +/// check | Ek bilgi + +Router’ları dahil ederken performans konusunda endişelenmeniz gerekmez. + +Bu işlem mikrosaniyeler sürer ve sadece startup sırasında olur. + +Dolayısıyla performansı etkilemez. ⚡ + +/// + +### Özel `prefix`, `tags`, `responses` ve `dependencies` ile Bir `APIRouter` Dahil Edin { #include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies } + +Şimdi, kurumunuzun size `app/internal/admin.py` dosyasını verdiğini düşünelim. + +Bu dosyada, kurumunuzun birden fazla proje arasında paylaştığı bazı admin *path operation*’larını içeren bir `APIRouter` var. + +Bu örnekte çok basit olacak. Ancak kurum içinde başka projelerle paylaşıldığı için, bunu değiştirip `prefix`, `dependencies`, `tags` vs. doğrudan `APIRouter`’a ekleyemediğimizi varsayalım: + +{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} + +Yine de bu `APIRouter`’ı dahil ederken özel bir `prefix` ayarlamak istiyoruz ki tüm *path operation*’ları `/admin` ile başlasın; ayrıca bu projede hâlihazırda kullandığımız `dependencies` ile güvene almak, `tags` ve `responses` eklemek istiyoruz. + +Orijinal `APIRouter`’ı değiştirmeden, bu parametreleri `app.include_router()`’a vererek hepsini tanımlayabiliriz: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *} + +Böylece orijinal `APIRouter` değişmeden kalır; yani aynı `app/internal/admin.py` dosyasını kurum içindeki diğer projelerle de paylaşmaya devam edebiliriz. + +Sonuç olarak, uygulamamızda `admin` module’ündeki her bir *path operation* şunlara sahip olur: + +* `/admin` prefix’i. +* `admin` tag’i. +* `get_token_header` dependency’si. +* `418` response’u. 🍵 + +Ancak bu sadece bizim uygulamamızdaki o `APIRouter` için geçerlidir; onu kullanan diğer kodlar için değil. + +Dolayısıyla örneğin diğer projeler aynı `APIRouter`’ı farklı bir authentication yöntemiyle kullanabilir. + +### Bir *Path Operation* Dahil Edin { #include-a-path-operation } + +*Path operation*’ları doğrudan `FastAPI` uygulamasına da ekleyebiliriz. + +Burada bunu yapıyoruz... sadece yapabildiğimizi göstermek için 🤷: + +{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *} + +ve `app.include_router()` ile eklenen diğer tüm *path operation*’larla birlikte doğru şekilde çalışır. + +/// info | Çok Teknik Detaylar + +**Not**: Bu oldukça teknik bir detay; büyük ihtimalle **direkt geçebilirsiniz**. + +--- + +`APIRouter`’lar "mount" edilmez; uygulamanın geri kalanından izole değildir. + +Çünkü *path operation*’larını OpenAPI şemasına ve kullanıcı arayüzlerine dahil etmek istiyoruz. + +Onları tamamen izole edip bağımsız şekilde "mount" edemediğimiz için, *path operation*’lar doğrudan eklenmek yerine "klonlanır" (yeniden oluşturulur). + +/// + +## Otomatik API Dokümanını Kontrol Edin { #check-the-automatic-api-docs } + +Şimdi uygulamanızı çalıştırın: + +
+ +```console +$ fastapi dev app/main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Ve dokümanları http://127.0.0.1:8000/docs adresinde açın. + +Tüm submodule’lerdeki path’leri, doğru path’ler (ve prefix’ler) ve doğru tag’lerle birlikte içeren otomatik API dokümanını göreceksiniz: + + + +## Aynı Router'ı Farklı `prefix` ile Birden Fazla Kez Dahil Edin { #include-the-same-router-multiple-times-with-different-prefix } + +`.include_router()` ile aynı router’ı farklı prefix’ler kullanarak birden fazla kez de dahil edebilirsiniz. + +Örneğin aynı API’yi `/api/v1` ve `/api/latest` gibi farklı prefix’ler altında sunmak için faydalı olabilir. + +Bu, muhtemelen ihtiyacınız olmayan ileri seviye bir kullanımdır; ancak gerekirse diye mevcut. + +## Bir `APIRouter`’ı Başka Birine Dahil Edin { #include-an-apirouter-in-another } + +Bir `APIRouter`’ı `FastAPI` uygulamasına dahil ettiğiniz gibi, bir `APIRouter`’ı başka bir `APIRouter`’a da şu şekilde dahil edebilirsiniz: + +```Python +router.include_router(other_router) +``` + +`router`’ı `FastAPI` uygulamasına dahil etmeden önce bunu yaptığınızdan emin olun; böylece `other_router` içindeki *path operation*’lar da dahil edilmiş olur. diff --git a/docs/tr/docs/tutorial/body-fields.md b/docs/tr/docs/tutorial/body-fields.md new file mode 100644 index 000000000..6a0f3314a --- /dev/null +++ b/docs/tr/docs/tutorial/body-fields.md @@ -0,0 +1,60 @@ +# Body - Alanlar { #body-fields } + +`Query`, `Path` ve `Body` ile *path operation function* parametrelerinde ek doğrulama ve metadata tanımlayabildiğiniz gibi, Pydantic modellerinin içinde de Pydantic'in `Field`'ını kullanarak doğrulama ve metadata tanımlayabilirsiniz. + +## `Field`'ı import edin { #import-field } + +Önce import etmeniz gerekir: + +{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[4] *} + +/// warning | Uyarı + +`Field`'ın, diğerlerinin (`Query`, `Path`, `Body` vb.) aksine `fastapi`'den değil doğrudan `pydantic`'den import edildiğine dikkat edin. + +/// + +## Model attribute'larını tanımlayın { #declare-model-attributes } + +Ardından `Field`'ı model attribute'larıyla birlikte kullanabilirsiniz: + +{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[11:14] *} + +`Field`, `Query`, `Path` ve `Body` ile aynı şekilde çalışır; aynı parametrelerin tamamına sahiptir, vb. + +/// note | Teknik Detaylar + +Aslında, `Query`, `Path` ve birazdan göreceğiniz diğerleri, ortak bir `Param` sınıfının alt sınıflarından nesneler oluşturur; `Param` sınıfı da Pydantic'in `FieldInfo` sınıfının bir alt sınıfıdır. + +Pydantic'in `Field`'ı da `FieldInfo`'nun bir instance'ını döndürür. + +`Body` ayrıca doğrudan `FieldInfo`'nun bir alt sınıfından nesneler döndürür. Daha sonra göreceğiniz başka bazıları da `Body` sınıfının alt sınıflarıdır. + +`fastapi`'den `Query`, `Path` ve diğerlerini import ettiğinizde, bunların aslında özel sınıflar döndüren fonksiyonlar olduğunu unutmayın. + +/// + +/// tip | İpucu + +Type, varsayılan değer ve `Field` ile tanımlanan her model attribute'unun yapısının, *path operation function* parametresiyle aynı olduğuna dikkat edin; sadece `Path`, `Query` ve `Body` yerine `Field` kullanılmıştır. + +/// + +## Ek bilgi ekleyin { #add-extra-information } + +`Field`, `Query`, `Body` vb. içinde ek bilgi tanımlayabilirsiniz. Bu bilgiler oluşturulan JSON Schema'ya dahil edilir. + +Örnek (examples) tanımlamayı öğrenirken, dokümanların ilerleyen kısımlarında ek bilgi ekleme konusunu daha ayrıntılı göreceksiniz. + +/// warning | Uyarı + +`Field`'a geçirilen ekstra key'ler, uygulamanız için üretilen OpenAPI schema'sında da yer alır. +Bu key'ler OpenAPI spesifikasyonunun bir parçası olmak zorunda olmadığından, örneğin [OpenAPI validator](https://validator.swagger.io/) gibi bazı OpenAPI araçları üretilen schema'nızla çalışmayabilir. + +/// + +## Özet { #recap } + +Model attribute'ları için ek doğrulamalar ve metadata tanımlamak üzere Pydantic'in `Field`'ını kullanabilirsiniz. + +Ayrıca, ek keyword argument'ları kullanarak JSON Schema'ya ekstra metadata da iletebilirsiniz. diff --git a/docs/tr/docs/tutorial/body-multiple-params.md b/docs/tr/docs/tutorial/body-multiple-params.md new file mode 100644 index 000000000..3bac80d6a --- /dev/null +++ b/docs/tr/docs/tutorial/body-multiple-params.md @@ -0,0 +1,175 @@ +# Body - Birden Fazla Parametre { #body-multiple-parameters } + +Artık `Path` ve `Query` kullanmayı gördüğümüze göre, request body bildirimlerinin daha ileri kullanım senaryolarına bakalım. + +## `Path`, `Query` ve body parametrelerini karıştırma { #mix-path-query-and-body-parameters } + +Öncelikle, elbette `Path`, `Query` ve request body parametre bildirimlerini serbestçe karıştırabilirsiniz ve **FastAPI** ne yapacağını bilir. + +Ayrıca, varsayılan değeri `None` yaparak body parametrelerini opsiyonel olarak da tanımlayabilirsiniz: + +{* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} + +/// note | Not + +Bu durumda body'den alınacak `item` opsiyoneldir. Çünkü varsayılan değeri `None` olarak ayarlanmıştır. + +/// + +## Birden fazla body parametresi { #multiple-body-parameters } + +Önceki örnekte, *path operation*'lar `Item`'ın özelliklerini içeren bir JSON body beklerdi, örneğin: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +Ancak birden fazla body parametresi de tanımlayabilirsiniz; örneğin `item` ve `user`: + +{* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *} + + +Bu durumda **FastAPI**, fonksiyonda birden fazla body parametresi olduğunu fark eder (iki parametre de Pydantic modelidir). + +Bunun üzerine, body içinde anahtar (field name) olarak parametre adlarını kullanır ve şu şekilde bir body bekler: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + } +} +``` + +/// note | Not + +`item` daha öncekiyle aynı şekilde tanımlanmış olsa bile, artık body içinde `item` anahtarı altında gelmesi beklenir. + +/// + +**FastAPI**, request'ten otomatik dönüşümü yapar; böylece `item` parametresi kendi içeriğini alır, `user` için de aynı şekilde olur. + +Birleşik verinin validasyonunu yapar ve OpenAPI şeması ile otomatik dokümantasyonda da bunu bu şekilde dokümante eder. + +## Body içinde tekil değerler { #singular-values-in-body } + +Query ve path parametreleri için ek veri tanımlamak üzere `Query` ve `Path` olduğu gibi, **FastAPI** bunların karşılığı olarak `Body` de sağlar. + +Örneğin, önceki modeli genişleterek, aynı body içinde `item` ve `user` dışında bir de `importance` anahtarı olmasını isteyebilirsiniz. + +Bunu olduğu gibi tanımlarsanız, tekil bir değer olduğu için **FastAPI** bunun bir query parametresi olduğunu varsayar. + +Ama `Body` kullanarak, **FastAPI**'ye bunu body içinde başka bir anahtar olarak ele almasını söyleyebilirsiniz: + +{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} + + +Bu durumda **FastAPI** şu şekilde bir body bekler: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + }, + "importance": 5 +} +``` + +Yine veri tiplerini dönüştürür, validate eder, dokümante eder, vb. + +## Birden fazla body parametresi ve query { #multiple-body-params-and-query } + +Elbette ihtiyaç duyduğunuzda, body parametrelerine ek olarak query parametreleri de tanımlayabilirsiniz. + +Varsayılan olarak tekil değerler query parametresi olarak yorumlandığı için, ayrıca `Query` eklemeniz gerekmez; şöyle yazmanız yeterlidir: + +```Python +q: Union[str, None] = None +``` + +Ya da Python 3.10 ve üzeri için: + +```Python +q: str | None = None +``` + +Örneğin: + +{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} + + +/// info | Bilgi + +`Body`, `Query`, `Path` ve daha sonra göreceğiniz diğerleriyle aynı ek validasyon ve metadata parametrelerine de sahiptir. + +/// + +## Tek bir body parametresini gömme { #embed-a-single-body-parameter } + +Diyelim ki Pydantic'teki `Item` modelinden gelen yalnızca tek bir `item` body parametreniz var. + +Varsayılan olarak **FastAPI**, body'nin doğrudan bu modelin içeriği olmasını bekler. + +Ancak, ek body parametreleri tanımladığınızda olduğu gibi, `item` anahtarı olan bir JSON ve onun içinde modelin içeriğini beklemesini istiyorsanız, `Body`'nin özel parametresi olan `embed`'i kullanabilirsiniz: + +```Python +item: Item = Body(embed=True) +``` + +yani şöyle: + +{* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} + + +Bu durumda **FastAPI** şu şekilde bir body bekler: + +```JSON hl_lines="2" +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + } +} +``` + +şunun yerine: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +## Özet { #recap } + +Bir request yalnızca tek bir body içerebilse de, *path operation function*'ınıza birden fazla body parametresi ekleyebilirsiniz. + +Ancak **FastAPI** bunu yönetir; fonksiyonunuza doğru veriyi verir ve *path operation* içinde doğru şemayı validate edip dokümante eder. + +Ayrıca tekil değerlerin body'nin bir parçası olarak alınmasını da tanımlayabilirsiniz. + +Ve yalnızca tek bir parametre tanımlanmış olsa bile, **FastAPI**'ye body'yi bir anahtarın içine gömmesini söyleyebilirsiniz. diff --git a/docs/tr/docs/tutorial/body-nested-models.md b/docs/tr/docs/tutorial/body-nested-models.md new file mode 100644 index 000000000..b4ffef3f1 --- /dev/null +++ b/docs/tr/docs/tutorial/body-nested-models.md @@ -0,0 +1,220 @@ +# Body - İç İçe Modeller { #body-nested-models } + +**FastAPI** ile (Pydantic sayesinde) istediğiniz kadar derin iç içe geçmiş modelleri tanımlayabilir, doğrulayabilir, dokümante edebilir ve kullanabilirsiniz. + +## List alanları { #list-fields } + +Bir attribute’u bir alt tipe sahip olacak şekilde tanımlayabilirsiniz. Örneğin, bir Python `list`: + +{* ../../docs_src/body_nested_models/tutorial001_py310.py hl[12] *} + +Bu, `tags`’in bir list olmasını sağlar; ancak list’in elemanlarının tipini belirtmez. + +## Tip parametresi olan list alanları { #list-fields-with-type-parameter } + +Ancak Python’da, iç tipleri olan list’leri (ya da "type parameter" içeren tipleri) tanımlamanın belirli bir yolu vardır: + +### Tip parametresiyle bir `list` tanımlayın { #declare-a-list-with-a-type-parameter } + +`list`, `dict`, `tuple` gibi type parameter (iç tip) alan tipleri tanımlamak için, iç tipi(leri) köşeli parantezlerle "type parameter" olarak verin: `[` ve `]` + +```Python +my_list: list[str] +``` + +Bu, tip tanımları için standart Python sözdizimidir. + +İç tipleri olan model attribute’ları için de aynı standart sözdizimini kullanın. + +Dolayısıyla örneğimizde, `tags`’i özel olarak bir "string list’i" yapabiliriz: + +{* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} + +## Set tipleri { #set-types } + +Sonra bunu düşününce, tag’lerin tekrar etmemesi gerektiğini fark ederiz; büyük ihtimalle benzersiz string’ler olmalıdır. + +Python’da benzersiz öğelerden oluşan kümeler için özel bir veri tipi vardır: `set`. + +O zaman `tags`’i string’lerden oluşan bir set olarak tanımlayabiliriz: + +{* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} + +Böylece duplicate veri içeren bir request alsanız bile, bu veri benzersiz öğelerden oluşan bir set’e dönüştürülür. + +Ve bu veriyi ne zaman output etseniz, kaynakta duplicate olsa bile, benzersiz öğelerden oluşan bir set olarak output edilir. + +Ayrıca buna göre annotate / dokümante edilir. + +## İç İçe Modeller { #nested-models } + +Bir Pydantic modelinin her attribute’unun bir tipi vardır. + +Ancak bu tip, kendi başına başka bir Pydantic modeli de olabilir. + +Yani belirli attribute adları, tipleri ve validation kurallarıyla derin iç içe JSON "object"leri tanımlayabilirsiniz. + +Hem de istediğiniz kadar iç içe. + +### Bir alt model tanımlayın { #define-a-submodel } + +Örneğin bir `Image` modeli tanımlayabiliriz: + +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} + +### Alt modeli tip olarak kullanın { #use-the-submodel-as-a-type } + +Ardından bunu bir attribute’un tipi olarak kullanabiliriz: + +{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[18] *} + +Bu da **FastAPI**’nin aşağıdakine benzer bir body bekleyeceği anlamına gelir: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": ["rock", "metal", "bar"], + "image": { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + } +} +``` + +Yine, sadece bu tanımı yaparak **FastAPI** ile şunları elde edersiniz: + +* Editör desteği (tamamlama vb.), iç içe modeller için bile +* Veri dönüştürme +* Veri doğrulama (validation) +* Otomatik dokümantasyon + +## Özel tipler ve doğrulama { #special-types-and-validation } + +`str`, `int`, `float` vb. normal tekil tiplerin yanında, `str`’den türeyen daha karmaşık tekil tipleri de kullanabilirsiniz. + +Tüm seçenekleri görmek için Pydantic Type Overview sayfasına göz atın. Sonraki bölümde bazı örnekleri göreceksiniz. + +Örneğin `Image` modelinde bir `url` alanımız olduğuna göre, bunu `str` yerine Pydantic’in `HttpUrl` tipinden bir instance olacak şekilde tanımlayabiliriz: + +{* ../../docs_src/body_nested_models/tutorial005_py310.py hl[2,8] *} + +String’in geçerli bir URL olup olmadığı kontrol edilir ve JSON Schema / OpenAPI’de de buna göre dokümante edilir. + +## Alt modellerden oluşan list’lere sahip attribute’lar { #attributes-with-lists-of-submodels } + +Pydantic modellerini `list`, `set` vb. tiplerin alt tipi olarak da kullanabilirsiniz: + +{* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *} + +Bu, aşağıdaki gibi bir JSON body bekler (dönüştürür, doğrular, dokümante eder vb.): + +```JSON hl_lines="11" +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": [ + "rock", + "metal", + "bar" + ], + "images": [ + { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + }, + { + "url": "http://example.com/dave.jpg", + "name": "The Baz" + } + ] +} +``` + +/// info | Bilgi + +`images` key’inin artık image object’lerinden oluşan bir list içerdiğine dikkat edin. + +/// + +## Çok derin iç içe modeller { #deeply-nested-models } + +İstediğiniz kadar derin iç içe modeller tanımlayabilirsiniz: + +{* ../../docs_src/body_nested_models/tutorial007_py310.py hl[7,12,18,21,25] *} + +/// info | Bilgi + +`Offer`’ın bir `Item` list’i olduğuna, `Item`’ların da opsiyonel bir `Image` list’ine sahip olduğuna dikkat edin. + +/// + +## Sadece list olan body’ler { #bodies-of-pure-lists } + +Beklediğiniz JSON body’nin en üst seviye değeri bir JSON `array` (Python’da `list`) ise, tipi Pydantic modellerinde olduğu gibi fonksiyonun parametresinde tanımlayabilirsiniz: + +```Python +images: list[Image] +``` + +şu örnekte olduğu gibi: + +{* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *} + +## Her yerde editör desteği { #editor-support-everywhere } + +Ve her yerde editör desteği alırsınız. + +List içindeki öğeler için bile: + + + +Pydantic modelleri yerine doğrudan `dict` ile çalışsaydınız bu tür bir editör desteğini alamazdınız. + +Ancak bunlarla uğraşmanız da gerekmez; gelen dict’ler otomatik olarak dönüştürülür ve output’unuz da otomatik olarak JSON’a çevrilir. + +## Rastgele `dict` body’leri { #bodies-of-arbitrary-dicts } + +Body’yi, key’leri bir tipte ve value’ları başka bir tipte olan bir `dict` olarak da tanımlayabilirsiniz. + +Bu şekilde (Pydantic modellerinde olduğu gibi) geçerli field/attribute adlarının önceden ne olduğunu bilmeniz gerekmez. + +Bu, önceden bilmediğiniz key’leri almak istediğiniz durumlarda faydalıdır. + +--- + +Bir diğer faydalı durum da key’lerin başka bir tipte olmasını istediğiniz zamandır (ör. `int`). + +Burada göreceğimiz şey de bu. + +Bu durumda, `int` key’lere ve `float` value’lara sahip olduğu sürece herhangi bir `dict` kabul edersiniz: + +{* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *} + +/// tip | İpucu + +JSON key olarak yalnızca `str` destekler, bunu unutmayın. + +Ancak Pydantic otomatik veri dönüştürme yapar. + +Yani API client’larınız key’leri sadece string olarak gönderebilse bile, bu string’ler saf tamsayı içeriyorsa Pydantic bunları dönüştürür ve doğrular. + +Ve `weights` olarak aldığınız `dict`, gerçekte `int` key’lere ve `float` value’lara sahip olur. + +/// + +## Özet { #recap } + +**FastAPI** ile Pydantic modellerinin sağladığı en yüksek esnekliği elde ederken, kodunuzu da basit, kısa ve şık tutarsınız. + +Üstelik tüm avantajlarla birlikte: + +* Editör desteği (her yerde tamamlama!) +* Veri dönüştürme (diğer adıyla parsing / serialization) +* Veri doğrulama (validation) +* Schema dokümantasyonu +* Otomatik dokümanlar diff --git a/docs/tr/docs/tutorial/body-updates.md b/docs/tr/docs/tutorial/body-updates.md new file mode 100644 index 000000000..a9ad13d2e --- /dev/null +++ b/docs/tr/docs/tutorial/body-updates.md @@ -0,0 +1,100 @@ +# Body - Güncellemeler { #body-updates } + +## `PUT` ile değiştirerek güncelleme { #update-replacing-with-put } + +Bir öğeyi güncellemek için HTTP `PUT` operasyonunu kullanabilirsiniz. + +Girdi verisini JSON olarak saklanabilecek bir formata (ör. bir NoSQL veritabanı ile) dönüştürmek için `jsonable_encoder` kullanabilirsiniz. Örneğin, `datetime` değerlerini `str`'ye çevirmek gibi. + +{* ../../docs_src/body_updates/tutorial001_py310.py hl[28:33] *} + +`PUT`, mevcut verinin yerine geçmesi gereken veriyi almak için kullanılır. + +### Değiştirerek güncelleme uyarısı { #warning-about-replacing } + +Bu, `bar` öğesini `PUT` ile, body içinde şu verilerle güncellemek isterseniz: + +```Python +{ + "name": "Barz", + "price": 3, + "description": None, +} +``` + +zaten kayıtlı olan `"tax": 20.2` alanını içermediği için, input model `"tax": 10.5` varsayılan değerini kullanacaktır. + +Ve veri, bu "yeni" `tax` değeri olan `10.5` ile kaydedilecektir. + +## `PATCH` ile kısmi güncellemeler { #partial-updates-with-patch } + +Veriyi *kısmen* güncellemek için HTTP `PATCH` operasyonunu da kullanabilirsiniz. + +Bu, yalnızca güncellemek istediğiniz veriyi gönderip, geri kalanını olduğu gibi bırakabileceğiniz anlamına gelir. + +/// note | Not + +`PATCH`, `PUT`'a göre daha az yaygın kullanılır ve daha az bilinir. + +Hatta birçok ekip, kısmi güncellemeler için bile yalnızca `PUT` kullanır. + +Bunları nasıl isterseniz öyle kullanmakta **özgürsünüz**; **FastAPI** herhangi bir kısıtlama dayatmaz. + +Ancak bu kılavuz, aşağı yukarı, bunların nasıl kullanılması amaçlandığını gösterir. + +/// + +### Pydantic'in `exclude_unset` parametresini kullanma { #using-pydantics-exclude-unset-parameter } + +Kısmi güncellemeler almak istiyorsanız, Pydantic modelinin `.model_dump()` metodundaki `exclude_unset` parametresini kullanmak çok faydalıdır. + +Örneğin: `item.model_dump(exclude_unset=True)`. + +Bu, `item` modeli oluşturulurken set edilmiş verileri içeren; varsayılan değerleri hariç tutan bir `dict` üretir. + +Sonrasında bunu, yalnızca set edilmiş (request'te gönderilmiş) veriyi içeren; varsayılan değerleri atlayan bir `dict` üretmek için kullanabilirsiniz: + +{* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} + +### Pydantic'in `update` parametresini kullanma { #using-pydantics-update-parameter } + +Artık `.model_copy()` ile mevcut modelin bir kopyasını oluşturup, güncellenecek verileri içeren bir `dict` ile `update` parametresini geçebilirsiniz. + +Örneğin: `stored_item_model.model_copy(update=update_data)`: + +{* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} + +### Kısmi güncellemeler özeti { #partial-updates-recap } + +Özetle, kısmi güncelleme uygulamak için şunları yaparsınız: + +* (İsteğe bağlı olarak) `PUT` yerine `PATCH` kullanın. +* Kayıtlı veriyi alın. +* Bu veriyi bir Pydantic modeline koyun. +* Input modelinden, varsayılan değerler olmadan bir `dict` üretin (`exclude_unset` kullanarak). + * Bu şekilde, modelinizdeki varsayılan değerlerle daha önce saklanmış değerlerin üzerine yazmak yerine, yalnızca kullanıcının gerçekten set ettiği değerleri güncellersiniz. +* Kayıtlı modelin bir kopyasını oluşturun ve alınan kısmi güncellemeleri kullanarak attribute'larını güncelleyin (`update` parametresini kullanarak). +* Kopyalanan modeli DB'nizde saklanabilecek bir şeye dönüştürün (ör. `jsonable_encoder` kullanarak). + * Bu, modelin `.model_dump()` metodunu yeniden kullanmaya benzer; ancak değerlerin JSON'a dönüştürülebilecek veri tiplerine çevrilmesini garanti eder (ör. `datetime` -> `str`). +* Veriyi DB'nize kaydedin. +* Güncellenmiş modeli döndürün. + +{* ../../docs_src/body_updates/tutorial002_py310.py hl[28:35] *} + +/// tip | İpucu + +Aynı tekniği HTTP `PUT` operasyonu ile de kullanabilirsiniz. + +Ancak buradaki örnek `PATCH` kullanıyor, çünkü bu kullanım senaryoları için tasarlanmıştır. + +/// + +/// note | Not + +Input modelin yine de doğrulandığına dikkat edin. + +Dolayısıyla, tüm attribute'ların atlanabildiği kısmi güncellemeler almak istiyorsanız, tüm attribute'ları optional olarak işaretlenmiş (varsayılan değerlerle veya `None` ile) bir modele ihtiyacınız vardır. + +**Güncelleme** için tüm değerleri optional olan modeller ile **oluşturma** için zorunlu değerlere sahip modelleri ayırmak için, [Extra Models](extra-models.md){.internal-link target=_blank} bölümünde anlatılan fikirleri kullanabilirsiniz. + +/// diff --git a/docs/tr/docs/tutorial/body.md b/docs/tr/docs/tutorial/body.md new file mode 100644 index 000000000..0557ef737 --- /dev/null +++ b/docs/tr/docs/tutorial/body.md @@ -0,0 +1,166 @@ +# Request Body { #request-body } + +Bir client'ten (örneğin bir tarayıcıdan) API'nize veri göndermeniz gerektiğinde, bunu **request body** olarak gönderirsiniz. + +Bir **request** body, client'in API'nize gönderdiği veridir. Bir **response** body ise API'nizin client'e gönderdiği veridir. + +API'niz neredeyse her zaman bir **response** body göndermek zorundadır. Ancak client'lerin her zaman **request body** göndermesi gerekmez; bazen sadece bir path isterler, belki birkaç query parametresiyle birlikte, ama body göndermezler. + +Bir **request** body tanımlamak için, tüm gücü ve avantajlarıyla Pydantic modellerini kullanırsınız. + +/// info | Bilgi + +Veri göndermek için şunlardan birini kullanmalısınız: `POST` (en yaygını), `PUT`, `DELETE` veya `PATCH`. + +`GET` request'i ile body göndermek, spesifikasyonlarda tanımsız bir davranıştır; yine de FastAPI bunu yalnızca çok karmaşık/uç kullanım senaryoları için destekler. + +Önerilmediği için Swagger UI ile etkileşimli dokümanlar, `GET` kullanırken body için dokümantasyonu göstermez ve aradaki proxy'ler bunu desteklemeyebilir. + +/// + +## Pydantic'in `BaseModel`'ini import edin { #import-pydantics-basemodel } + +Önce, `pydantic` içinden `BaseModel`'i import etmeniz gerekir: + +{* ../../docs_src/body/tutorial001_py310.py hl[2] *} + +## Veri modelinizi oluşturun { #create-your-data-model } + +Sonra veri modelinizi, `BaseModel`'den kalıtım alan bir class olarak tanımlarsınız. + +Tüm attribute'lar için standart Python type'larını kullanın: + +{* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} + + +Query parametrelerini tanımlarken olduğu gibi, bir model attribute'ü default bir değere sahipse zorunlu değildir. Aksi halde zorunludur. Sadece opsiyonel yapmak için `None` kullanın. + +Örneğin, yukarıdaki model şu şekilde bir JSON "`object`" (veya Python `dict`) tanımlar: + +```JSON +{ + "name": "Foo", + "description": "An optional description", + "price": 45.2, + "tax": 3.5 +} +``` + +...`description` ve `tax` opsiyonel olduğu için (default değerleri `None`), şu JSON "`object`" da geçerli olur: + +```JSON +{ + "name": "Foo", + "price": 45.2 +} +``` + +## Parametre olarak tanımlayın { #declare-it-as-a-parameter } + +Bunu *path operation*'ınıza eklemek için, path ve query parametrelerini tanımladığınız şekilde tanımlayın: + +{* ../../docs_src/body/tutorial001_py310.py hl[16] *} + +...ve type'ını, oluşturduğunuz model olan `Item` olarak belirtin. + +## Sonuçlar { #results } + +Sadece bu Python type tanımıyla, **FastAPI** şunları yapar: + +* Request'in body'sini JSON olarak okur. +* İlgili type'lara dönüştürür (gerekirse). +* Veriyi doğrular (validate eder). + * Veri geçersizse, tam olarak nerede ve hangi verinin hatalı olduğunu söyleyen, anlaşılır bir hata döndürür. +* Aldığı veriyi `item` parametresi içinde size verir. + * Fonksiyonda bunun type'ını `Item` olarak tanımladığınız için, tüm attribute'lar ve type'ları için editor desteğini (tamamlama vb.) de alırsınız. +* Modeliniz için JSON Schema tanımları üretir; projeniz için anlamlıysa bunları başka yerlerde de kullanabilirsiniz. +* Bu şemalar üretilen OpenAPI şemasının bir parçası olur ve otomatik dokümantasyon UIs tarafından kullanılır. + +## Otomatik dokümanlar { #automatic-docs } + +Modellerinizin JSON Schema'ları, OpenAPI tarafından üretilen şemanın bir parçası olur ve etkileşimli API dokümanlarında gösterilir: + + + +Ayrıca, ihtiyaç duyan her *path operation* içindeki API dokümanlarında da kullanılır: + + + +## Editor desteği { #editor-support } + +Editor'ünüzde, fonksiyonunuzun içinde her yerde type hint'leri ve tamamlama (completion) alırsınız (Pydantic modeli yerine `dict` alsaydınız bu olmazdı): + + + +Yanlış type işlemleri için hata kontrolleri de alırsınız: + + + +Bu bir tesadüf değil; tüm framework bu tasarımın etrafında inşa edildi. + +Ayrıca, bunun tüm editor'lerle çalışacağından emin olmak için herhangi bir implementasyon yapılmadan önce tasarım aşamasında kapsamlı şekilde test edildi. + +Hatta bunu desteklemek için Pydantic'in kendisinde bile bazı değişiklikler yapıldı. + +Önceki ekran görüntüleri Visual Studio Code ile alınmıştır. + +Ancak PyCharm ve diğer Python editor'lerinin çoğunda da aynı editor desteğini alırsınız: + + + +/// tip | İpucu + +Editor olarak PyCharm kullanıyorsanız, Pydantic PyCharm Plugin kullanabilirsiniz. + +Pydantic modelleri için editor desteğini şu açılardan iyileştirir: + +* auto-completion +* type checks +* refactoring +* searching +* inspections + +/// + +## Modeli kullanın { #use-the-model } + +Fonksiyonun içinde model nesnesinin tüm attribute'larına doğrudan erişebilirsiniz: + +{* ../../docs_src/body/tutorial002_py310.py *} + +## Request body + path parametreleri { #request-body-path-parameters } + +Path parametrelerini ve request body'yi aynı anda tanımlayabilirsiniz. + +**FastAPI**, path parametreleriyle eşleşen fonksiyon parametrelerinin **path'ten alınması** gerektiğini ve Pydantic model olarak tanımlanan fonksiyon parametrelerinin **request body'den alınması** gerektiğini anlayacaktır. + +{* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} + + +## Request body + path + query parametreleri { #request-body-path-query-parameters } + +**body**, **path** ve **query** parametrelerini aynı anda da tanımlayabilirsiniz. + +**FastAPI** bunların her birini tanır ve veriyi doğru yerden alır. + +{* ../../docs_src/body/tutorial004_py310.py hl[16] *} + +Fonksiyon parametreleri şu şekilde tanınır: + +* Parametre, **path** içinde de tanımlıysa path parametresi olarak kullanılır. +* Parametre **tekil bir type**'taysa (`int`, `float`, `str`, `bool` vb.), **query** parametresi olarak yorumlanır. +* Parametre bir **Pydantic model** type'ı olarak tanımlandıysa, request **body** olarak yorumlanır. + +/// note | Not + +FastAPI, `q` değerinin zorunlu olmadığını `= None` default değerinden anlayacaktır. + +`str | None` (Python 3.10+) veya `Union[str, None]` (Python 3.9+) içindeki `Union`, FastAPI tarafından bu değerin zorunlu olmadığını belirlemek için kullanılmaz; FastAPI bunun zorunlu olmadığını `= None` default değeri olduğu için bilir. + +Ancak type annotation'larını eklemek, editor'ünüzün size daha iyi destek vermesini ve hataları yakalamasını sağlar. + +/// + +## Pydantic olmadan { #without-pydantic } + +Pydantic modellerini kullanmak istemiyorsanız, **Body** parametrelerini de kullanabilirsiniz. [Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank} dokümanına bakın. diff --git a/docs/tr/docs/tutorial/cookie-param-models.md b/docs/tr/docs/tutorial/cookie-param-models.md new file mode 100644 index 000000000..a5bf51560 --- /dev/null +++ b/docs/tr/docs/tutorial/cookie-param-models.md @@ -0,0 +1,76 @@ +# Cookie Parameter Models { #cookie-parameter-models } + +Birbirleriyle ilişkili bir **cookie** grubunuz varsa, bunları tanımlamak için bir **Pydantic model** oluşturabilirsiniz. + +Bu sayede **model'i yeniden kullanabilir**, **birden fazla yerde** tekrar tekrar kullanabilir ve tüm parametreler için validation ve metadata'yı tek seferde tanımlayabilirsiniz. + +/// note | Not + +Bu özellik FastAPI `0.115.0` sürümünden beri desteklenmektedir. + +/// + +/// tip | İpucu + +Aynı teknik `Query`, `Cookie` ve `Header` için de geçerlidir. + +/// + +## Pydantic Model ile Cookies { #cookies-with-a-pydantic-model } + +İhtiyacınız olan **cookie** parametrelerini bir **Pydantic model** içinde tanımlayın ve ardından parametreyi `Cookie` olarak bildirin: + +{* ../../docs_src/cookie_param_models/tutorial001_an_py310.py hl[9:12,16] *} + +**FastAPI**, request ile gelen **cookies** içinden **her bir field** için veriyi **extract** eder ve size tanımladığınız Pydantic model'i verir. + +## Dokümanları Kontrol Edin { #check-the-docs } + +Tanımlanan cookie'leri `/docs` altındaki docs UI'da görebilirsiniz: + +
+ +
+ +/// info | Bilgi + +Tarayıcıların cookie'leri özel biçimlerde ve arka planda yönetmesi nedeniyle, **JavaScript**'in cookie'lere erişmesine kolayca izin vermediğini aklınızda bulundurun. + +`/docs` altındaki **API docs UI**'a giderseniz, *path operation*'larınız için cookie'lerin **dokümantasyonunu** görebilirsiniz. + +Ancak verileri **doldurup** "Execute" düğmesine tıklasanız bile, docs UI **JavaScript** ile çalıştığı için cookie'ler gönderilmez; dolayısıyla hiç değer girmemişsiniz gibi bir **error** mesajı görürsünüz. + +/// + +## Fazladan Cookies'leri Yasaklayın { #forbid-extra-cookies } + +Bazı özel kullanım senaryolarında (muhtemelen çok yaygın değildir) almak istediğiniz cookie'leri **kısıtlamak** isteyebilirsiniz. + +API'niz artık kendi cookie consent'ını kontrol etme gücüne sahip. + +Pydantic'in model configuration'ını kullanarak `extra` olan herhangi bir field'ı `forbid` edebilirsiniz: + +{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *} + +Bir client **fazladan cookie** göndermeye çalışırsa, bir **error** response alır. + +Onayınızı almak için bunca çaba harcayan zavallı cookie banner'ları... API'nin bunu reddetmesi için. + +Örneğin client, değeri `good-list-please` olan bir `santa_tracker` cookie'si göndermeye çalışırsa, client `santa_tracker` cookie is not allowed diyen bir **error** response alır: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["cookie", "santa_tracker"], + "msg": "Extra inputs are not permitted", + "input": "good-list-please", + } + ] +} +``` + +## Özet { #summary } + +**FastAPI**'de **cookies** tanımlamak için **Pydantic model**'lerini kullanabilirsiniz. 😎 diff --git a/docs/tr/docs/tutorial/cors.md b/docs/tr/docs/tutorial/cors.md new file mode 100644 index 000000000..aae560022 --- /dev/null +++ b/docs/tr/docs/tutorial/cors.md @@ -0,0 +1,89 @@ +# CORS (Cross-Origin Resource Sharing) { #cors-cross-origin-resource-sharing } + +CORS veya "Cross-Origin Resource Sharing", tarayıcıda çalışan bir frontend’in JavaScript kodunun bir backend ile iletişim kurduğu ve backend’in frontend’den farklı bir "origin"de olduğu durumları ifade eder. + +## Origin { #origin } + +Origin; protocol (`http`, `https`), domain (`myapp.com`, `localhost`, `localhost.tiangolo.com`) ve port’un (`80`, `443`, `8080`) birleşimidir. + +Dolayısıyla şunların hepsi farklı origin’lerdir: + +* `http://localhost` +* `https://localhost` +* `http://localhost:8080` + +Hepsi `localhost` üzerinde olsa bile, farklı protocol veya port kullandıkları için farklı "origin" sayılırlar. + +## Adımlar { #steps } + +Diyelim ki tarayıcınızda `http://localhost:8080` adresinde çalışan bir frontend’iniz var ve JavaScript’i, `http://localhost` adresinde çalışan bir backend ile iletişim kurmaya çalışıyor (port belirtmediğimiz için tarayıcı varsayılan port olan `80`’i kullanacaktır). + +Bu durumda tarayıcı, `:80`-backend’e bir HTTP `OPTIONS` request’i gönderir. Eğer backend, bu farklı origin’den (`http://localhost:8080`) gelen iletişimi yetkilendiren uygun header’ları gönderirse, `:8080`-tarayıcı frontend’deki JavaScript’in `:80`-backend’e request göndermesine izin verir. + +Bunu sağlayabilmek için `:80`-backend’in bir "allowed origins" listesi olmalıdır. + +Bu örnekte `:8080`-frontend’in doğru çalışması için listede `http://localhost:8080` bulunmalıdır. + +## Wildcard'lar { #wildcards } + +Listeyi `"*"` (bir "wildcard") olarak tanımlayıp, hepsine izin verildiğini söylemek de mümkündür. + +Ancak bu, credentials içeren her şeyi hariç tutarak yalnızca belirli iletişim türlerine izin verir: Cookie’ler, Bearer Token’larla kullanılanlar gibi Authorization header’ları, vb. + +Bu yüzden her şeyin düzgün çalışması için, izin verilen origin’leri açıkça belirtmek daha iyidir. + +## `CORSMiddleware` Kullanımı { #use-corsmiddleware } + +Bunu **FastAPI** uygulamanızda `CORSMiddleware` ile yapılandırabilirsiniz. + +* `CORSMiddleware`’i import edin. +* İzin verilen origin’lerden (string) oluşan bir liste oluşturun. +* Bunu **FastAPI** uygulamanıza bir "middleware" olarak ekleyin. + +Ayrıca backend’in şunlara izin verip vermediğini de belirtebilirsiniz: + +* Credentials (Authorization header’ları, Cookie’ler, vb). +* Belirli HTTP method’ları (`POST`, `PUT`) veya wildcard `"*"` ile hepsini. +* Belirli HTTP header’ları veya wildcard `"*"` ile hepsini. + +{* ../../docs_src/cors/tutorial001_py39.py hl[2,6:11,13:19] *} + + +`CORSMiddleware` implementasyonu tarafından kullanılan varsayılan parametreler kısıtlayıcıdır; bu nedenle tarayıcıların Cross-Domain bağlamında kullanmasına izin vermek için belirli origin’leri, method’ları veya header’ları açıkça etkinleştirmeniz gerekir. + +Aşağıdaki argümanlar desteklenir: + +* `allow_origins` - Cross-origin request yapmasına izin verilmesi gereken origin’lerin listesi. Örn. `['https://example.org', 'https://www.example.org']`. Herhangi bir origin’e izin vermek için `['*']` kullanabilirsiniz. +* `allow_origin_regex` - Cross-origin request yapmasına izin verilmesi gereken origin’lerle eşleşecek bir regex string’i. Örn. `'https://.*\.example\.org'`. +* `allow_methods` - Cross-origin request’lerde izin verilmesi gereken HTTP method’larının listesi. Varsayılanı `['GET']`. Tüm standart method’lara izin vermek için `['*']` kullanabilirsiniz. +* `allow_headers` - Cross-origin request’lerde desteklenmesi gereken HTTP request header’larının listesi. Varsayılanı `[]`. Tüm header’lara izin vermek için `['*']` kullanabilirsiniz. `Accept`, `Accept-Language`, `Content-Language` ve `Content-Type` header’larına basit CORS request'leri için her zaman izin verilir. +* `allow_credentials` - Cross-origin request’ler için cookie desteği olup olmayacağını belirtir. Varsayılanı `False`. + + `allow_credentials` `True` olarak ayarlanmışsa, `allow_origins`, `allow_methods` ve `allow_headers` değerlerinin hiçbiri `['*']` olamaz. Hepsinin açıkça belirtilmesi gerekir. + +* `expose_headers` - Tarayıcının erişebilmesi gereken response header’larını belirtir. Varsayılanı `[]`. +* `max_age` - Tarayıcıların CORS response’larını cache’lemesi için saniye cinsinden azami süreyi ayarlar. Varsayılanı `600`. + +Middleware iki özel HTTP request türüne yanıt verir... + +### CORS preflight request'leri { #cors-preflight-requests } + +Bunlar, `Origin` ve `Access-Control-Request-Method` header’larına sahip herhangi bir `OPTIONS` request’idir. + +Bu durumda middleware gelen request’i intercept eder ve uygun CORS header’larıyla yanıt verir; bilgilendirme amaçlı olarak da `200` veya `400` response döndürür. + +### Basit request'ler { #simple-requests } + +`Origin` header’ı olan herhangi bir request. Bu durumda middleware request’i normal şekilde geçirir, ancak response’a uygun CORS header’larını ekler. + +## Daha Fazla Bilgi { #more-info } + +CORS hakkında daha fazla bilgi için Mozilla CORS dokümantasyonuna bakın. + +/// note | Teknik Detaylar + +`from starlette.middleware.cors import CORSMiddleware` şeklinde de kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olması için `fastapi.middleware` içinde bazı middleware’ler sağlar. Ancak mevcut middleware’lerin çoğu doğrudan Starlette’ten gelir. + +/// diff --git a/docs/tr/docs/tutorial/debugging.md b/docs/tr/docs/tutorial/debugging.md new file mode 100644 index 000000000..54d5c9252 --- /dev/null +++ b/docs/tr/docs/tutorial/debugging.md @@ -0,0 +1,113 @@ +# Debugging { #debugging } + +Visual Studio Code veya PyCharm gibi editörünüzde debugger'ı bağlayabilirsiniz. + +## `uvicorn`'ı Çağırma { #call-uvicorn } + +FastAPI uygulamanızda `uvicorn`'ı import edip doğrudan çalıştırın: + +{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *} + +### `__name__ == "__main__"` Hakkında { #about-name-main } + +`__name__ == "__main__"` ifadesinin temel amacı, dosyanız şu şekilde çağrıldığında çalışacak: + +
+ +```console +$ python myapp.py +``` + +
+ +ancak başka bir dosya onu import ettiğinde çalışmayacak bir kod bölümüne sahip olmaktır, örneğin: + +```Python +from myapp import app +``` + +#### Daha fazla detay { #more-details } + +Dosyanızın adının `myapp.py` olduğunu varsayalım. + +Şu şekilde çalıştırırsanız: + +
+ +```console +$ python myapp.py +``` + +
+ +Python tarafından otomatik oluşturulan, dosyanızın içindeki `__name__` adlı dahili değişkenin değeri `"__main__"` string'i olur. + +Dolayısıyla şu bölüm: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +çalışır. + +--- + +Ancak modülü (dosyayı) import ederseniz bu gerçekleşmez. + +Yani örneğin `importer.py` adında başka bir dosyanız var ve içinde şunlar bulunuyorsa: + +```Python +from myapp import app + +# Some more code +``` + +bu durumda `myapp.py` içindeki otomatik oluşturulan `__name__` değişkeni `"__main__"` değerine sahip olmaz. + +Bu yüzden şu satır: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +çalıştırılmaz. + +/// info | Bilgi + +Daha fazla bilgi için resmi Python dokümantasyonuna bakın. + +/// + +## Kodunuzu Debugger ile Çalıştırma { #run-your-code-with-your-debugger } + +Uvicorn server'ını doğrudan kodunuzdan çalıştırdığınız için, Python programınızı (FastAPI uygulamanızı) debugger'dan doğrudan başlatabilirsiniz. + +--- + +Örneğin Visual Studio Code'da şunları yapabilirsiniz: + +* "Debug" paneline gidin. +* "Add configuration..." seçin. +* "Python" seçin +* "`Python: Current File (Integrated Terminal)`" seçeneğiyle debugger'ı çalıştırın. + +Böylece server, **FastAPI** kodunuzla başlar; breakpoint'lerinizde durur vb. + +Aşağıdaki gibi görünebilir: + + + +--- + +PyCharm kullanıyorsanız şunları yapabilirsiniz: + +* "Run" menüsünü açın. +* "Debug..." seçeneğini seçin. +* Bir context menü açılır. +* Debug edilecek dosyayı seçin (bu örnekte `main.py`). + +Böylece server, **FastAPI** kodunuzla başlar; breakpoint'lerinizde durur vb. + +Aşağıdaki gibi görünebilir: + + diff --git a/docs/tr/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/tr/docs/tutorial/dependencies/classes-as-dependencies.md new file mode 100644 index 000000000..9ee57cb29 --- /dev/null +++ b/docs/tr/docs/tutorial/dependencies/classes-as-dependencies.md @@ -0,0 +1,288 @@ +# Dependency Olarak Class'lar { #classes-as-dependencies } + +**Dependency Injection** sistemine daha derinlemesine geçmeden önce, bir önceki örneği geliştirelim. + +## Önceki Örnekten Bir `dict` { #a-dict-from-the-previous-example } + +Önceki örnekte, dependency'mizden ("dependable") bir `dict` döndürüyorduk: + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[9] *} + +Ama sonra *path operation function* içindeki `commons` parametresinde bir `dict` alıyoruz. + +Ve biliyoruz ki editor'ler `dict`'ler için çok fazla destek (ör. completion) veremez; çünkü key'lerini ve value type'larını bilemezler. + +Daha iyisini yapabiliriz... + +## Bir Şeyi Dependency Yapan Nedir { #what-makes-a-dependency } + +Şimdiye kadar dependency'leri function olarak tanımlanmış şekilde gördünüz. + +Ancak dependency tanımlamanın tek yolu bu değil (muhtemelen en yaygını bu olsa da). + +Buradaki kritik nokta, bir dependency'nin "callable" olması gerektiğidir. + +Python'da "**callable**", Python'ın bir function gibi "çağırabildiği" her şeydir. + +Yani elinizde `something` adlı bir nesne varsa (function _olmak zorunda değil_) ve onu şöyle "çağırabiliyorsanız" (çalıştırabiliyorsanız): + +```Python +something() +``` + +veya + +```Python +something(some_argument, some_keyword_argument="foo") +``` + +o zaman bu bir "callable" demektir. + +## Dependency Olarak Class'lar { #classes-as-dependencies_1 } + +Python'da bir class'tan instance oluştururken de aynı söz dizimini kullandığınızı fark etmiş olabilirsiniz. + +Örneğin: + +```Python +class Cat: + def __init__(self, name: str): + self.name = name + + +fluffy = Cat(name="Mr Fluffy") +``` + +Bu durumda `fluffy`, `Cat` class'ının bir instance'ıdır. + +Ve `fluffy` oluşturmak için `Cat`'i "çağırmış" olursunuz. + +Dolayısıyla bir Python class'ı da bir **callable**'dır. + +O zaman **FastAPI** içinde bir Python class'ını dependency olarak kullanabilirsiniz. + +FastAPI'nin aslında kontrol ettiği şey, bunun bir "callable" olması (function, class ya da başka bir şey) ve tanımlı parametreleridir. + +Eğer **FastAPI**'de bir dependency olarak bir "callable" verirseniz, FastAPI o "callable" için parametreleri analiz eder ve bunları *path operation function* parametreleriyle aynı şekilde işler. Sub-dependency'ler dahil. + +Bu, hiç parametresi olmayan callable'lar için de geçerlidir. Tıpkı hiç parametresi olmayan *path operation function*'larda olduğu gibi. + +O zaman yukarıdaki `common_parameters` adlı "dependable" dependency'sini `CommonQueryParams` class'ına çevirebiliriz: + +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[11:15] *} + +Class instance'ını oluşturmak için kullanılan `__init__` metoduna dikkat edin: + +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[12] *} + +...bizim önceki `common_parameters` ile aynı parametrelere sahip: + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8] *} + +Bu parametreler, dependency'yi "çözmek" için **FastAPI**'nin kullanacağı şeylerdir. + +Her iki durumda da şunlar olacak: + +* `str` olan opsiyonel bir `q` query parametresi. +* Default değeri `0` olan `int` tipinde bir `skip` query parametresi. +* Default değeri `100` olan `int` tipinde bir `limit` query parametresi. + +Her iki durumda da veriler dönüştürülecek, doğrulanacak, OpenAPI şemasında dokümante edilecek, vb. + +## Kullanalım { #use-it } + +Artık bu class'ı kullanarak dependency'nizi tanımlayabilirsiniz. + +{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[19] *} + +**FastAPI**, `CommonQueryParams` class'ını çağırır. Bu, o class'ın bir "instance"ını oluşturur ve bu instance, sizin function'ınıza `commons` parametresi olarak geçirilir. + +## Type Annotation vs `Depends` { #type-annotation-vs-depends } + +Yukarıdaki kodda `CommonQueryParams`'ı iki kez yazdığımıza dikkat edin: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ Annotated Olmadan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü kullanmayı tercih edin. + +/// + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +//// + +Şuradaki son `CommonQueryParams`: + +```Python +... Depends(CommonQueryParams) +``` + +...FastAPI'nin dependency'nin ne olduğunu anlamak için gerçekten kullandığı şeydir. + +FastAPI tanımlanan parametreleri buradan çıkarır ve aslında çağıracağı şey de budur. + +--- + +Bu durumda, şuradaki ilk `CommonQueryParams`: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, ... +``` + +//// + +//// tab | Python 3.9+ Annotated Olmadan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü kullanmayı tercih edin. + +/// + +```Python +commons: CommonQueryParams ... +``` + +//// + +...**FastAPI** için özel bir anlam taşımaz. FastAPI bunu veri dönüştürme, doğrulama vb. için kullanmaz (çünkü bunlar için `Depends(CommonQueryParams)` kullanıyor). + +Hatta şunu bile yazabilirsiniz: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[Any, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ Annotated Olmadan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü kullanmayı tercih edin. + +/// + +```Python +commons = Depends(CommonQueryParams) +``` + +//// + +...şu örnekte olduğu gibi: + +{* ../../docs_src/dependencies/tutorial003_an_py310.py hl[19] *} + +Ancak type'ı belirtmeniz önerilir; böylece editor'ünüz `commons` parametresine ne geçirileceğini bilir ve size code completion, type check'leri vb. konularda yardımcı olur: + + + +## Kısayol { #shortcut } + +Ama burada bir miktar kod tekrarımız olduğunu görüyorsunuz; `CommonQueryParams`'ı iki kez yazıyoruz: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ Annotated Olmadan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü kullanmayı tercih edin. + +/// + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +//// + +**FastAPI**, bu durumlar için bir kısayol sağlar: dependency'nin *özellikle* FastAPI'nin bir instance oluşturmak için "çağıracağı" bir class olduğu durumlar. + +Bu özel durumlarda şunu yapabilirsiniz: + +Şunu yazmak yerine: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] +``` + +//// + +//// tab | Python 3.9+ Annotated Olmadan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü kullanmayı tercih edin. + +/// + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +//// + +...şunu yazarsınız: + +//// tab | Python 3.9+ + +```Python +commons: Annotated[CommonQueryParams, Depends()] +``` + +//// + +//// tab | Python 3.9+ Annotated Olmadan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü kullanmayı tercih edin. + +/// + +```Python +commons: CommonQueryParams = Depends() +``` + +//// + +Dependency'yi parametrenin type'ı olarak tanımlarsınız ve `Depends(CommonQueryParams)` içinde class'ı *yeniden* yazmak yerine, parametre vermeden `Depends()` kullanırsınız. + +Aynı örnek şu hale gelir: + +{* ../../docs_src/dependencies/tutorial004_an_py310.py hl[19] *} + +...ve **FastAPI** ne yapması gerektiğini bilir. + +/// tip | İpucu + +Bu size faydalı olmaktan çok kafa karıştırıcı geliyorsa, kullanmayın; buna *ihtiyacınız* yok. + +Bu sadece bir kısayoldur. Çünkü **FastAPI** kod tekrarını en aza indirmenize yardımcı olmayı önemser. + +/// diff --git a/docs/tr/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/tr/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md new file mode 100644 index 000000000..4903aec4a --- /dev/null +++ b/docs/tr/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -0,0 +1,69 @@ +# Path Operation Decorator'lerinde Dependency'ler { #dependencies-in-path-operation-decorators } + +Bazı durumlarda bir dependency'nin döndürdüğü değere *path operation function* içinde gerçekten ihtiyacınız olmaz. + +Ya da dependency zaten bir değer döndürmüyordur. + +Ancak yine de çalıştırılmasını/çözülmesini istersiniz. + +Bu gibi durumlarda, `Depends` ile bir *path operation function* parametresi tanımlamak yerine, *path operation decorator*'üne `dependencies` adında bir `list` ekleyebilirsiniz. + +## *Path Operation Decorator*'üne `dependencies` Ekleyin { #add-dependencies-to-the-path-operation-decorator } + +*Path operation decorator*, opsiyonel bir `dependencies` argümanı alır. + +Bu, `Depends()` öğelerinden oluşan bir `list` olmalıdır: + +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[19] *} + +Bu dependency'ler normal dependency'lerle aynı şekilde çalıştırılır/çözülür. Ancak (eğer bir değer döndürüyorlarsa) bu değer *path operation function*'ınıza aktarılmaz. + +/// tip | İpucu + +Bazı editörler, kullanılmayan function parametrelerini kontrol eder ve bunları hata olarak gösterebilir. + +Bu `dependencies` yaklaşımıyla, editör/araç hatalarına takılmadan dependency'lerin çalıştırılmasını sağlayabilirsiniz. + +Ayrıca kodunuzda kullanılmayan bir parametreyi gören yeni geliştiricilerin bunun gereksiz olduğunu düşünmesi gibi bir kafa karışıklığını da azaltabilir. + +/// + +/// info | Bilgi + +Bu örnekte uydurma özel header'lar olan `X-Key` ve `X-Token` kullanıyoruz. + +Ancak gerçek senaryolarda, security uygularken, entegre [Security yardımcı araçlarını (bir sonraki bölüm)](../security/index.md){.internal-link target=_blank} kullanmak size daha fazla fayda sağlar. + +/// + +## Dependency Hataları ve Return Değerleri { #dependencies-errors-and-return-values } + +Normalde kullandığınız aynı dependency *function*'larını burada da kullanabilirsiniz. + +### Dependency Gereksinimleri { #dependency-requirements } + +Request gereksinimleri (header'lar gibi) veya başka alt dependency'ler tanımlayabilirler: + +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[8,13] *} + +### Exception Fırlatmak { #raise-exceptions } + +Bu dependency'ler, normal dependency'lerde olduğu gibi `raise` ile exception fırlatabilir: + +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[10,15] *} + +### Return Değerleri { #return-values } + +Ayrıca değer döndürebilirler ya da döndürmeyebilirler; dönen değer kullanılmayacaktır. + +Yani başka bir yerde zaten kullandığınız, değer döndüren normal bir dependency'yi tekrar kullanabilirsiniz; değer kullanılmasa bile dependency çalıştırılacaktır: + +{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[11,16] *} + +## Bir *Path Operation* Grubu İçin Dependency'ler { #dependencies-for-a-group-of-path-operations } + +Daha sonra, muhtemelen birden fazla dosya kullanarak daha büyük uygulamaları nasıl yapılandıracağınızı okurken ([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank}), bir *path operation* grubu için tek bir `dependencies` parametresini nasıl tanımlayacağınızı öğreneceksiniz. + +## Global Dependency'ler { #global-dependencies } + +Sırada, dependency'leri tüm `FastAPI` uygulamasına nasıl ekleyeceğimizi göreceğiz; böylece her *path operation* için geçerli olacaklar. diff --git a/docs/tr/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/tr/docs/tutorial/dependencies/dependencies-with-yield.md new file mode 100644 index 000000000..bd025f799 --- /dev/null +++ b/docs/tr/docs/tutorial/dependencies/dependencies-with-yield.md @@ -0,0 +1,289 @@ +# `yield` ile Dependency'ler { #dependencies-with-yield } + +FastAPI, işini bitirdikten sonra ek adımlar çalıştıran dependency'leri destekler. + +Bunu yapmak için `return` yerine `yield` kullanın ve ek adımları (kodu) `yield` satırından sonra yazın. + +/// tip | İpucu + +Her dependency için yalnızca **bir kez** `yield` kullandığınızdan emin olun. + +/// + +/// note | Teknik Detaylar + +Şunlarla kullanılabilen herhangi bir fonksiyon: + +* `@contextlib.contextmanager` veya +* `@contextlib.asynccontextmanager` + +bir **FastAPI** dependency'si olarak kullanılabilir. + +Hatta FastAPI bu iki decorator'ı içeride (internally) kullanır. + +/// + +## `yield` ile Bir Veritabanı Dependency'si { #a-database-dependency-with-yield } + +Örneğin bunu, bir veritabanı session'ı oluşturmak ve iş bittikten sonra kapatmak için kullanabilirsiniz. + +Response oluşturulmadan önce yalnızca `yield` satırına kadar olan (ve `yield` dahil) kod çalıştırılır: + +{* ../../docs_src/dependencies/tutorial007_py39.py hl[2:4] *} + +`yield` edilen değer, *path operation*'lara ve diğer dependency'lere enjekte edilen (injected) değerdir: + +{* ../../docs_src/dependencies/tutorial007_py39.py hl[4] *} + +Response'dan sonra `yield` satırını takip eden kod çalıştırılır: + +{* ../../docs_src/dependencies/tutorial007_py39.py hl[5:6] *} + +/// tip | İpucu + +`async` ya da normal fonksiyonlar kullanabilirsiniz. + +**FastAPI**, normal dependency'lerde olduğu gibi her ikisinde de doğru şekilde davranır. + +/// + +## `yield` ve `try` ile Bir Dependency { #a-dependency-with-yield-and-try } + +`yield` kullanan bir dependency içinde bir `try` bloğu kullanırsanız, dependency kullanılırken fırlatılan (raised) herhangi bir exception'ı alırsınız. + +Örneğin, başka bir dependency'de veya bir *path operation* içinde çalışan bir kod, bir veritabanı transaction'ını "rollback" yaptıysa ya da başka bir exception oluşturduysa, o exception dependency'nizde size gelir. + +Dolayısıyla `except SomeException` ile dependency içinde o spesifik exception'ı yakalayabilirsiniz. + +Aynı şekilde, exception olsun ya da olmasın çıkış (exit) adımlarının çalıştırılmasını garanti etmek için `finally` kullanabilirsiniz. + +{* ../../docs_src/dependencies/tutorial007_py39.py hl[3,5] *} + +## `yield` ile Alt Dependency'ler { #sub-dependencies-with-yield } + +Her boyutta ve şekilde alt dependency'ler ve alt dependency "ağaçları" (trees) oluşturabilirsiniz; bunların herhangi biri veya hepsi `yield` kullanabilir. + +**FastAPI**, `yield` kullanan her dependency'deki "exit code"'un doğru sırayla çalıştırılmasını sağlar. + +Örneğin, `dependency_c`, `dependency_b`'ye; `dependency_b` de `dependency_a`'ya bağlı olabilir: + +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[6,14,22] *} + +Ve hepsi `yield` kullanabilir. + +Bu durumda `dependency_c`, exit code'unu çalıştırabilmek için `dependency_b`'den gelen değerin (burada `dep_b`) hâlâ erişilebilir olmasına ihtiyaç duyar. + +Aynı şekilde `dependency_b` de exit code'u için `dependency_a`'dan gelen değerin (burada `dep_a`) erişilebilir olmasına ihtiyaç duyar. + +{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[18:19,26:27] *} + +Benzer şekilde, bazı dependency'ler `yield`, bazıları `return` kullanabilir ve bunların bazıları diğerlerine bağlı olabilir. + +Ayrıca birden fazla `yield` kullanan dependency gerektiren tek bir dependency'niz de olabilir, vb. + +İstediğiniz herhangi bir dependency kombinasyonunu kullanabilirsiniz. + +**FastAPI** her şeyin doğru sırada çalışmasını sağlar. + +/// note | Teknik Detaylar + +Bu, Python'un Context Managers yapısı sayesinde çalışır. + +**FastAPI** bunu sağlamak için içeride onları kullanır. + +/// + +## `yield` ve `HTTPException` ile Dependency'ler { #dependencies-with-yield-and-httpexception } + +`yield` kullanan dependency'lerde `try` bloklarıyla bazı kodları çalıştırıp ardından `finally` sonrasında exit code çalıştırabileceğinizi gördünüz. + +Ayrıca `except` ile fırlatılan exception'ı yakalayıp onunla bir şey yapabilirsiniz. + +Örneğin `HTTPException` gibi farklı bir exception fırlatabilirsiniz. + +/// tip | İpucu + +Bu biraz ileri seviye bir tekniktir ve çoğu durumda gerçekten ihtiyaç duymazsınız; çünkü exception'ları (`HTTPException` dahil) uygulamanızın geri kalan kodundan, örneğin *path operation function* içinden fırlatabilirsiniz. + +Ama ihtiyaç duyarsanız diye burada. 🤓 + +/// + +{* ../../docs_src/dependencies/tutorial008b_an_py39.py hl[18:22,31] *} + +Exception yakalayıp buna göre özel bir response oluşturmak istiyorsanız bir [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} oluşturun. + +## `yield` ve `except` ile Dependency'ler { #dependencies-with-yield-and-except } + +`yield` kullanan bir dependency içinde `except` ile bir exception yakalar ve bunu tekrar fırlatmazsanız (ya da yeni bir exception fırlatmazsanız), FastAPI normal Python'da olduğu gibi bir exception olduğunu fark edemez: + +{* ../../docs_src/dependencies/tutorial008c_an_py39.py hl[15:16] *} + +Bu durumda client, biz `HTTPException` veya benzeri bir şey fırlatmadığımız için olması gerektiği gibi *HTTP 500 Internal Server Error* response'u görür; ancak server **hiç log üretmez** ve hatanın ne olduğuna dair başka bir işaret de olmaz. 😱 + +### `yield` ve `except` Kullanan Dependency'lerde Her Zaman `raise` Edin { #always-raise-in-dependencies-with-yield-and-except } + +`yield` kullanan bir dependency içinde bir exception yakalarsanız, başka bir `HTTPException` veya benzeri bir şey fırlatmıyorsanız, **orijinal exception'ı tekrar raise etmelisiniz**. + +Aynı exception'ı `raise` ile tekrar fırlatabilirsiniz: + +{* ../../docs_src/dependencies/tutorial008d_an_py39.py hl[17] *} + +Artık client yine aynı *HTTP 500 Internal Server Error* response'unu alır, ama server log'larda bizim özel `InternalError`'ımızı görür. 😎 + +## `yield` Kullanan Dependency'lerin Çalıştırılması { #execution-of-dependencies-with-yield } + +Çalıştırma sırası kabaca aşağıdaki diyagramdaki gibidir. Zaman yukarıdan aşağı akar. Her sütun, etkileşime giren veya kod çalıştıran parçalardan birini temsil eder. + +```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 | Bilgi + +Client'a yalnızca **tek bir response** gönderilir. Bu, error response'lardan biri olabilir ya da *path operation*'dan dönen response olabilir. + +Bu response'lardan biri gönderildikten sonra başka bir response gönderilemez. + +/// + +/// tip | İpucu + +*Path operation function* içindeki koddan herhangi bir exception raise ederseniz, `HTTPException` dahil olmak üzere bu exception `yield` kullanan dependency'lere aktarılır. Çoğu durumda, doğru şekilde ele alındığından emin olmak için `yield` kullanan dependency'den aynı exception'ı (veya yeni bir tanesini) yeniden raise etmek istersiniz. + +/// + +## Erken Çıkış ve `scope` { #early-exit-and-scope } + +Normalde `yield` kullanan dependency'lerin exit code'u, client'a response gönderildikten **sonra** çalıştırılır. + +Ama *path operation function*'dan döndükten sonra dependency'yi kullanmayacağınızı biliyorsanız, `Depends(scope="function")` kullanarak FastAPI'ye dependency'yi *path operation function* döndükten sonra kapatmasını, ancak **response gönderilmeden önce** kapatmasını söyleyebilirsiniz. + +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} + +`Depends()` şu değerleri alabilen bir `scope` parametresi alır: + +* `"function"`: dependency'yi request'i işleyen *path operation function* çalışmadan önce başlat, *path operation function* bittikten sonra bitir, ancak response client'a geri gönderilmeden **önce** sonlandır. Yani dependency fonksiyonu, *path operation **function***'ın **etrafında** çalıştırılır. +* `"request"`: dependency'yi request'i işleyen *path operation function* çalışmadan önce başlat (`"function"` kullanımına benzer), ancak response client'a geri gönderildikten **sonra** sonlandır. Yani dependency fonksiyonu, **request** ve response döngüsünün **etrafında** çalıştırılır. + +Belirtilmezse ve dependency `yield` kullanıyorsa, varsayılan olarak `scope` `"request"` olur. + +### Alt dependency'ler için `scope` { #scope-for-sub-dependencies } + +`scope="request"` (varsayılan) ile bir dependency tanımladığınızda, herhangi bir alt dependency'nin `scope` değeri de `"request"` olmalıdır. + +Ancak `scope` değeri `"function"` olan bir dependency, hem `"function"` hem de `"request"` scope'una sahip dependency'lere bağlı olabilir. + +Bunun nedeni, bir dependency'nin exit code'unu alt dependency'lerden önce çalıştırabilmesi gerekmesidir; çünkü exit code sırasında hâlâ onları kullanması gerekebilir. + +```mermaid +sequenceDiagram + +participant client as Client +participant dep_req as Dep scope="request" +participant dep_func as Dep scope="function" +participant operation as Path Operation + + client ->> dep_req: Start request + Note over dep_req: Run code up to yield + dep_req ->> dep_func: Pass dependency + Note over dep_func: Run code up to yield + dep_func ->> operation: Run path operation with dependency + operation ->> dep_func: Return from path operation + Note over dep_func: Run code after yield + Note over dep_func: ✅ Dependency closed + dep_func ->> client: Send response to client + Note over client: Response sent + Note over dep_req: Run code after yield + Note over dep_req: ✅ Dependency closed +``` + +## `yield`, `HTTPException`, `except` ve Background Tasks ile Dependency'ler { #dependencies-with-yield-httpexception-except-and-background-tasks } + +`yield` kullanan dependency'ler, zaman içinde farklı kullanım senaryolarını kapsamak ve bazı sorunları düzeltmek için gelişti. + +FastAPI'nin farklı sürümlerinde nelerin değiştiğini görmek isterseniz, advanced guide'da şu bölümü okuyabilirsiniz: [Advanced Dependencies - Dependencies with `yield`, `HTTPException`, `except` and Background Tasks](../../advanced/advanced-dependencies.md#dependencies-with-yield-httpexception-except-and-background-tasks){.internal-link target=_blank}. + +## Context Managers { #context-managers } + +### "Context Managers" Nedir? { #what-are-context-managers } + +"Context Managers", `with` ifadesiyle kullanabildiğiniz Python nesneleridir. + +Örneğin, bir dosyayı okumak için `with` kullanabilirsiniz: + +```Python +with open("./somefile.txt") as f: + contents = f.read() + print(contents) +``` + +Temelde `open("./somefile.txt")`, "Context Manager" olarak adlandırılan bir nesne oluşturur. + +`with` bloğu bittiğinde, exception olsa bile dosyanın kapatılmasını garanti eder. + +`yield` ile bir dependency oluşturduğunuzda, **FastAPI** içeride bunun için bir context manager oluşturur ve bazı ilgili başka araçlarla birleştirir. + +### `yield` kullanan dependency'lerde context manager kullanma { #using-context-managers-in-dependencies-with-yield } + +/// warning | Uyarı + +Bu, az çok "ileri seviye" bir fikirdir. + +**FastAPI**'ye yeni başlıyorsanız şimdilik bunu atlamak isteyebilirsiniz. + +/// + +Python'da Context Manager'ları, iki method'a sahip bir class oluşturarak: `__enter__()` ve `__exit__()` yaratabilirsiniz. + +Ayrıca dependency fonksiyonunun içinde `with` veya `async with` ifadeleri kullanarak **FastAPI**'de `yield` kullanan dependency'lerin içinde de kullanabilirsiniz: + +{* ../../docs_src/dependencies/tutorial010_py39.py hl[1:9,13] *} + +/// tip | İpucu + +Bir context manager oluşturmanın başka bir yolu da şunlardır: + +* `@contextlib.contextmanager` veya +* `@contextlib.asynccontextmanager` + +Bunları, tek bir `yield` içeren bir fonksiyonu decorate etmek için kullanabilirsiniz. + +FastAPI, `yield` kullanan dependency'ler için içeride bunu yapar. + +Ancak FastAPI dependency'leri için bu decorator'ları kullanmak zorunda değilsiniz (hatta kullanmamalısınız). + +FastAPI bunu sizin yerinize içeride yapar. + +/// diff --git a/docs/tr/docs/tutorial/dependencies/global-dependencies.md b/docs/tr/docs/tutorial/dependencies/global-dependencies.md new file mode 100644 index 000000000..7f0025eaf --- /dev/null +++ b/docs/tr/docs/tutorial/dependencies/global-dependencies.md @@ -0,0 +1,16 @@ +# Global Dependencies { #global-dependencies } + +Bazı uygulama türlerinde, tüm uygulama için dependency eklemek isteyebilirsiniz. + +[`dependencies`'i *path operation decorator*'larına ekleyebildiğiniz](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} gibi, `FastAPI` uygulamasına da ekleyebilirsiniz. + +Bu durumda, uygulamadaki tüm *path operation*'lara uygulanırlar: + +{* ../../docs_src/dependencies/tutorial012_an_py39.py hl[17] *} + + +Ve [*path operation decorator*'larına `dependencies` ekleme](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} bölümündeki tüm fikirler hâlâ geçerlidir; ancak bu sefer, uygulamadaki tüm *path operation*'lar için geçerli olur. + +## *Path operations* grupları için Dependencies { #dependencies-for-groups-of-path-operations } + +İleride, daha büyük uygulamaları nasıl yapılandıracağınızı ([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank}) okurken, muhtemelen birden fazla dosyayla birlikte, bir *path operations* grubu için tek bir `dependencies` parametresini nasıl tanımlayacağınızı öğreneceksiniz. diff --git a/docs/tr/docs/tutorial/dependencies/index.md b/docs/tr/docs/tutorial/dependencies/index.md new file mode 100644 index 000000000..f1e446d67 --- /dev/null +++ b/docs/tr/docs/tutorial/dependencies/index.md @@ -0,0 +1,250 @@ +# Bağımlılıklar { #dependencies } + +**FastAPI**, çok güçlü ama aynı zamanda sezgisel bir **Dependency Injection** sistemine sahiptir. + +Kullanımı çok basit olacak şekilde tasarlanmıştır ve herhangi bir geliştiricinin diğer bileşenleri **FastAPI** ile entegre etmesini kolaylaştırır. + +## "Dependency Injection" Nedir? { #what-is-dependency-injection } + +Programlamada **"Dependency Injection"**, kodunuzun (bu örnekte *path operation function*'larınızın) çalışmak ve kullanmak için ihtiyaç duyduğu şeyleri: "dependencies" (bağımlılıklar) olarak beyan edebilmesi anlamına gelir. + +Ardından bu sistem (bu örnekte **FastAPI**), kodunuza gerekli bağımlılıkları sağlamak ("inject" etmek) için gereken her şeyi sizin yerinize halleder. + +Bu yaklaşım, şunlara ihtiyaç duyduğunuzda özellikle faydalıdır: + +* Paylaşılan bir mantığa sahip olmak (aynı kod mantığını tekrar tekrar kullanmak). +* Veritabanı bağlantılarını paylaşmak. +* Güvenlik, authentication, rol gereksinimleri vb. kuralları zorunlu kılmak. +* Ve daha birçok şey... + +Tüm bunları, kod tekrarını minimumda tutarak yaparsınız. + +## İlk Adımlar { #first-steps } + +Çok basit bir örneğe bakalım. Şimdilik o kadar basit olacak ki pek işe yaramayacak. + +Ama bu sayede **Dependency Injection** sisteminin nasıl çalıştığına odaklanabiliriz. + +### Bir dependency (bağımlılık) veya "dependable" Oluşturun { #create-a-dependency-or-dependable } + +Önce dependency'e odaklanalım. + +Bu, bir *path operation function*'ın alabileceği parametrelerin aynısını alabilen basit bir fonksiyondur: + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8:9] *} + +Bu kadar. + +**2 satır**. + +Ve tüm *path operation function*'larınızla aynı şekle ve yapıya sahiptir. + +Bunu, "decorator" olmadan (yani `@app.get("/some-path")` olmadan) yazılmış bir *path operation function* gibi düşünebilirsiniz. + +Ayrıca istediğiniz herhangi bir şeyi döndürebilir. + +Bu örnekte, bu dependency şunları bekler: + +* `str` olan, opsiyonel bir query parametresi `q`. +* `int` olan, opsiyonel bir query parametresi `skip` ve varsayılanı `0`. +* `int` olan, opsiyonel bir query parametresi `limit` ve varsayılanı `100`. + +Sonra da bu değerleri içeren bir `dict` döndürür. + +/// info | Bilgi + +FastAPI, `Annotated` desteğini 0.95.0 sürümünde ekledi (ve önermeye başladı). + +Daha eski bir sürüm kullanıyorsanız `Annotated` kullanmaya çalıştığınızda hata alırsınız. + +`Annotated` kullanmadan önce **FastAPI** sürümünü en az 0.95.1'e yükseltmek için [FastAPI sürümünü yükseltin](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}. + +/// + +### `Depends`'i Import Edin { #import-depends } + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[3] *} + +### "Dependant" İçinde Dependency'yi Tanımlayın { #declare-the-dependency-in-the-dependant } + +*Path operation function* parametrelerinizde `Body`, `Query` vb. kullandığınız gibi, yeni bir parametreyle `Depends` kullanın: + +{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[13,18] *} + +Fonksiyon parametrelerinde `Depends`'i `Body`, `Query` vb. ile aynı şekilde kullansanız da `Depends` biraz farklı çalışır. + +`Depends`'e yalnızca tek bir parametre verirsiniz. + +Bu parametre, bir fonksiyon gibi bir şey olmalıdır. + +Onu doğrudan **çağırmazsınız** (sonuna parantez eklemezsiniz), sadece `Depends()`'e parametre olarak verirsiniz. + +Ve bu fonksiyon da, *path operation function*'lar gibi parametre alır. + +/// tip | İpucu + +Fonksiyonların dışında başka hangi "şeylerin" dependency olarak kullanılabildiğini bir sonraki bölümde göreceksiniz. + +/// + +Yeni bir request geldiğinde, **FastAPI** şunları sizin yerinize yapar: + +* Dependency ("dependable") fonksiyonunuzu doğru parametrelerle çağırır. +* Fonksiyonunuzun sonucunu alır. +* Bu sonucu *path operation function*'ınızdaki parametreye atar. + +```mermaid +graph TB + +common_parameters(["common_parameters"]) +read_items["/items/"] +read_users["/users/"] + +common_parameters --> read_items +common_parameters --> read_users +``` + +Bu şekilde paylaşılan kodu bir kez yazarsınız ve onu *path operation*'larda çağırma işini **FastAPI** halleder. + +/// check | Ek bilgi + +Dikkat edin: Bunu "register" etmek ya da benzeri bir şey yapmak için özel bir class oluşturup **FastAPI**'ye bir yere geçirmeniz gerekmez. + +Sadece `Depends`'e verirsiniz ve gerisini **FastAPI** nasıl yapacağını bilir. + +/// + +## `Annotated` Dependency'lerini Paylaşın { #share-annotated-dependencies } + +Yukarıdaki örneklerde, ufak bir **kod tekrarı** olduğunu görüyorsunuz. + +`common_parameters()` dependency'sini kullanmanız gerektiğinde, type annotation ve `Depends()` içeren parametrenin tamamını yazmanız gerekir: + +```Python +commons: Annotated[dict, Depends(common_parameters)] +``` + +Ancak `Annotated` kullandığımız için bu `Annotated` değerini bir değişkende saklayıp birden fazla yerde kullanabiliriz: + +{* ../../docs_src/dependencies/tutorial001_02_an_py310.py hl[12,16,21] *} + +/// tip | İpucu + +Bu aslında standart Python'dır; buna "type alias" denir ve **FastAPI**'ye özel bir şey değildir. + +Ama **FastAPI**, `Annotated` dahil Python standartları üzerine kurulu olduğu için bu tekniği kodunuzda kullanabilirsiniz. 😎 + +/// + +Dependency'ler beklediğiniz gibi çalışmaya devam eder ve **en güzel kısmı** da şudur: **type bilgisi korunur**. Bu da editörünüzün size **autocompletion**, **inline errors** vb. sağlamaya devam edeceği anlamına gelir. `mypy` gibi diğer araçlar için de aynısı geçerlidir. + +Bu özellikle, **büyük bir kod tabanında**, aynı dependency'leri **birçok *path operation*** içinde tekrar tekrar kullandığınızda çok faydalı olacaktır. + +## `async` Olsa da Olmasa da { #to-async-or-not-to-async } + +Dependency'ler de **FastAPI** tarafından çağrılacağı için (tıpkı *path operation function*'larınız gibi), fonksiyonları tanımlarken aynı kurallar geçerlidir. + +`async def` ya da normal `def` kullanabilirsiniz. + +Ayrıca normal `def` *path operation function*'ları içinde `async def` dependency tanımlayabilir veya `async def` *path operation function*'ları içinde `def` dependency kullanabilirsiniz vb. + +Fark etmez. **FastAPI** ne yapacağını bilir. + +/// note | Not + +Eğer bilmiyorsanız, dokümanlarda `async` ve `await` için [Async: *"In a hurry?"*](../../async.md#in-a-hurry){.internal-link target=_blank} bölümüne bakın. + +/// + +## OpenAPI ile Entegre { #integrated-with-openapi } + +Dependency'lerinizin (ve alt dependency'lerin) tüm request tanımları, doğrulamaları ve gereksinimleri aynı OpenAPI şemasına entegre edilir. + +Bu nedenle interaktif dokümanlar, bu dependency'lerden gelen tüm bilgileri de içerir: + + + +## Basit Kullanım { #simple-usage } + +Şöyle düşünürseniz: *Path operation function*'lar, bir *path* ve *operation* eşleştiğinde kullanılacak şekilde tanımlanır; ardından **FastAPI** fonksiyonu doğru parametrelerle çağırır ve request'ten veriyi çıkarır. + +Aslında tüm (veya çoğu) web framework'ü de aynı şekilde çalışır. + +Bu fonksiyonları hiçbir zaman doğrudan çağırmazsınız. Onları framework'ünüz (bu örnekte **FastAPI**) çağırır. + +Dependency Injection sistemiyle, *path operation function*'ınızın, ondan önce çalıştırılması gereken başka bir şeye de "bağlı" olduğunu **FastAPI**'ye söyleyebilirsiniz; **FastAPI** bunu çalıştırır ve sonuçları "inject" eder. + +Aynı "dependency injection" fikri için kullanılan diğer yaygın terimler: + +* resources +* providers +* services +* injectables +* components + +## **FastAPI** Plug-in'leri { #fastapi-plug-ins } + +Entegrasyonlar ve "plug-in"ler **Dependency Injection** sistemi kullanılarak inşa edilebilir. Ancak aslında **"plug-in" oluşturmanıza gerek yoktur**; çünkü dependency'leri kullanarak *path operation function*'larınıza sunulabilecek sınırsız sayıda entegrasyon ve etkileşim tanımlayabilirsiniz. + +Dependency'ler, çok basit ve sezgisel bir şekilde oluşturulabilir. Böylece ihtiyacınız olan Python package'larını import edip, API fonksiyonlarınızla birkaç satır kodla *kelimenin tam anlamıyla* entegre edebilirsiniz. + +İlerleyen bölümlerde ilişkisel ve NoSQL veritabanları, güvenlik vb. konularda bunun örneklerini göreceksiniz. + +## **FastAPI** Uyumluluğu { #fastapi-compatibility } + +Dependency injection sisteminin sadeliği, **FastAPI**'yi şunlarla uyumlu hale getirir: + +* tüm ilişkisel veritabanları +* NoSQL veritabanları +* harici paketler +* harici API'ler +* authentication ve authorization sistemleri +* API kullanım izleme (monitoring) sistemleri +* response verisi injection sistemleri +* vb. + +## Basit ve Güçlü { #simple-and-powerful } + +Hiyerarşik dependency injection sistemi tanımlamak ve kullanmak çok basit olsa da, hâlâ oldukça güçlüdür. + +Kendileri de dependency tanımlayabilen dependency'ler tanımlayabilirsiniz. + +Sonuçta hiyerarşik bir dependency ağacı oluşur ve **Dependency Injection** sistemi tüm bu dependency'leri (ve alt dependency'lerini) sizin için çözer ve her adımda sonuçları sağlar ("inject" eder). + +Örneğin, 4 API endpoint'iniz (*path operation*) olduğunu varsayalım: + +* `/items/public/` +* `/items/private/` +* `/users/{user_id}/activate` +* `/items/pro/` + +O zaman her biri için farklı izin gereksinimlerini yalnızca dependency'ler ve alt dependency'lerle ekleyebilirsiniz: + +```mermaid +graph TB + +current_user(["current_user"]) +active_user(["active_user"]) +admin_user(["admin_user"]) +paying_user(["paying_user"]) + +public["/items/public/"] +private["/items/private/"] +activate_user["/users/{user_id}/activate"] +pro_items["/items/pro/"] + +current_user --> active_user +active_user --> admin_user +active_user --> paying_user + +current_user --> public +active_user --> private +admin_user --> activate_user +paying_user --> pro_items +``` + +## **OpenAPI** ile Entegre { #integrated-with-openapi_1 } + +Bu dependency'lerin tümü, gereksinimlerini beyan ederken aynı zamanda *path operation*'larınıza parametreler, doğrulamalar vb. da ekler. + +**FastAPI**, bunların hepsini OpenAPI şemasına eklemekle ilgilenir; böylece interaktif dokümantasyon sistemlerinde gösterilir. diff --git a/docs/tr/docs/tutorial/dependencies/sub-dependencies.md b/docs/tr/docs/tutorial/dependencies/sub-dependencies.md new file mode 100644 index 000000000..184db839b --- /dev/null +++ b/docs/tr/docs/tutorial/dependencies/sub-dependencies.md @@ -0,0 +1,105 @@ +# Alt Bağımlılıklar { #sub-dependencies } + +**Alt bağımlılıkları** olan bağımlılıklar oluşturabilirsiniz. + +İhtiyacınız olduğu kadar **derine** gidebilirler. + +Bunları çözme işini **FastAPI** üstlenir. + +## İlk bağımlılık "dependable" { #first-dependency-dependable } + +Şöyle bir ilk bağımlılık ("dependable") oluşturabilirsiniz: + +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[8:9] *} + +Burada `q` adında opsiyonel bir query parametresi `str` olarak tanımlanır ve sonra doğrudan geri döndürülür. + +Bu oldukça basit (pek faydalı değil), ama alt bağımlılıkların nasıl çalıştığına odaklanmamıza yardımcı olacak. + +## İkinci bağımlılık: "dependable" ve "dependant" { #second-dependency-dependable-and-dependant } + +Ardından, aynı zamanda kendi içinde bir bağımlılık da tanımlayan başka bir bağımlılık fonksiyonu (bir "dependable") oluşturabilirsiniz (yani o da bir "dependant" olur): + +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[13] *} + +Tanımlanan parametrelere odaklanalım: + +* Bu fonksiyonun kendisi bir bağımlılık ("dependable") olmasına rağmen, ayrıca başka bir bağımlılık daha tanımlar (başka bir şeye "depends" olur). + * `query_extractor`'a bağlıdır ve ondan dönen değeri `q` parametresine atar. +* Ayrıca `last_query` adında opsiyonel bir cookie'yi `str` olarak tanımlar. + * Kullanıcı herhangi bir query `q` sağlamadıysa, daha önce cookie'ye kaydettiğimiz en son kullanılan query'yi kullanırız. + +## Bağımlılığı Kullanma { #use-the-dependency } + +Sonra bu bağımlılığı şöyle kullanabiliriz: + +{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[23] *} + +/// info | Bilgi + +Dikkat edin, *path operation function* içinde yalnızca tek bir bağımlılık tanımlıyoruz: `query_or_cookie_extractor`. + +Ancak **FastAPI**, `query_or_cookie_extractor`'ı çağırmadan önce `query_extractor`'ı önce çözmesi gerektiğini bilir ve onun sonucunu `query_or_cookie_extractor`'a aktarır. + +/// + +```mermaid +graph TB + +query_extractor(["query_extractor"]) +query_or_cookie_extractor(["query_or_cookie_extractor"]) + +read_query["/items/"] + +query_extractor --> query_or_cookie_extractor --> read_query +``` + +## Aynı Bağımlılığı Birden Fazla Kez Kullanma { #using-the-same-dependency-multiple-times } + +Bağımlılıklarınızdan biri aynı *path operation* için birden fazla kez tanımlanırsa (örneğin birden fazla bağımlılığın ortak bir alt bağımlılığı varsa), **FastAPI** o alt bağımlılığı request başına yalnızca bir kez çağıracağını bilir. + +Dönen değeri bir "cache" içinde saklar ve aynı request içinde buna ihtiyaç duyan tüm "dependant"lara aktarır; böylece aynı request için bağımlılığı tekrar tekrar çağırmaz. + +Daha ileri bir senaryoda, "cached" değeri kullanmak yerine aynı request içinde her adımda (muhtemelen birden fazla kez) bağımlılığın çağrılması gerektiğini biliyorsanız, `Depends` kullanırken `use_cache=False` parametresini ayarlayabilirsiniz: + +//// tab | Python 3.9+ + +```Python hl_lines="1" +async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]): + return {"fresh_value": fresh_value} +``` + +//// + +//// tab | Python 3.9+ Annotated olmayan + +/// tip | İpucu + +Mümkünse `Annotated` sürümünü tercih edin. + +/// + +```Python hl_lines="1" +async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): + return {"fresh_value": fresh_value} +``` + +//// + +## Özet { #recap } + +Burada kullanılan havalı terimlerin ötesinde, **Dependency Injection** sistemi aslında oldukça basittir. + +*Path operation function*'lara benzeyen fonksiyonlardan ibarettir. + +Yine de çok güçlüdür ve keyfi derecede derin iç içe geçmiş bağımlılık "graph"ları (ağaçları) tanımlamanıza izin verir. + +/// tip | İpucu + +Bu basit örneklerle çok faydalı görünmeyebilir. + +Ancak **security** ile ilgili bölümlerde bunun ne kadar işe yaradığını göreceksiniz. + +Ayrıca size ne kadar kod kazandırdığını da göreceksiniz. + +/// diff --git a/docs/tr/docs/tutorial/encoder.md b/docs/tr/docs/tutorial/encoder.md new file mode 100644 index 000000000..e4790a032 --- /dev/null +++ b/docs/tr/docs/tutorial/encoder.md @@ -0,0 +1,35 @@ +# JSON Uyumlu Encoder { #json-compatible-encoder } + +Bazı durumlarda, bir veri tipini (örneğin bir Pydantic model) JSON ile uyumlu bir şeye (örneğin `dict`, `list` vb.) dönüştürmeniz gerekebilir. + +Örneğin, bunu bir veritabanında saklamanız gerekiyorsa. + +Bunun için **FastAPI**, `jsonable_encoder()` fonksiyonunu sağlar. + +## `jsonable_encoder` Kullanımı { #using-the-jsonable-encoder } + +Yalnızca JSON ile uyumlu veri kabul eden bir veritabanınız olduğunu düşünelim: `fake_db`. + +Örneğin bu veritabanı, JSON ile uyumlu olmadıkları için `datetime` objelerini kabul etmez. + +Dolayısıyla bir `datetime` objesinin, ISO formatında veriyi içeren bir `str`'e dönüştürülmesi gerekir. + +Aynı şekilde bu veritabanı bir Pydantic model'i (attribute'lara sahip bir obje) de kabul etmez; yalnızca bir `dict` kabul eder. + +Bunun için `jsonable_encoder` kullanabilirsiniz. + +Bir Pydantic model gibi bir obje alır ve JSON ile uyumlu bir versiyonunu döndürür: + +{* ../../docs_src/encoder/tutorial001_py310.py hl[4,21] *} + +Bu örnekte, Pydantic model'i bir `dict`'e, `datetime`'ı da bir `str`'e dönüştürür. + +Bu fonksiyonun çağrılmasıyla elde edilen sonuç, Python standardındaki `json.dumps()` ile encode edilebilecek bir şeydir. + +JSON formatında (string olarak) veriyi içeren büyük bir `str` döndürmez. Bunun yerine, tüm değerleri ve alt değerleri JSON ile uyumlu olacak şekilde, Python’un standart bir veri yapısını (örneğin bir `dict`) döndürür. + +/// note | Not + +`jsonable_encoder`, aslında **FastAPI** tarafından veriyi dönüştürmek için internal olarak kullanılır. Ancak birçok farklı senaryoda da oldukça faydalıdır. + +/// diff --git a/docs/tr/docs/tutorial/extra-data-types.md b/docs/tr/docs/tutorial/extra-data-types.md new file mode 100644 index 000000000..464d3a82a --- /dev/null +++ b/docs/tr/docs/tutorial/extra-data-types.md @@ -0,0 +1,62 @@ +# Ek Veri Tipleri { #extra-data-types } + +Şimdiye kadar şunlar gibi yaygın veri tiplerini kullanıyordunuz: + +* `int` +* `float` +* `str` +* `bool` + +Ancak daha karmaşık veri tiplerini de kullanabilirsiniz. + +Ve yine, şimdiye kadar gördüğünüz özelliklerin aynısına sahip olursunuz: + +* Harika editör desteği. +* Gelen request'lerden veri dönüştürme. +* response verileri için veri dönüştürme. +* Veri doğrulama. +* Otomatik annotation ve dokümantasyon. + +## Diğer veri tipleri { #other-data-types } + +Kullanabileceğiniz ek veri tiplerinden bazıları şunlardır: + +* `UUID`: + * Birçok veritabanı ve sistemde ID olarak yaygın kullanılan, standart bir "Universally Unique Identifier". + * request ve response'larda `str` olarak temsil edilir. +* `datetime.datetime`: + * Python `datetime.datetime`. + * request ve response'larda ISO 8601 formatında bir `str` olarak temsil edilir, örn: `2008-09-15T15:53:00+05:00`. +* `datetime.date`: + * Python `datetime.date`. + * request ve response'larda ISO 8601 formatında bir `str` olarak temsil edilir, örn: `2008-09-15`. +* `datetime.time`: + * Python `datetime.time`. + * request ve response'larda ISO 8601 formatında bir `str` olarak temsil edilir, örn: `14:23:55.003`. +* `datetime.timedelta`: + * Python `datetime.timedelta`. + * request ve response'larda toplam saniye sayısını ifade eden bir `float` olarak temsil edilir. + * Pydantic, bunu ayrıca bir "ISO 8601 time diff encoding" olarak temsil etmeye de izin verir, daha fazla bilgi için dokümanlara bakın. +* `frozenset`: + * request ve response'larda, `set` ile aynı şekilde ele alınır: + * request'lerde bir list okunur, tekrarlar kaldırılır ve `set`'e dönüştürülür. + * response'larda `set`, `list`'e dönüştürülür. + * Üretilen schema, `set` değerlerinin benzersiz olduğunu belirtir (JSON Schema'nın `uniqueItems` özelliğini kullanarak). +* `bytes`: + * Standart Python `bytes`. + * request ve response'larda `str` gibi ele alınır. + * Üretilen schema, bunun `binary` "format"ına sahip bir `str` olduğunu belirtir. +* `Decimal`: + * Standart Python `Decimal`. + * request ve response'larda `float` ile aynı şekilde işlenir. +* Geçerli tüm Pydantic veri tiplerini burada görebilirsiniz: Pydantic data types. + +## Örnek { #example } + +Yukarıdaki tiplerden bazılarını kullanan parametrelere sahip bir örnek *path operation* şöyle: + +{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[1,3,12:16] *} + +Fonksiyonun içindeki parametrelerin doğal veri tiplerinde olduğunu unutmayın; örneğin normal tarih işlemleri yapabilirsiniz: + +{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[18:19] *} diff --git a/docs/tr/docs/tutorial/extra-models.md b/docs/tr/docs/tutorial/extra-models.md new file mode 100644 index 000000000..9aae28e05 --- /dev/null +++ b/docs/tr/docs/tutorial/extra-models.md @@ -0,0 +1,211 @@ +# Ek Modeller { #extra-models } + +Önceki örnekten devam edersek, birbiriyle ilişkili birden fazla modelin olması oldukça yaygındır. + +Bu durum özellikle kullanıcı modellerinde sık görülür, çünkü: + +* **input modeli** bir `password` içerebilmelidir. +* **output modeli** `password` içermemelidir. +* **database modeli** büyük ihtimalle hash'lenmiş bir `password` tutmalıdır. + +/// danger + +Kullanıcının düz metin (plaintext) `password`'ünü asla saklamayın. Her zaman sonradan doğrulayabileceğiniz "güvenli bir hash" saklayın. + +Eğer bilmiyorsanız, "password hash" nedir konusunu [güvenlik bölümlerinde](security/simple-oauth2.md#password-hashing){.internal-link target=_blank} öğreneceksiniz. + +/// + +## Birden Çok Model { #multiple-models } + +`password` alanlarıyla birlikte modellerin genel olarak nasıl görünebileceğine ve nerelerde kullanılacaklarına dair bir fikir: + +{* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} + +### `**user_in.model_dump()` Hakkında { #about-user-in-model-dump } + +#### Pydantic'in `.model_dump()` Metodu { #pydantics-model-dump } + +`user_in`, `UserIn` sınıfına ait bir Pydantic modelidir. + +Pydantic modellerinde, model verilerini içeren bir `dict` döndüren `.model_dump()` metodu bulunur. + +Yani, şöyle bir Pydantic nesnesi `user_in` oluşturursak: + +```Python +user_in = UserIn(username="john", password="secret", email="john.doe@example.com") +``` + +ve sonra şunu çağırırsak: + +```Python +user_dict = user_in.model_dump() +``` + +artık `user_dict` değişkeninde modelin verilerini içeren bir `dict` vardır (Pydantic model nesnesi yerine bir `dict` elde etmiş oluruz). + +Ve eğer şunu çağırırsak: + +```Python +print(user_dict) +``` + +şöyle bir Python `dict` elde ederiz: + +```Python +{ + 'username': 'john', + 'password': 'secret', + 'email': 'john.doe@example.com', + 'full_name': None, +} +``` + +#### Bir `dict`'i Unpack Etmek { #unpacking-a-dict } + +`user_dict` gibi bir `dict` alıp bunu bir fonksiyona (ya da sınıfa) `**user_dict` ile gönderirsek, Python bunu "unpack" eder. Yani `user_dict` içindeki key ve value'ları doğrudan key-value argümanları olarak geçirir. + +Dolayısıyla, yukarıdaki `user_dict` ile devam edersek, şunu yazmak: + +```Python +UserInDB(**user_dict) +``` + +şuna eşdeğer bir sonuç üretir: + +```Python +UserInDB( + username="john", + password="secret", + email="john.doe@example.com", + full_name=None, +) +``` + +Ya da daha net şekilde, `user_dict`'i doğrudan kullanarak, gelecekte içeriği ne olursa olsun: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], +) +``` + +#### Bir Pydantic Modelinden Diğerinin İçeriğiyle Pydantic Model Oluşturmak { #a-pydantic-model-from-the-contents-of-another } + +Yukarıdaki örnekte `user_dict`'i `user_in.model_dump()` ile elde ettiğimiz için, şu kod: + +```Python +user_dict = user_in.model_dump() +UserInDB(**user_dict) +``` + +şuna eşdeğerdir: + +```Python +UserInDB(**user_in.model_dump()) +``` + +...çünkü `user_in.model_dump()` bir `dict` döndürür ve biz de bunu `UserInDB`'ye `**` önekiyle vererek Python'ın "unpack" etmesini sağlarız. + +Böylece, bir Pydantic modelindeki verilerden başka bir Pydantic model üretmiş oluruz. + +#### Bir `dict`'i Unpack Etmek ve Ek Keyword'ler { #unpacking-a-dict-and-extra-keywords } + +Sonrasında, aşağıdaki gibi ek keyword argümanı `hashed_password=hashed_password` eklemek: + +```Python +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) +``` + +...şuna benzer bir sonuca dönüşür: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], + hashed_password = hashed_password, +) +``` + +/// warning + +Ek destek fonksiyonları olan `fake_password_hasher` ve `fake_save_user` sadece verinin olası bir akışını göstermek içindir; elbette gerçek bir güvenlik sağlamazlar. + +/// + +## Tekrarı Azaltma { #reduce-duplication } + +Kod tekrarını azaltmak, **FastAPI**'nin temel fikirlerinden biridir. + +Kod tekrarı; bug, güvenlik problemi, kodun senkron dışına çıkması (bir yeri güncelleyip diğerlerini güncellememek) gibi sorunların olasılığını artırır. + +Bu modellerin hepsi verinin büyük bir kısmını paylaşıyor ve attribute adlarını ve type'larını tekrar ediyor. + +Daha iyisini yapabiliriz. + +Diğer modellerimiz için temel olacak bir `UserBase` modeli tanımlayabiliriz. Sonra da bu modelden türeyen (subclass) modeller oluşturup onun attribute'larını (type deklarasyonları, doğrulama vb.) miras aldırabiliriz. + +Tüm veri dönüştürme, doğrulama, dokümantasyon vb. her zamanki gibi çalışmaya devam eder. + +Bu sayede modeller arasındaki farkları (plaintext `password` olan, `hashed_password` olan ve `password` olmayan) sadece o farklılıklar olarak tanımlayabiliriz: + +{* ../../docs_src/extra_models/tutorial002_py310.py hl[7,13:14,17:18,21:22] *} + +## `Union` veya `anyOf` { #union-or-anyof } + +Bir response'u iki ya da daha fazla type'ın `Union`'ı olarak tanımlayabilirsiniz; bu, response'un bunlardan herhangi biri olabileceği anlamına gelir. + +OpenAPI'de bu `anyOf` ile tanımlanır. + +Bunu yapmak için standart Python type hint'i olan `typing.Union`'ı kullanın: + +/// note + +Bir `Union` tanımlarken en spesifik type'ı önce, daha az spesifik olanı sonra ekleyin. Aşağıdaki örnekte daha spesifik olan `PlaneItem`, `Union[PlaneItem, CarItem]` içinde `CarItem`'dan önce gelir. + +/// + +{* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *} + +### Python 3.10'da `Union` { #union-in-python-3-10 } + +Bu örnekte `Union[PlaneItem, CarItem]` değerini `response_model` argümanına veriyoruz. + +Bunu bir **type annotation** içine koymak yerine bir **argümana değer** olarak geçtiğimiz için, Python 3.10'da bile `Union` kullanmamız gerekiyor. + +Eğer bu bir type annotation içinde olsaydı, dikey çizgiyi kullanabilirdik: + +```Python +some_variable: PlaneItem | CarItem +``` + +Ancak bunu `response_model=PlaneItem | CarItem` atamasına koyarsak hata alırız; çünkü Python bunu bir type annotation olarak yorumlamak yerine `PlaneItem` ile `CarItem` arasında **geçersiz bir işlem** yapmaya çalışır. + +## Model Listesi { #list-of-models } + +Aynı şekilde, nesne listesi döndüren response'ları da tanımlayabilirsiniz. + +Bunun için standart Python `typing.List`'i (ya da Python 3.9 ve üzeri için sadece `list`) kullanın: + +{* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *} + +## Rastgele `dict` ile Response { #response-with-arbitrary-dict } + +Bir Pydantic modeli kullanmadan, sadece key ve value type'larını belirterek düz, rastgele bir `dict` ile de response tanımlayabilirsiniz. + +Bu, geçerli field/attribute adlarını (Pydantic modeli için gerekli olurdu) önceden bilmiyorsanız kullanışlıdır. + +Bu durumda `typing.Dict` (ya da Python 3.9 ve üzeri için sadece `dict`) kullanabilirsiniz: + +{* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *} + +## Özet { #recap } + +Her duruma göre birden fazla Pydantic modeli kullanın ve gerekirse özgürce inheritance uygulayın. + +Bir entity'nin farklı "state"lere sahip olması gerekiyorsa, o entity için tek bir veri modeli kullanmak zorunda değilsiniz. Örneğin `password` içeren, `password_hash` içeren ve `password` içermeyen state'lere sahip kullanıcı "entity"si gibi. diff --git a/docs/tr/docs/tutorial/handling-errors.md b/docs/tr/docs/tutorial/handling-errors.md new file mode 100644 index 000000000..a4d693792 --- /dev/null +++ b/docs/tr/docs/tutorial/handling-errors.md @@ -0,0 +1,244 @@ +# Hataları Yönetme { #handling-errors } + +API’nizi kullanan bir client’a hata bildirmek zorunda olduğunuz pek çok durum vardır. + +Bu client; frontend’i olan bir tarayıcı, başka birinin yazdığı bir kod, bir IoT cihazı vb. olabilir. + +Client’a şunları söylemeniz gerekebilir: + +* Client’ın bu işlem için yeterli yetkisi yok. +* Client’ın bu kaynağa erişimi yok. +* Client’ın erişmeye çalıştığı öğe mevcut değil. +* vb. + +Bu durumlarda genellikle **400** aralığında (**400** ile **499** arası) bir **HTTP status code** döndürürsünüz. + +Bu, 200 HTTP status code’larına (200 ile 299 arası) benzer. Bu "200" status code’ları, request’in bir şekilde "başarılı" olduğunu ifade eder. + +400 aralığındaki status code’lar ise hatanın client tarafından kaynaklandığını gösterir. + +Şu meşhur **"404 Not Found"** hatalarını (ve şakalarını) hatırlıyor musunuz? + +## `HTTPException` Kullanma { #use-httpexception } + +Client’a hata içeren HTTP response’ları döndürmek için `HTTPException` kullanırsınız. + +### `HTTPException`’ı Import Etme { #import-httpexception } + +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[1] *} + +### Kodunuzda Bir `HTTPException` Raise Etme { #raise-an-httpexception-in-your-code } + +`HTTPException`, API’lerle ilgili ek veriler içeren normal bir Python exception’ıdır. + +Python exception’ı olduğu için `return` etmezsiniz, `raise` edersiniz. + +Bu aynı zamanda şunu da ifade eder: *path operation function*’ınızın içinde çağırdığınız bir yardımcı (utility) fonksiyonun içindeyken `HTTPException` raise ederseniz, *path operation function* içindeki kodun geri kalanı çalışmaz; request’i hemen sonlandırır ve `HTTPException` içindeki HTTP hatasını client’a gönderir. + +Bir değer döndürmek yerine exception raise etmenin faydası, Dependencies ve Security bölümünde daha da netleşecektir. + +Bu örnekte, client var olmayan bir ID ile bir item istediğinde, `404` status code’u ile bir exception raise edelim: + +{* ../../docs_src/handling_errors/tutorial001_py39.py hl[11] *} + +### Ortaya Çıkan Response { #the-resulting-response } + +Client `http://example.com/items/foo` (bir `item_id` `"foo"`) isterse, HTTP status code olarak 200 ve şu JSON response’u alır: + +```JSON +{ + "item": "The Foo Wrestlers" +} +``` + +Ancak client `http://example.com/items/bar` (mevcut olmayan bir `item_id` `"bar"`) isterse, HTTP status code olarak 404 ("not found" hatası) ve şu JSON response’u alır: + +```JSON +{ + "detail": "Item not found" +} +``` + +/// tip | İpucu + +Bir `HTTPException` raise ederken, `detail` parametresine sadece `str` değil, JSON’a dönüştürülebilen herhangi bir değer geçebilirsiniz. + +Örneğin `dict`, `list` vb. geçebilirsiniz. + +Bunlar **FastAPI** tarafından otomatik olarak işlenir ve JSON’a dönüştürülür. + +/// + +## Özel Header’lar Eklemek { #add-custom-headers } + +Bazı durumlarda HTTP hata response’una özel header’lar eklemek faydalıdır. Örneğin bazı güvenlik türlerinde. + +Muhtemelen bunu doğrudan kendi kodunuzda kullanmanız gerekmeyecek. + +Ama ileri seviye bir senaryo için ihtiyaç duyarsanız, özel header’lar ekleyebilirsiniz: + +{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *} + +## Özel Exception Handler’ları Kurmak { #install-custom-exception-handlers } + +Starlette’in aynı exception yardımcı araçlarıyla özel exception handler’lar ekleyebilirsiniz. + +Diyelim ki sizin (ya da kullandığınız bir kütüphanenin) `raise` edebileceği `UnicornException` adında özel bir exception’ınız var. + +Ve bu exception’ı FastAPI ile global olarak handle etmek istiyorsunuz. + +`@app.exception_handler()` ile özel bir exception handler ekleyebilirsiniz: + +{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *} + +Burada `/unicorns/yolo` için request atarsanız, *path operation* bir `UnicornException` `raise` eder. + +Ancak bu, `unicorn_exception_handler` tarafından handle edilir. + +Böylece HTTP status code’u `418` olan, JSON içeriği şu şekilde temiz bir hata response’u alırsınız: + +```JSON +{"message": "Oops! yolo did something. There goes a rainbow..."} +``` + +/// note | Teknik Detaylar + +`from starlette.requests import Request` ve `from starlette.responses import JSONResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.responses` içeriğini `fastapi.responses` olarak da sunar. Ancak mevcut response’ların çoğu doğrudan Starlette’ten gelir. `Request` için de aynısı geçerlidir. + +/// + +## Varsayılan Exception Handler’ları Override Etmek { #override-the-default-exception-handlers } + +**FastAPI** bazı varsayılan exception handler’lara sahiptir. + +Bu handler’lar, `HTTPException` `raise` ettiğinizde ve request geçersiz veri içerdiğinde varsayılan JSON response’ları döndürmekten sorumludur. + +Bu exception handler’ları kendi handler’larınızla override edebilirsiniz. + +### Request Validation Exception’larını Override Etmek { #override-request-validation-exceptions } + +Bir request geçersiz veri içerdiğinde, **FastAPI** içeride `RequestValidationError` raise eder. + +Ve bunun için varsayılan bir exception handler da içerir. + +Override etmek için `RequestValidationError`’ı import edin ve exception handler’ı `@app.exception_handler(RequestValidationError)` ile decorate edin. + +Exception handler, bir `Request` ve exception’ı alır. + +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *} + +Artık `/items/foo`’ya giderseniz, şu varsayılan JSON hatası yerine: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +şu şekilde bir metin (text) versiyonu alırsınız: + +``` +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer +``` + +### `HTTPException` Hata Handler’ını Override Etmek { #override-the-httpexception-error-handler } + +Benzer şekilde `HTTPException` handler’ını da override edebilirsiniz. + +Örneğin bu hatalar için JSON yerine plain text response döndürmek isteyebilirsiniz: + +{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *} + +/// note | Teknik Detaylar + +`from starlette.responses import PlainTextResponse` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.responses` içeriğini `fastapi.responses` olarak da sunar. Ancak mevcut response’ların çoğu doğrudan Starlette’ten gelir. + +/// + +/// warning | Uyarı + +`RequestValidationError`, validation hatasının gerçekleştiği dosya adı ve satır bilgilerini içerir; isterseniz bunu log’larınıza ilgili bilgilerle birlikte yazdırabilirsiniz. + +Ancak bu, eğer sadece string’e çevirip bu bilgiyi doğrudan response olarak döndürürseniz sisteminiz hakkında bir miktar bilgi sızdırabileceğiniz anlamına gelir. Bu yüzden burada kod, her bir hatayı ayrı ayrı çıkarıp gösterir. + +/// + +### `RequestValidationError` Body’sini Kullanmak { #use-the-requestvalidationerror-body } + +`RequestValidationError`, geçersiz veriyle aldığı `body`’yi içerir. + +Uygulamanızı geliştirirken body’yi log’lamak, debug etmek, kullanıcıya döndürmek vb. için bunu kullanabilirsiniz. + +{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *} + +Şimdi şu gibi geçersiz bir item göndermeyi deneyin: + +```JSON +{ + "title": "towel", + "size": "XL" +} +``` + +Aldığınız body’yi de içeren, verinin geçersiz olduğunu söyleyen bir response alırsınız: + +```JSON hl_lines="12-15" +{ + "detail": [ + { + "loc": [ + "body", + "size" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ], + "body": { + "title": "towel", + "size": "XL" + } +} +``` + +#### FastAPI’nin `HTTPException`’ı vs Starlette’in `HTTPException`’ı { #fastapis-httpexception-vs-starlettes-httpexception } + +**FastAPI**’nin kendi `HTTPException`’ı vardır. + +Ve **FastAPI**’nin `HTTPException` hata sınıfı, Starlette’in `HTTPException` hata sınıfından kalıtım alır (inherit). + +Tek fark şudur: **FastAPI**’nin `HTTPException`’ı `detail` alanı için JSON’a çevrilebilir herhangi bir veri kabul ederken, Starlette’in `HTTPException`’ı burada sadece string kabul eder. + +Bu yüzden kodunuzda her zamanki gibi **FastAPI**’nin `HTTPException`’ını raise etmeye devam edebilirsiniz. + +Ancak bir exception handler register ederken, bunu Starlette’in `HTTPException`’ı için register etmelisiniz. + +Böylece Starlette’in internal kodunun herhangi bir bölümü ya da bir Starlette extension/plug-in’i Starlette `HTTPException` raise ederse, handler’ınız bunu yakalayıp (catch) handle edebilir. + +Bu örnekte, iki `HTTPException`’ı da aynı kodda kullanabilmek için Starlette’in exception’ı `StarletteHTTPException` olarak yeniden adlandırılıyor: + +```Python +from starlette.exceptions import HTTPException as StarletteHTTPException +``` + +### **FastAPI**’nin Exception Handler’larını Yeniden Kullanmak { #reuse-fastapis-exception-handlers } + +Exception’ı, **FastAPI**’nin aynı varsayılan exception handler’larıyla birlikte kullanmak isterseniz, varsayılan exception handler’ları `fastapi.exception_handlers` içinden import edip yeniden kullanabilirsiniz: + +{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *} + +Bu örnekte sadece oldukça açıklayıcı bir mesajla hatayı yazdırıyorsunuz; ama fikir anlaşılıyor. Exception’ı kullanıp ardından varsayılan exception handler’ları olduğu gibi yeniden kullanabilirsiniz. diff --git a/docs/tr/docs/tutorial/header-param-models.md b/docs/tr/docs/tutorial/header-param-models.md new file mode 100644 index 000000000..9170515dc --- /dev/null +++ b/docs/tr/docs/tutorial/header-param-models.md @@ -0,0 +1,72 @@ +# Header Parametre Modelleri { #header-parameter-models } + +Birbiriyle ilişkili **header parametreleri**nden oluşan bir grubunuz varsa, bunları tanımlamak için bir **Pydantic model** oluşturabilirsiniz. + +Bu sayede modeli **birden fazla yerde yeniden kullanabilir**, ayrıca tüm parametreler için doğrulamaları ve metadata bilgilerini tek seferde tanımlayabilirsiniz. 😎 + +/// note | Not + +Bu özellik FastAPI `0.115.0` sürümünden beri desteklenmektedir. 🤓 + +/// + +## Pydantic Model ile Header Parametreleri { #header-parameters-with-a-pydantic-model } + +İhtiyacınız olan **header parametreleri**ni bir **Pydantic model** içinde tanımlayın, ardından parametreyi `Header` olarak belirtin: + +{* ../../docs_src/header_param_models/tutorial001_an_py310.py hl[9:14,18] *} + +**FastAPI**, request içindeki **headers** bölümünden **her alan** için veriyi **çıkarır** ve size tanımladığınız Pydantic model örneğini verir. + +## Dokümanları Kontrol Edin { #check-the-docs } + +Gerekli header'ları `/docs` altındaki doküman arayüzünde görebilirsiniz: + +
+ +
+ +## Ek Header'ları Yasaklayın { #forbid-extra-headers } + +Bazı özel kullanım senaryolarında (muhtemelen çok yaygın değil), kabul etmek istediğiniz header'ları **kısıtlamak** isteyebilirsiniz. + +Pydantic'in model yapılandırmasını kullanarak `extra` alanları `forbid` edebilirsiniz: + +{* ../../docs_src/header_param_models/tutorial002_an_py310.py hl[10] *} + +Bir client bazı **ek header'lar** göndermeye çalışırsa, **hata** response'u alır. + +Örneğin client, değeri `plumbus` olan bir `tool` header'ı göndermeye çalışırsa, `tool` header parametresine izin verilmediğini söyleyen bir **hata** response'u alır: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["header", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } + ] +} +``` + +## Alt Çizgileri Dönüştürmeyi Kapatın { #disable-convert-underscores } + +Normal header parametrelerinde olduğu gibi, parametre adlarında alt çizgi karakterleri olduğunda bunlar **otomatik olarak tireye dönüştürülür**. + +Örneğin kodda `save_data` adlı bir header parametreniz varsa, beklenen HTTP header `save-data` olur ve dokümanlarda da bu şekilde görünür. + +Herhangi bir sebeple bu otomatik dönüşümü kapatmanız gerekiyorsa, header parametreleri için kullandığınız Pydantic model'lerde de bunu devre dışı bırakabilirsiniz. + +{* ../../docs_src/header_param_models/tutorial003_an_py310.py hl[19] *} + +/// warning | Uyarı + +`convert_underscores` değerini `False` yapmadan önce, bazı HTTP proxy'lerinin ve server'ların alt çizgi içeren header'ların kullanımına izin vermediğini unutmayın. + +/// + +## Özet { #summary } + +**FastAPI**'de **headers** tanımlamak için **Pydantic model** kullanabilirsiniz. 😎 diff --git a/docs/tr/docs/tutorial/header-params.md b/docs/tr/docs/tutorial/header-params.md new file mode 100644 index 000000000..ca787f534 --- /dev/null +++ b/docs/tr/docs/tutorial/header-params.md @@ -0,0 +1,91 @@ +# Header Parametreleri { #header-parameters } + +`Query`, `Path` ve `Cookie` parametrelerini nasıl tanımlıyorsanız, Header parametrelerini de aynı şekilde tanımlayabilirsiniz. + +## `Header`'ı Import Edin { #import-header } + +Önce `Header`'ı import edin: + +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *} + +## `Header` Parametrelerini Tanımlayın { #declare-header-parameters } + +Ardından header parametrelerini, `Path`, `Query` ve `Cookie` ile kullandığınız yapının aynısıyla tanımlayın. + +Default değeri ve ek validation ya da annotation parametrelerinin tamamını belirleyebilirsiniz: + +{* ../../docs_src/header_params/tutorial001_an_py310.py hl[9] *} + +/// note | Teknik Detaylar + +`Header`, `Path`, `Query` ve `Cookie`'nin "kardeş" sınıfıdır. Ayrıca aynı ortak `Param` sınıfından kalıtım alır. + +Ancak şunu unutmayın: `fastapi`'den `Query`, `Path`, `Header` ve diğerlerini import ettiğinizde, bunlar aslında özel sınıfları döndüren fonksiyonlardır. + +/// + +/// info | Bilgi + +Header'ları tanımlamak için `Header` kullanmanız gerekir; aksi halde parametreler query parametreleri olarak yorumlanır. + +/// + +## Otomatik Dönüştürme { #automatic-conversion } + +`Header`, `Path`, `Query` ve `Cookie`'nin sağladıklarına ek olarak küçük bir ekstra işlevsellik sunar. + +Standart header'ların çoğu, "hyphen" karakteri (diğer adıyla "minus symbol" (`-`)) ile ayrılır. + +Ancak `user-agent` gibi bir değişken adı Python'da geçersizdir. + +Bu yüzden, default olarak `Header`, header'ları almak ve dokümante etmek için parametre adlarındaki underscore (`_`) karakterlerini hyphen (`-`) ile dönüştürür. + +Ayrıca HTTP header'ları büyük/küçük harfe duyarlı değildir; dolayısıyla onları standart Python stiliyle (diğer adıyla "snake_case") tanımlayabilirsiniz. + +Yani `User_Agent` gibi bir şey yazıp ilk harfleri büyütmeniz gerekmeden, Python kodunda normalde kullandığınız gibi `user_agent` kullanabilirsiniz. + +Herhangi bir nedenle underscore'ların hyphen'lara otomatik dönüştürülmesini kapatmanız gerekirse, `Header`'ın `convert_underscores` parametresini `False` yapın: + +{* ../../docs_src/header_params/tutorial002_an_py310.py hl[10] *} + +/// warning | Uyarı + +`convert_underscores`'u `False` yapmadan önce, bazı HTTP proxy'lerinin ve server'ların underscore içeren header'ların kullanımına izin vermediğini unutmayın. + +/// + +## Yinelenen Header'lar { #duplicate-headers } + +Yinelenen header'lar almak mümkündür. Yani aynı header'ın birden fazla değeri olabilir. + +Bu tür durumları, type tanımında bir list kullanarak belirtebilirsiniz. + +Yinelenen header'daki tüm değerleri Python `list` olarak alırsınız. + +Örneğin, birden fazla kez gelebilen `X-Token` header'ını tanımlamak için şöyle yazabilirsiniz: + +{* ../../docs_src/header_params/tutorial003_an_py310.py hl[9] *} + +Eğer bu *path operation* ile iki HTTP header göndererek iletişim kurarsanız: + +``` +X-Token: foo +X-Token: bar +``` + +response şöyle olur: + +```JSON +{ + "X-Token values": [ + "bar", + "foo" + ] +} +``` + +## Özet { #recap } + +Header'ları `Header` ile tanımlayın; `Query`, `Path` ve `Cookie` ile kullanılan ortak kalıbı burada da kullanın. + +Değişkenlerinizdeki underscore'lar konusunda endişelenmeyin, **FastAPI** bunları dönüştürmeyi halleder. diff --git a/docs/tr/docs/tutorial/index.md b/docs/tr/docs/tutorial/index.md new file mode 100644 index 000000000..f672c9e20 --- /dev/null +++ b/docs/tr/docs/tutorial/index.md @@ -0,0 +1,95 @@ +# Eğitim - Kullanıcı Rehberi { #tutorial-user-guide } + +Bu eğitim, **FastAPI**'yi özelliklerinin çoğuyla birlikte adım adım nasıl kullanacağınızı gösterir. + +Her bölüm bir öncekilerin üzerine kademeli olarak eklenir, ancak konular birbirinden ayrılacak şekilde yapılandırılmıştır; böylece API ihtiyaçlarınıza göre doğrudan belirli bir konuya gidip aradığınızı bulabilirsiniz. + +Ayrıca, ileride tekrar dönüp tam olarak ihtiyaç duyduğunuz şeyi görebileceğiniz bir referans olarak da tasarlanmıştır. + +## Kodu Çalıştırın { #run-the-code } + +Tüm code block'lar kopyalanıp doğrudan kullanılabilir (zaten test edilmiş Python dosyalarıdır). + +Örneklerden herhangi birini çalıştırmak için, kodu `main.py` adlı bir dosyaya kopyalayın ve şu komutla `fastapi dev`'i başlatın: + +
+ +```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. +``` + +
+ +Kodu yazmanız ya da kopyalayıp düzenlemeniz ve yerelinizde çalıştırmanız **şiddetle önerilir**. + +Editörünüzde kullanmak FastAPI'nin avantajlarını gerçekten gösterir: ne kadar az kod yazmanız gerektiğini, type check'leri, autocompletion'ı vb. görürsünüz. + +--- + +## FastAPI'yi Kurun { #install-fastapi } + +İlk adım FastAPI'yi kurmaktır. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan emin olun, etkinleştirin ve ardından **FastAPI'yi kurun**: + +
+ +```console +$ pip install "fastapi[standard]" + +---> 100% +``` + +
+ +/// note | Not + +`pip install "fastapi[standard]"` ile kurduğunuzda, bazı varsayılan opsiyonel standard bağımlılıklarla birlikte gelir. Bunlara `fastapi-cloud-cli` da dahildir; bu sayede FastAPI Cloud'a deploy edebilirsiniz. + +Bu opsiyonel bağımlılıkları istemiyorsanız bunun yerine `pip install fastapi` kurabilirsiniz. + +Standard bağımlılıkları kurmak istiyor ama `fastapi-cloud-cli` olmasın diyorsanız, `pip install "fastapi[standard-no-fastapi-cloud-cli]"` ile kurabilirsiniz. + +/// + +## İleri Düzey Kullanıcı Rehberi { #advanced-user-guide } + +Bu **Eğitim - Kullanıcı Rehberi**'ni bitirdikten sonra daha sonra okuyabileceğiniz bir **İleri Düzey Kullanıcı Rehberi** de var. + +**İleri Düzey Kullanıcı Rehberi** bunun üzerine inşa eder, aynı kavramları kullanır ve size bazı ek özellikler öğretir. + +Ancak önce **Eğitim - Kullanıcı Rehberi**'ni (şu anda okuduğunuz bölümü) okumalısınız. + +Yalnızca **Eğitim - Kullanıcı Rehberi** ile eksiksiz bir uygulama oluşturabilmeniz hedeflenmiştir; ardından ihtiyaçlarınıza göre, **İleri Düzey Kullanıcı Rehberi**'ndeki ek fikirlerden bazılarını kullanarak farklı şekillerde genişletebilirsiniz. diff --git a/docs/tr/docs/tutorial/metadata.md b/docs/tr/docs/tutorial/metadata.md new file mode 100644 index 000000000..dacd68cf5 --- /dev/null +++ b/docs/tr/docs/tutorial/metadata.md @@ -0,0 +1,120 @@ +# Metadata ve Doküman URL'leri { #metadata-and-docs-urls } + +**FastAPI** uygulamanızda çeşitli metadata yapılandırmalarını özelleştirebilirsiniz. + +## API için Metadata { #metadata-for-api } + +OpenAPI spesifikasyonunda ve otomatik API doküman arayüzlerinde kullanılan şu alanları ayarlayabilirsiniz: + +| Parametre | Tip | Açıklama | +|------------|------|-------------| +| `title` | `str` | API'nin başlığı. | +| `summary` | `str` | API'nin kısa özeti. OpenAPI 3.1.0, FastAPI 0.99.0 sürümünden itibaren mevcut. | +| `description` | `str` | API'nin kısa açıklaması. Markdown kullanabilir. | +| `version` | `string` | API'nin sürümü. Bu, OpenAPI'nin değil, kendi uygulamanızın sürümüdür. Örneğin `2.5.0`. | +| `terms_of_service` | `str` | API'nin Kullanım Koşulları (Terms of Service) için bir URL. Verilirse, URL formatında olmalıdır. | +| `contact` | `dict` | Yayınlanan API için iletişim bilgileri. Birden fazla alan içerebilir.
contact alanları
ParametreTipAçıklama
namestrİletişim kişisi/kuruluşunu tanımlayan ad.
urlstrİletişim bilgilerine işaret eden URL. URL formatında OLMALIDIR.
emailstrİletişim kişisi/kuruluşunun e-posta adresi. E-posta adresi formatında OLMALIDIR.
| +| `license_info` | `dict` | Yayınlanan API için lisans bilgileri. Birden fazla alan içerebilir.
license_info alanları
ParametreTipAçıklama
namestrZORUNLU (license_info ayarlanmışsa). API için kullanılan lisans adı.
identifierstrAPI için bir SPDX lisans ifadesi. identifier alanı, url alanıyla karşılıklı olarak dışlayıcıdır (ikisi aynı anda kullanılamaz). OpenAPI 3.1.0, FastAPI 0.99.0 sürümünden itibaren mevcut.
urlstrAPI için kullanılan lisansa ait URL. URL formatında OLMALIDIR.
| + +Şu şekilde ayarlayabilirsiniz: + +{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *} + +/// tip | İpucu + +`description` alanına Markdown yazabilirsiniz; çıktı tarafında render edilir. + +/// + +Bu yapılandırmayla otomatik API dokümanları şöyle görünür: + + + +## License identifier { #license-identifier } + +OpenAPI 3.1.0 ve FastAPI 0.99.0 sürümünden itibaren, `license_info` içinde `url` yerine bir `identifier` da ayarlayabilirsiniz. + +Örneğin: + +{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *} + +## Tag'ler için Metadata { #metadata-for-tags } + +`openapi_tags` parametresiyle, path operation'larınızı gruplamak için kullandığınız farklı tag'ler adına ek metadata da ekleyebilirsiniz. + +Bu parametre, her tag için bir sözlük (dictionary) içeren bir liste alır. + +Her sözlük şunları içerebilir: + +* `name` (**zorunlu**): *path operation*'larda ve `APIRouter`'larda `tags` parametresinde kullandığınız tag adıyla aynı olan bir `str`. +* `description`: tag için kısa bir açıklama içeren `str`. Markdown içerebilir ve doküman arayüzünde gösterilir. +* `externalDocs`: harici dokümanları tanımlayan bir `dict`: + * `description`: harici dokümanlar için kısa açıklama içeren `str`. + * `url` (**zorunlu**): harici dokümantasyonun URL'sini içeren `str`. + +### Tag'ler için metadata oluşturun { #create-metadata-for-tags } + +`users` ve `items` tag'lerini içeren bir örnekle deneyelim. + +Tag'leriniz için metadata oluşturup `openapi_tags` parametresine geçin: + +{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *} + +Açıklamaların içinde Markdown kullanabileceğinizi unutmayın; örneğin "login" kalın (**login**) ve "fancy" italik (_fancy_) olarak gösterilecektir. + +/// tip | İpucu + +Kullandığınız tüm tag'ler için metadata eklemek zorunda değilsiniz. + +/// + +### Tag'lerinizi kullanın { #use-your-tags } + +*path operation*'larınızı (ve `APIRouter`'ları) farklı tag'lere atamak için `tags` parametresini kullanın: + +{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *} + +/// info | Bilgi + +Tag'ler hakkında daha fazlası için: [Path Operation Configuration](path-operation-configuration.md#tags){.internal-link target=_blank}. + +/// + +### Dokümanları kontrol edin { #check-the-docs } + +Artık dokümanlara baktığınızda, eklediğiniz tüm metadata gösterilir: + + + +### Tag sırası { #order-of-tags } + +Her tag metadata sözlüğünün listedeki sırası, doküman arayüzünde gösterilecek sırayı da belirler. + +Örneğin alfabetik sıralamada `users`, `items`'tan sonra gelirdi; ancak listedeki ilk sözlük olarak `users` metadata'sını eklediğimiz için, dokümanlarda önce o görünür. + +## OpenAPI URL'si { #openapi-url } + +Varsayılan olarak OpenAPI şeması `/openapi.json` adresinden sunulur. + +Ancak bunu `openapi_url` parametresiyle yapılandırabilirsiniz. + +Örneğin `/api/v1/openapi.json` adresinden sunulacak şekilde ayarlamak için: + +{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *} + +OpenAPI şemasını tamamen kapatmak isterseniz `openapi_url=None` ayarlayabilirsiniz; bu, onu kullanan dokümantasyon arayüzlerini de devre dışı bırakır. + +## Doküman URL'leri { #docs-urls } + +Dahil gelen iki dokümantasyon arayüzünü yapılandırabilirsiniz: + +* **Swagger UI**: `/docs` adresinden sunulur. + * URL'sini `docs_url` parametresiyle ayarlayabilirsiniz. + * `docs_url=None` ayarlayarak devre dışı bırakabilirsiniz. +* **ReDoc**: `/redoc` adresinden sunulur. + * URL'sini `redoc_url` parametresiyle ayarlayabilirsiniz. + * `redoc_url=None` ayarlayarak devre dışı bırakabilirsiniz. + +Örneğin Swagger UI'yi `/documentation` adresinden sunup ReDoc'u kapatmak için: + +{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *} diff --git a/docs/tr/docs/tutorial/middleware.md b/docs/tr/docs/tutorial/middleware.md new file mode 100644 index 000000000..68222265e --- /dev/null +++ b/docs/tr/docs/tutorial/middleware.md @@ -0,0 +1,95 @@ +# Middleware { #middleware } + +**FastAPI** uygulamalarına middleware ekleyebilirsiniz. + +"Middleware", herhangi bir özel *path operation* tarafından işlenmeden önce her **request** ile çalışan bir fonksiyondur. Ayrıca geri döndürmeden önce her **response** ile de çalışır. + +* Uygulamanıza gelen her **request**'i alır. +* Ardından o **request** üzerinde bir işlem yapabilir veya gerekli herhangi bir kodu çalıştırabilir. +* Sonra **request**'i uygulamanın geri kalanı tarafından işlenmesi için iletir (bir *path operation* tarafından). +* Ardından uygulama tarafından üretilen **response**'u alır (bir *path operation* tarafından). +* Sonra o **response** üzerinde bir işlem yapabilir veya gerekli herhangi bir kodu çalıştırabilir. +* Son olarak **response**'u döndürür. + +/// note | Teknik Detaylar + +`yield` ile dependency'leriniz varsa, çıkış (exit) kodu middleware'den *sonra* çalışır. + +Herhangi bir background task varsa ([Background Tasks](background-tasks.md){.internal-link target=_blank} bölümünde ele alınıyor, ileride göreceksiniz), bunlar tüm middleware'ler *tamamlandıktan sonra* çalışır. + +/// + +## Middleware Oluşturma { #create-a-middleware } + +Bir middleware oluşturmak için bir fonksiyonun üzerine `@app.middleware("http")` decorator'ünü kullanırsınız. + +Middleware fonksiyonu şunları alır: + +* `request`. +* Parametre olarak `request` alacak bir `call_next` fonksiyonu. + * Bu fonksiyon `request`'i ilgili *path operation*'a iletir. + * Ardından ilgili *path operation* tarafından üretilen `response`'u döndürür. +* Sonrasında `response`'u döndürmeden önce ayrıca değiştirebilirsiniz. + +{* ../../docs_src/middleware/tutorial001_py39.py hl[8:9,11,14] *} + +/// tip | İpucu + +Özel (proprietary) header'lar `X-` prefix'i kullanılarak eklenebilir, bunu aklınızda tutun. + +Ancak tarayıcıdaki bir client'ın görebilmesini istediğiniz özel header'larınız varsa, bunları CORS konfigürasyonlarınıza ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) eklemeniz gerekir. Bunun için, Starlette'ın CORS dokümanlarında belgelenen `expose_headers` parametresini kullanın. + +/// + +/// note | Teknik Detaylar + +`from starlette.requests import Request` da kullanabilirdiniz. + +**FastAPI** bunu geliştirici olarak size kolaylık olsun diye sunar. Ancak doğrudan Starlette'tan gelir. + +/// + +### `response`'tan Önce ve Sonra { #before-and-after-the-response } + +Herhangi bir *path operation* `request`'i almadan önce, `request` ile birlikte çalışacak kod ekleyebilirsiniz. + +Ayrıca `response` üretildikten sonra, geri döndürmeden önce de kod çalıştırabilirsiniz. + +Örneğin, request'i işleyip response üretmenin kaç saniye sürdüğünü içeren `X-Process-Time` adlı özel bir header ekleyebilirsiniz: + +{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *} + +/// tip | İpucu + +Burada `time.time()` yerine `time.perf_counter()` kullanıyoruz, çünkü bu kullanım senaryolarında daha hassas olabilir. 🤓 + +/// + +## Birden Fazla Middleware Çalıştırma Sırası { #multiple-middleware-execution-order } + +`@app.middleware()` decorator'ü veya `app.add_middleware()` metodu ile birden fazla middleware eklediğinizde, eklenen her yeni middleware uygulamayı sarar ve bir stack oluşturur. En son eklenen middleware en *dıştaki* (outermost), ilk eklenen ise en *içteki* (innermost) olur. + +Request tarafında önce en *dıştaki* middleware çalışır. + +Response tarafında ise en son o çalışır. + +Örneğin: + +```Python +app.add_middleware(MiddlewareA) +app.add_middleware(MiddlewareB) +``` + +Bu, aşağıdaki çalıştırma sırasını oluşturur: + +* **Request**: MiddlewareB → MiddlewareA → route + +* **Response**: route → MiddlewareA → MiddlewareB + +Bu stack davranışı, middleware'lerin öngörülebilir ve kontrol edilebilir bir sırayla çalıştırılmasını sağlar. + +## Diğer Middleware'ler { #other-middlewares } + +Diğer middleware'ler hakkında daha fazlasını daha sonra [Advanced User Guide: Advanced Middleware](../advanced/middleware.md){.internal-link target=_blank} bölümünde okuyabilirsiniz. + +Bir sonraki bölümde, middleware ile CORS'un nasıl ele alınacağını göreceksiniz. diff --git a/docs/tr/docs/tutorial/path-operation-configuration.md b/docs/tr/docs/tutorial/path-operation-configuration.md new file mode 100644 index 000000000..610b863a4 --- /dev/null +++ b/docs/tr/docs/tutorial/path-operation-configuration.md @@ -0,0 +1,107 @@ +# Path Operation Yapılandırması { #path-operation-configuration } + +Onu yapılandırmak için *path operation decorator*’ınıza geçebileceğiniz çeşitli parametreler vardır. + +/// warning | Uyarı + +Bu parametrelerin *path operation function*’ınıza değil, doğrudan *path operation decorator*’ına verildiğine dikkat edin. + +/// + +## Response Status Code { #response-status-code } + +*Path operation*’ınızın response’unda kullanılacak (HTTP) `status_code`’u tanımlayabilirsiniz. + +`404` gibi `int` kodu doğrudan verebilirsiniz. + +Ancak her sayısal kodun ne işe yaradığını hatırlamıyorsanız, `status` içindeki kısayol sabitlerini kullanabilirsiniz: + +{* ../../docs_src/path_operation_configuration/tutorial001_py310.py hl[1,15] *} + +Bu status code response’da kullanılacak ve OpenAPI şemasına eklenecektir. + +/// note | Teknik Detaylar + +`from starlette import status` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak işinizi kolaylaştırmak için `starlette.status`’u `fastapi.status` olarak da sunar. Ancak kaynağı doğrudan Starlette’tir. + +/// + +## Tags { #tags } + +*Path operation*’ınıza tag ekleyebilirsiniz; `tags` parametresine `str` öğelerinden oluşan bir `list` verin (genellikle tek bir `str`): + +{* ../../docs_src/path_operation_configuration/tutorial002_py310.py hl[15,20,25] *} + +Bunlar OpenAPI şemasına eklenecek ve otomatik dokümantasyon arayüzleri tarafından kullanılacaktır: + + + +### Enum ile Tags { #tags-with-enums } + +Büyük bir uygulamanız varsa, zamanla **birden fazla tag** birikebilir ve ilişkili *path operation*’lar için her zaman **aynı tag**’i kullandığınızdan emin olmak isteyebilirsiniz. + +Bu durumlarda tag’leri bir `Enum` içinde tutmak mantıklı olabilir. + +**FastAPI** bunu düz string’lerde olduğu gibi aynı şekilde destekler: + +{* ../../docs_src/path_operation_configuration/tutorial002b_py39.py hl[1,8:10,13,18] *} + +## Özet ve açıklama { #summary-and-description } + +Bir `summary` ve `description` ekleyebilirsiniz: + +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} + +## Docstring’den Description { #description-from-docstring } + +Açıklamalar genelde uzun olur ve birden fazla satıra yayılır; bu yüzden *path operation* açıklamasını, fonksiyonun içinde docstring olarak tanımlayabilirsiniz; **FastAPI** de onu buradan okur. + +Docstring içinde Markdown yazabilirsiniz; doğru şekilde yorumlanır ve gösterilir (docstring girintisi dikkate alınarak). + +{* ../../docs_src/path_operation_configuration/tutorial004_py310.py hl[17:25] *} + +Interactive docs’ta şöyle kullanılacaktır: + + + +## Response description { #response-description } + +`response_description` parametresi ile response açıklamasını belirtebilirsiniz: + +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} + +/// info | Bilgi + +`response_description` özellikle response’u ifade eder; `description` ise genel olarak *path operation*’ı ifade eder. + +/// + +/// check | Ek bilgi + +OpenAPI, her *path operation* için bir response description zorunlu kılar. + +Bu yüzden siz sağlamazsanız, **FastAPI** otomatik olarak "Successful response" üretir. + +/// + + + +## Bir *path operation*’ı Deprecate Etmek { #deprecate-a-path-operation } + +Bir *path operation*’ı kaldırmadan, deprecated olarak işaretlemeniz gerekiyorsa `deprecated` parametresini verin: + +{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} + +Interactive docs’ta deprecated olduğu net şekilde işaretlenecektir: + + + +Deprecated olan ve olmayan *path operation*’ların nasıl göründüğüne bakın: + + + +## Özet { #recap } + +*Path operation*’larınızı, *path operation decorator*’larına parametre geçirerek kolayca yapılandırabilir ve metadata ekleyebilirsiniz. diff --git a/docs/tr/docs/tutorial/path-params-numeric-validations.md b/docs/tr/docs/tutorial/path-params-numeric-validations.md new file mode 100644 index 000000000..e0118d71d --- /dev/null +++ b/docs/tr/docs/tutorial/path-params-numeric-validations.md @@ -0,0 +1,154 @@ +# Path Parametreleri ve Sayısal Doğrulamalar { #path-parameters-and-numeric-validations } + +`Query` ile query parametreleri için daha fazla doğrulama ve metadata tanımlayabildiğiniz gibi, `Path` ile de path parametreleri için aynı tür doğrulama ve metadata tanımlayabilirsiniz. + +## `Path`'i İçe Aktarın { #import-path } + +Önce `fastapi` içinden `Path`'i ve `Annotated`'ı içe aktarın: + +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *} + +/// info | Bilgi + +FastAPI, 0.95.0 sürümünde `Annotated` desteğini ekledi (ve bunu önermeye başladı). + +Daha eski bir sürüm kullanıyorsanız, `Annotated` kullanmaya çalıştığınızda hata alırsınız. + +`Annotated` kullanmadan önce mutlaka FastAPI sürümünü en az 0.95.1 olacak şekilde [FastAPI sürümünü yükseltin](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}. + +/// + +## Metadata Tanımlayın { #declare-metadata } + +`Query` için geçerli olan parametrelerin aynısını tanımlayabilirsiniz. + +Örneğin, `item_id` path parametresi için bir `title` metadata değeri tanımlamak isterseniz şunu yazabilirsiniz: + +{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} + +/// note | Not + +Bir path parametresi her zaman zorunludur, çünkü path'in bir parçası olmak zorundadır. `None` ile tanımlasanız veya bir varsayılan değer verseniz bile bu hiçbir şeyi değiştirmez; yine her zaman zorunlu olur. + +/// + +## Parametreleri İhtiyacınıza Göre Sıralayın { #order-the-parameters-as-you-need } + +/// tip | İpucu + +`Annotated` kullanıyorsanız, bu muhtemelen o kadar önemli ya da gerekli değildir. + +/// + +Diyelim ki query parametresi `q`'yu zorunlu bir `str` olarak tanımlamak istiyorsunuz. + +Ayrıca bu parametre için başka bir şey tanımlamanız gerekmiyor; dolayısıyla `Query` kullanmanıza da aslında gerek yok. + +Ancak `item_id` path parametresi için yine de `Path` kullanmanız gerekiyor. Ve bir sebepten `Annotated` kullanmak istemiyorsunuz. + +Python, "default" değeri olan bir parametreyi, "default" değeri olmayan bir parametreden önce yazarsanız şikayet eder. + +Ama bunların sırasını değiştirebilir ve default değeri olmayan parametreyi (query parametresi `q`) en başa koyabilirsiniz. + +Bu **FastAPI** için önemli değildir. FastAPI parametreleri isimlerine, tiplerine ve default tanımlarına (`Query`, `Path`, vb.) göre tespit eder; sırayla ilgilenmez. + +Dolayısıyla fonksiyonunuzu şöyle tanımlayabilirsiniz: + +{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *} + +Ancak şunu unutmayın: `Annotated` kullanırsanız bu problem olmaz; çünkü `Query()` veya `Path()` için fonksiyon parametresi default değerlerini kullanmıyorsunuz. + +{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *} + +## Parametreleri İhtiyacınıza Göre Sıralayın: Küçük Hileler { #order-the-parameters-as-you-need-tricks } + +/// tip | İpucu + +`Annotated` kullanıyorsanız, bu muhtemelen o kadar önemli ya da gerekli değildir. + +/// + +İşte bazen işe yarayan **küçük bir hile**; ama çok sık ihtiyacınız olmayacak. + +Şunları yapmak istiyorsanız: + +* `q` query parametresini `Query` kullanmadan ve herhangi bir default değer vermeden tanımlamak +* `item_id` path parametresini `Path` kullanarak tanımlamak +* bunları farklı bir sırada yazmak +* `Annotated` kullanmamak + +...Python bunun için küçük, özel bir sözdizimi sunar. + +Fonksiyonun ilk parametresi olarak `*` geçin. + +Python bu `*` ile bir şey yapmaz; ama bundan sonraki tüm parametrelerin keyword argument (anahtar-değer çiftleri) olarak çağrılması gerektiğini bilir; buna kwargs da denir. Default değerleri olmasa bile. + +{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *} + +### `Annotated` ile Daha İyi { #better-with-annotated } + +Şunu da unutmayın: `Annotated` kullanırsanız, fonksiyon parametresi default değerlerini kullanmadığınız için bu sorun ortaya çıkmaz ve muhtemelen `*` kullanmanız da gerekmez. + +{* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *} + +## Sayı Doğrulamaları: Büyük Eşit { #number-validations-greater-than-or-equal } + +`Query` ve `Path` (ve ileride göreceğiniz diğerleri) ile sayı kısıtları tanımlayabilirsiniz. + +Burada `ge=1` ile, `item_id` değerinin `1`'den "`g`reater than or `e`qual" olacak şekilde bir tam sayı olması gerekir. + +{* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *} + +## Sayı Doğrulamaları: Büyük ve Küçük Eşit { #number-validations-greater-than-and-less-than-or-equal } + +Aynısı şunlar için de geçerlidir: + +* `gt`: `g`reater `t`han +* `le`: `l`ess than or `e`qual + +{* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *} + +## Sayı Doğrulamaları: `float` Değerler, Büyük ve Küçük { #number-validations-floats-greater-than-and-less-than } + +Sayı doğrulamaları `float` değerler için de çalışır. + +Burada gt tanımlayabilmek (sadece ge değil) önemli hale gelir. Çünkü örneğin bir değerin `0`'dan büyük olmasını isteyebilirsiniz; `1`'den küçük olsa bile. + +Bu durumda `0.5` geçerli bir değer olur. Ancak `0.0` veya `0` geçerli olmaz. + +Aynısı lt için de geçerlidir. + +{* ../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py hl[13] *} + +## Özet { #recap } + +`Query`, `Path` (ve henüz görmedikleriniz) ile metadata ve string doğrulamalarını, [Query Parametreleri ve String Doğrulamalar](query-params-str-validations.md){.internal-link target=_blank} bölümündekiyle aynı şekilde tanımlayabilirsiniz. + +Ayrıca sayısal doğrulamalar da tanımlayabilirsiniz: + +* `gt`: `g`reater `t`han +* `ge`: `g`reater than or `e`qual +* `lt`: `l`ess `t`han +* `le`: `l`ess than or `e`qual + +/// info | Bilgi + +`Query`, `Path` ve ileride göreceğiniz diğer class'lar ortak bir `Param` class'ının alt class'larıdır. + +Hepsi, gördüğünüz ek doğrulama ve metadata parametrelerini paylaşır. + +/// + +/// note | Teknik Detaylar + +`Query`, `Path` ve diğerlerini `fastapi` içinden import ettiğinizde, bunlar aslında birer fonksiyondur. + +Çağrıldıklarında, aynı isme sahip class'ların instance'larını döndürürler. + +Yani `Query`'yi import edersiniz; bu bir fonksiyondur. Onu çağırdığınızda, yine `Query` adlı bir class'ın instance'ını döndürür. + +Bu fonksiyonlar (class'ları doğrudan kullanmak yerine) editörünüzün type'larıyla ilgili hata işaretlememesi için vardır. + +Bu sayede, bu hataları yok saymak üzere özel ayarlar eklemeden normal editörünüzü ve coding araçlarınızı kullanabilirsiniz. + +/// diff --git a/docs/tr/docs/tutorial/query-param-models.md b/docs/tr/docs/tutorial/query-param-models.md new file mode 100644 index 000000000..75139a677 --- /dev/null +++ b/docs/tr/docs/tutorial/query-param-models.md @@ -0,0 +1,68 @@ +# Query Parameter Modelleri { #query-parameter-models } + +Birbirleriyle ilişkili bir **query parameter** grubunuz varsa, bunları tanımlamak için bir **Pydantic model** oluşturabilirsiniz. + +Böylece **modeli yeniden kullanabilir**, **birden fazla yerde** tekrar tekrar kullanabilir ve tüm parametreler için validation (doğrulama) ile metadata’yı tek seferde tanımlayabilirsiniz. 😎 + +/// note | Not + +Bu özellik FastAPI `0.115.0` sürümünden beri desteklenmektedir. 🤓 + +/// + +## Pydantic Model ile Query Parameters { #query-parameters-with-a-pydantic-model } + +İhtiyacınız olan **query parameter**’ları bir **Pydantic model** içinde tanımlayın, ardından parametreyi `Query` olarak belirtin: + +{* ../../docs_src/query_param_models/tutorial001_an_py310.py hl[9:13,17] *} + +**FastAPI**, request’teki **query parameter**’lardan **her field** için veriyi **extract** eder ve tanımladığınız Pydantic model’i size verir. + +## Dokümanları Kontrol Edin { #check-the-docs } + +Query parameter’ları `/docs` altındaki dokümantasyon arayüzünde görebilirsiniz: + +
+ +
+ +## Ek Query Parameter'ları Yasaklayın { #forbid-extra-query-parameters } + +Bazı özel kullanım senaryolarında (muhtemelen çok yaygın değil), almak istediğiniz query parameter’ları **kısıtlamak** isteyebilirsiniz. + +Pydantic’in model konfigürasyonunu kullanarak `extra` field’ları `forbid` edebilirsiniz: + +{* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} + +Bir client, **query parameter**’larda **ek (extra)** veri göndermeye çalışırsa, **error** response alır. + +Örneğin client, değeri `plumbus` olan bir `tool` query parameter’ı göndermeye çalışırsa: + +```http +https://example.com/items/?limit=10&tool=plumbus +``` + +`tool` query parameter’ına izin verilmediğini söyleyen bir **error** response alır: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["query", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus" + } + ] +} +``` + +## Özet { #summary } + +**FastAPI** içinde **query parameter**’ları tanımlamak için **Pydantic model**’leri kullanabilirsiniz. 😎 + +/// tip | İpucu + +Spoiler: cookie ve header’ları tanımlamak için de Pydantic model’leri kullanabilirsiniz; ancak bunu tutorial’ın ilerleyen bölümlerinde göreceksiniz. 🤫 + +/// diff --git a/docs/tr/docs/tutorial/query-params-str-validations.md b/docs/tr/docs/tutorial/query-params-str-validations.md new file mode 100644 index 000000000..18f0249e5 --- /dev/null +++ b/docs/tr/docs/tutorial/query-params-str-validations.md @@ -0,0 +1,473 @@ +# Query Parametreleri ve String Doğrulamaları { #query-parameters-and-string-validations } + +**FastAPI**, parametreleriniz için ek bilgi ve doğrulamalar (validation) tanımlamanıza izin verir. + +Örnek olarak şu uygulamayı ele alalım: + +{* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} + +Query parametresi `q`, `str | None` tipindedir. Yani tipi `str`’dir ama `None` da olabilir. Nitekim varsayılan değer `None` olduğu için FastAPI bunun zorunlu olmadığını anlar. + +/// note | Not + +FastAPI, `q`’nun zorunlu olmadığını `= None` varsayılan değerinden anlar. + +`str | None` kullanmak, editörünüzün daha iyi destek vermesini ve hataları yakalamasını sağlar. + +/// + +## Ek doğrulama { #additional-validation } + +`q` opsiyonel olsa bile, verildiği durumda **uzunluğunun 50 karakteri geçmemesini** zorlayacağız. + +### `Query` ve `Annotated` import edin { #import-query-and-annotated } + +Bunu yapmak için önce şunları import edin: + +* `fastapi` içinden `Query` +* `typing` içinden `Annotated` + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} + +/// info | Bilgi + +FastAPI, 0.95.0 sürümünde `Annotated` desteğini ekledi (ve önermeye başladı). + +Daha eski bir sürüm kullanıyorsanız `Annotated` kullanmaya çalışırken hata alırsınız. + +`Annotated` kullanmadan önce FastAPI sürümünü en az 0.95.1’e yükseltmek için [FastAPI sürümünü yükseltin](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}. + +/// + +## `q` parametresinin tipinde `Annotated` kullanın { #use-annotated-in-the-type-for-the-q-parameter } + +[Python Types Intro](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank} içinde `Annotated` ile parametrelerinize metadata ekleyebileceğinizi söylemiştim, hatırlıyor musunuz? + +Şimdi bunu FastAPI ile kullanmanın zamanı. 🚀 + +Şu tip anotasyonuna sahiptik: + +//// tab | Python 3.10+ + +```Python +q: str | None = None +``` + +//// + +//// tab | Python 3.9+ + +```Python +q: Union[str, None] = None +``` + +//// + +Şimdi bunu `Annotated` ile saracağız; şöyle olacak: + +//// tab | Python 3.10+ + +```Python +q: Annotated[str | None] = None +``` + +//// + +//// tab | Python 3.9+ + +```Python +q: Annotated[Union[str, None]] = None +``` + +//// + +Bu iki sürüm de aynı anlama gelir: `q`, `str` veya `None` olabilen bir parametredir ve varsayılan olarak `None`’dır. + +Şimdi işin eğlenceli kısmına geçelim. 🎉 + +## `q` parametresindeki `Annotated` içine `Query` ekleyin { #add-query-to-annotated-in-the-q-parameter } + +Artık ek bilgi (bu durumda ek doğrulama) koyabildiğimiz bir `Annotated`’ımız olduğuna göre, `Annotated` içine `Query` ekleyin ve `max_length` parametresini `50` olarak ayarlayın: + +{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} + +Varsayılan değerin hâlâ `None` olduğuna dikkat edin; yani parametre hâlâ opsiyonel. + +Ama şimdi `Annotated` içinde `Query(max_length=50)` kullanarak FastAPI’ye bu değer için **ek doğrulama** istediğimizi söylüyoruz: en fazla 50 karakter. 😎 + +/// tip | İpucu + +Burada `Query()` kullanıyoruz çünkü bu bir **query parametresi**. İleride `Path()`, `Body()`, `Header()` ve `Cookie()` gibi, `Query()` ile aynı argümanları kabul eden diğerlerini de göreceğiz. + +/// + +FastAPI artık şunları yapacak: + +* Verinin uzunluğunun en fazla 50 karakter olduğundan emin olacak şekilde **doğrulayacak** +* Veri geçerli değilse client için **net bir hata** gösterecek +* Parametreyi OpenAPI şemasındaki *path operation* içinde **dokümante edecek** (dolayısıyla **otomatik dokümantasyon arayüzünde** görünecek) + +## Alternatif (eski): Varsayılan değer olarak `Query` { #alternative-old-query-as-the-default-value } + +FastAPI’nin önceki sürümlerinde (0.95.0 öncesi) `Query`’yi `Annotated` içine koymak yerine, parametrenizin varsayılan değeri olarak kullanmanız gerekiyordu. Etrafta bu şekilde yazılmış kod görme ihtimaliniz yüksek; bu yüzden açıklayalım. + +/// tip | İpucu + +Yeni kodlarda ve mümkün olduğunda, yukarıda anlatıldığı gibi `Annotated` kullanın. Birden fazla avantajı vardır (aşağıda anlatılıyor) ve dezavantajı yoktur. 🍰 + +/// + +Fonksiyon parametresinin varsayılan değeri olarak `Query()` kullanıp `max_length` parametresini 50 yapmak şöyle olurdu: + +{* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} + +Bu senaryoda (`Annotated` kullanmadığımız için) fonksiyondaki `None` varsayılan değerini `Query()` ile değiştirmemiz gerekiyor. Bu durumda varsayılan değeri `Query(default=None)` ile vermeliyiz; bu, (en azından FastAPI açısından) aynı varsayılan değeri tanımlama amacına hizmet eder. + +Yani: + +```Python +q: str | None = Query(default=None) +``` + +...parametreyi `None` varsayılan değeriyle opsiyonel yapar; şununla aynı: + +```Python +q: str | None = None +``` + +Ancak `Query` sürümü bunun bir query parametresi olduğunu açıkça belirtir. + +Sonrasında `Query`’ye daha fazla parametre geçebiliriz. Bu örnekte string’ler için geçerli olan `max_length`: + +```Python +q: str | None = Query(default=None, max_length=50) +``` + +Bu, veriyi doğrular, veri geçerli değilse net bir hata gösterir ve parametreyi OpenAPI şemasındaki *path operation* içinde dokümante eder. + +### Varsayılan değer olarak `Query` veya `Annotated` içinde `Query` { #query-as-the-default-value-or-in-annotated } + +`Annotated` içinde `Query` kullanırken `Query` için `default` parametresini kullanamayacağınızı unutmayın. + +Bunun yerine fonksiyon parametresinin gerçek varsayılan değerini kullanın. Aksi halde tutarsız olur. + +Örneğin şu kullanım izinli değildir: + +```Python +q: Annotated[str, Query(default="rick")] = "morty" +``` + +...çünkü varsayılan değerin `"rick"` mi `"morty"` mi olması gerektiği belli değildir. + +Bu nedenle (tercihen) şöyle kullanırsınız: + +```Python +q: Annotated[str, Query()] = "rick" +``` + +...veya eski kod tabanlarında şuna rastlarsınız: + +```Python +q: str = Query(default="rick") +``` + +### `Annotated`’ın avantajları { #advantages-of-annotated } + +Fonksiyon parametrelerindeki varsayılan değer stiline göre **`Annotated` kullanmanız önerilir**; birden fazla nedenle **daha iyidir**. 🤓 + +**Fonksiyon parametresinin** **varsayılan** değeri, **gerçek varsayılan** değerdir; bu genel olarak Python açısından daha sezgiseldir. 😌 + +Aynı fonksiyonu FastAPI olmadan **başka yerlerde** de **çağırabilirsiniz** ve **beklendiği gibi çalışır**. Eğer **zorunlu** bir parametre varsa (varsayılan değer yoksa) editörünüz hata ile bunu belirtir; ayrıca gerekli parametreyi vermeden çalıştırırsanız **Python** da şikayet eder. + +`Annotated` kullanmayıp bunun yerine **(eski) varsayılan değer stilini** kullanırsanız, o fonksiyonu FastAPI olmadan **başka yerlerde** çağırdığınızda doğru çalışması için argümanları geçmeniz gerektiğini **hatırlamak** zorunda kalırsınız; yoksa değerler beklediğinizden farklı olur (ör. `str` yerine `QueryInfo` veya benzeri). Üstelik editörünüz de şikayet etmez ve Python da fonksiyonu çalıştırırken şikayet etmez; ancak içerideki operasyonlar hata verince ortaya çıkar. + +`Annotated` birden fazla metadata anotasyonu alabildiği için, artık aynı fonksiyonu Typer gibi başka araçlarla da kullanabilirsiniz. 🚀 + +## Daha fazla doğrulama ekleyin { #add-more-validations } + +`min_length` parametresini de ekleyebilirsiniz: + +{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} + +## Regular expression ekleyin { #add-regular-expressions } + +Parametrenin eşleşmesi gereken bir `pattern` regular expression tanımlayabilirsiniz: + +{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} + +Bu özel regular expression pattern’i, gelen parametre değerinin şunları sağladığını kontrol eder: + +* `^`: Aşağıdaki karakterlerle başlar; öncesinde karakter yoktur. +* `fixedquery`: Tam olarak `fixedquery` değerine sahiptir. +* `$`: Orada biter; `fixedquery` sonrasında başka karakter yoktur. + +Bu **"regular expression"** konuları gözünüzü korkutuyorsa sorun değil. Birçok kişi için zor bir konudur. Regular expression’lara ihtiyaç duymadan da pek çok şey yapabilirsiniz. + +Artık ihtiyaç duyduğunuzda **FastAPI** içinde kullanabileceğinizi biliyorsunuz. + +## Varsayılan değerler { #default-values } + +Elbette `None` dışında varsayılan değerler de kullanabilirsiniz. + +Örneğin `q` query parametresi için `min_length` değerini `3` yapmak ve varsayılan değer olarak `"fixedquery"` vermek istediğinizi düşünelim: + +{* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *} + +/// note | Not + +`None` dahil herhangi bir tipte varsayılan değere sahip olmak, parametreyi opsiyonel (zorunlu değil) yapar. + +/// + +## Zorunlu parametreler { #required-parameters } + +Daha fazla doğrulama veya metadata tanımlamamız gerekmiyorsa, `q` query parametresini yalnızca varsayılan değer tanımlamayarak zorunlu yapabiliriz: + +```Python +q: str +``` + +şunun yerine: + +```Python +q: str | None = None +``` + +Ancak biz artık `Query` ile tanımlıyoruz; örneğin şöyle: + +```Python +q: Annotated[str | None, Query(min_length=3)] = None +``` + +Dolayısıyla `Query` kullanırken bir değeri zorunlu yapmak istediğinizde, varsayılan değer tanımlamamanız yeterlidir: + +{* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *} + +### Zorunlu ama `None` olabilir { #required-can-be-none } + +Bir parametrenin `None` kabul edebileceğini söyleyip yine de zorunlu olmasını sağlayabilirsiniz. Bu, client’ların değer göndermesini zorunlu kılar; değer `None` olsa bile. + +Bunu yapmak için `None`’ı geçerli bir tip olarak tanımlayın ama varsayılan değer vermeyin: + +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} + +## Query parametresi listesi / birden fazla değer { #query-parameter-list-multiple-values } + +Bir query parametresini `Query` ile açıkça tanımladığınızda, bir değer listesi alacak şekilde (başka bir deyişle, birden fazla değer alacak şekilde) de tanımlayabilirsiniz. + +Örneğin URL’de `q` query parametresinin birden fazla kez görünebilmesini istiyorsanız şöyle yazabilirsiniz: + +{* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *} + +Sonra şu URL ile: + +``` +http://localhost:8000/items/?q=foo&q=bar +``` + +*path operation function* içinde, *function parameter* olan `q` parametresinde, birden fazla `q` *query parameters* değerini (`foo` ve `bar`) bir Python `list`’i olarak alırsınız. + +Dolayısıyla bu URL’ye response şöyle olur: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +/// tip | İpucu + +Yukarıdaki örnekte olduğu gibi tipi `list` olan bir query parametresi tanımlamak için `Query`’yi açıkça kullanmanız gerekir; aksi halde request body olarak yorumlanır. + +/// + +Etkileşimli API dokümanları da buna göre güncellenir ve birden fazla değere izin verir: + + + +### Varsayılanlarla query parametresi listesi / birden fazla değer { #query-parameter-list-multiple-values-with-defaults } + +Hiç değer verilmezse varsayılan bir `list` de tanımlayabilirsiniz: + +{* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *} + +Şu adrese giderseniz: + +``` +http://localhost:8000/items/ +``` + +`q`’nun varsayılanı `["foo", "bar"]` olur ve response şöyle olur: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +#### Sadece `list` kullanmak { #using-just-list } + +`list[str]` yerine doğrudan `list` de kullanabilirsiniz: + +{* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *} + +/// note | Not + +Bu durumda FastAPI, listenin içeriğini kontrol etmez. + +Örneğin `list[int]`, listenin içeriğinin integer olduğunu kontrol eder (ve dokümante eder). Ancak tek başına `list` bunu yapmaz. + +/// + +## Daha fazla metadata tanımlayın { #declare-more-metadata } + +Parametre hakkında daha fazla bilgi ekleyebilirsiniz. + +Bu bilgiler oluşturulan OpenAPI’a dahil edilir ve dokümantasyon arayüzleri ile harici araçlar tarafından kullanılır. + +/// note | Not + +Farklı araçların OpenAPI desteği farklı seviyelerde olabilir. + +Bazıları tanımladığınız ek bilgilerin hepsini göstermeyebilir; ancak çoğu durumda eksik özellik geliştirme planındadır. + +/// + +Bir `title` ekleyebilirsiniz: + +{* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *} + +Ve bir `description`: + +{* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} + +## Alias parametreleri { #alias-parameters } + +Parametrenin adının `item-query` olmasını istediğinizi düşünün. + +Örneğin: + +``` +http://127.0.0.1:8000/items/?item-query=foobaritems +``` + +Ancak `item-query` geçerli bir Python değişken adı değildir. + +En yakın seçenek `item_query` olur. + +Ama sizin hâlâ tam olarak `item-query` olmasına ihtiyacınız var... + +O zaman bir `alias` tanımlayabilirsiniz; bu alias, parametre değerini bulmak için kullanılacaktır: + +{* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} + +## Parametreleri deprecated yapmak { #deprecating-parameters } + +Diyelim ki artık bu parametreyi istemiyorsunuz. + +Bazı client’lar hâlâ kullandığı için bir süre tutmanız gerekiyor, ama dokümanların bunu açıkça deprecated olarak göstermesini istiyorsunuz. + +O zaman `Query`’ye `deprecated=True` parametresini geçin: + +{* ../../docs_src/query_params_str_validations/tutorial010_an_py310.py hl[19] *} + +Dokümanlarda şöyle görünür: + + + +## Parametreleri OpenAPI’dan hariç tutun { #exclude-parameters-from-openapi } + +Oluşturulan OpenAPI şemasından (dolayısıyla otomatik dokümantasyon sistemlerinden) bir query parametresini hariç tutmak için `Query`’nin `include_in_schema` parametresini `False` yapın: + +{* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} + +## Özel Doğrulama { #custom-validation } + +Yukarıdaki parametrelerle yapılamayan bazı **özel doğrulama** ihtiyaçlarınız olabilir. + +Bu durumlarda, normal doğrulamadan sonra (ör. değerin `str` olduğunun doğrulanmasından sonra) uygulanacak bir **custom validator function** kullanabilirsiniz. + +Bunu, `Annotated` içinde Pydantic’in `AfterValidator`’ını kullanarak yapabilirsiniz. + +/// tip | İpucu + +Pydantic’te `BeforeValidator` ve başka validator’lar da vardır. 🤓 + +/// + +Örneğin bu custom validator, bir item ID’sinin ISBN kitap numarası için `isbn-` ile veya IMDB film URL ID’si için `imdb-` ile başladığını kontrol eder: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} + +/// info | Bilgi + +Bu özellik Pydantic 2 ve üzeri sürümlerde mevcuttur. 😎 + +/// + +/// tip | İpucu + +Veritabanı veya başka bir API gibi herhangi bir **harici bileşen** ile iletişim gerektiren bir doğrulama yapmanız gerekiyorsa, bunun yerine **FastAPI Dependencies** kullanmalısınız; onları ileride öğreneceksiniz. + +Bu custom validator’lar, request’te sağlanan **yalnızca** **aynı veri** ile kontrol edilebilen şeyler içindir. + +/// + +### O Kodu Anlamak { #understand-that-code } + +Önemli nokta, **`Annotated` içinde bir fonksiyonla birlikte `AfterValidator` kullanmak**. İsterseniz bu kısmı atlayabilirsiniz. 🤸 + +--- + +Ama bu örnek kodun detaylarını merak ediyorsanız, birkaç ek bilgi: + +#### `value.startswith()` ile String { #string-with-value-startswith } + +Fark ettiniz mi? `value.startswith()` ile bir string, tuple alabilir ve tuple içindeki her değeri kontrol eder: + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *} + +#### Rastgele Bir Item { #a-random-item } + +`data.items()` ile, her dictionary öğesi için key ve value içeren tuple’lardan oluşan bir iterable object elde ederiz. + +Bu iterable object’i `list(data.items())` ile düzgün bir `list`’e çeviririz. + +Ardından `random.choice()` ile list’ten **rastgele bir değer** alırız; yani `(id, name)` içeren bir tuple elde ederiz. Şuna benzer: `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`. + +Sonra tuple içindeki bu **iki değeri** `id` ve `name` değişkenlerine **atarız**. + +Böylece kullanıcı bir item ID’si vermemiş olsa bile yine de rastgele bir öneri alır. + +...bütün bunları **tek bir basit satırda** yapıyoruz. 🤯 Python’u sevmemek elde mi? 🐍 + +{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} + +## Özet { #recap } + +Parametreleriniz için ek doğrulamalar ve metadata tanımlayabilirsiniz. + +Genel doğrulamalar ve metadata: + +* `alias` +* `title` +* `description` +* `deprecated` + +String’lere özel doğrulamalar: + +* `min_length` +* `max_length` +* `pattern` + +`AfterValidator` ile custom doğrulamalar. + +Bu örneklerde `str` değerleri için doğrulamanın nasıl tanımlanacağını gördünüz. + +Sayılar gibi diğer tipler için doğrulamaları nasıl tanımlayacağınızı öğrenmek için sonraki bölümlere geçin. diff --git a/docs/tr/docs/tutorial/request-files.md b/docs/tr/docs/tutorial/request-files.md new file mode 100644 index 000000000..0bbc557e0 --- /dev/null +++ b/docs/tr/docs/tutorial/request-files.md @@ -0,0 +1,176 @@ +# Request Dosyaları { #request-files } + +İstemcinin upload edeceği dosyaları `File` kullanarak tanımlayabilirsiniz. + +/// info | Bilgi + +Upload edilen dosyaları alabilmek için önce `python-multipart` yükleyin. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, aktive ettiğinizden ve ardından paketi yüklediğinizden emin olun. Örneğin: + +```console +$ pip install python-multipart +``` + +Bunun nedeni, upload edilen dosyaların "form data" olarak gönderilmesidir. + +/// + +## `File` Import Edin { #import-file } + +`fastapi` içinden `File` ve `UploadFile` import edin: + +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *} + +## `File` Parametrelerini Tanımlayın { #define-file-parameters } + +`Body` veya `Form` için yaptığınız gibi dosya parametreleri oluşturun: + +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[9] *} + +/// info | Bilgi + +`File`, doğrudan `Form`’dan türeyen bir sınıftır. + +Ancak unutmayın: `fastapi` içinden `Query`, `Path`, `File` ve diğerlerini import ettiğinizde, bunlar aslında özel sınıflar döndüren fonksiyonlardır. + +/// + +/// tip | İpucu + +File body’leri tanımlamak için `File` kullanmanız gerekir; aksi halde parametreler query parametreleri veya body (JSON) parametreleri olarak yorumlanır. + +/// + +Dosyalar "form data" olarak upload edilir. + +*path operation function* parametrenizin tipini `bytes` olarak tanımlarsanız, **FastAPI** dosyayı sizin için okur ve içeriği `bytes` olarak alırsınız. + +Bunun, dosyanın tüm içeriğinin bellekte tutulacağı anlamına geldiğini unutmayın. Küçük dosyalar için iyi çalışır. + +Ancak bazı durumlarda `UploadFile` kullanmak size fayda sağlayabilir. + +## `UploadFile` ile Dosya Parametreleri { #file-parameters-with-uploadfile } + +Tipi `UploadFile` olan bir dosya parametresi tanımlayın: + +{* ../../docs_src/request_files/tutorial001_an_py39.py hl[14] *} + +`UploadFile` kullanmanın `bytes`’a göre birkaç avantajı vardır: + +* Parametrenin varsayılan değerinde `File()` kullanmak zorunda değilsiniz. +* "Spooled" bir dosya kullanır: + * Belirli bir maksimum boyuta kadar bellekte tutulan, bu limiti aşınca diske yazılan bir dosya. +* Bu sayede görüntüler, videolar, büyük binary’ler vb. gibi büyük dosyalarda tüm belleği tüketmeden iyi çalışır. +* Upload edilen dosyadan metadata alabilirsiniz. +* file-like bir `async` arayüze sahiptir. +* `SpooledTemporaryFile` nesnesini dışa açar; bunu, file-like nesne bekleyen diğer library’lere doğrudan geçebilirsiniz. + +### `UploadFile` { #uploadfile } + +`UploadFile` şu attribute’lara sahiptir: + +* `filename`: Upload edilen orijinal dosya adını içeren bir `str` (örn. `myimage.jpg`). +* `content_type`: Content type’ı (MIME type / media type) içeren bir `str` (örn. `image/jpeg`). +* `file`: Bir `SpooledTemporaryFile` (bir file-like nesne). Bu, "file-like" nesne bekleyen diğer fonksiyonlara veya library’lere doğrudan verebileceğiniz gerçek Python file nesnesidir. + +`UploadFile` şu `async` method’lara sahiptir. Bunların hepsi altta ilgili dosya method’larını çağırır (dahili `SpooledTemporaryFile` kullanarak). + +* `write(data)`: Dosyaya `data` (`str` veya `bytes`) yazar. +* `read(size)`: Dosyadan `size` (`int`) kadar byte/karakter okur. +* `seek(offset)`: Dosyada `offset` (`int`) byte pozisyonuna gider. + * Örn. `await myfile.seek(0)` dosyanın başına gider. + * Bu, özellikle bir kez `await myfile.read()` çalıştırdıysanız ve sonra içeriği yeniden okumaya ihtiyaç duyuyorsanız faydalıdır. +* `close()`: Dosyayı kapatır. + +Bu method’ların hepsi `async` olduğundan, bunları "await" etmeniz gerekir. + +Örneğin, bir `async` *path operation function* içinde içeriği şöyle alabilirsiniz: + +```Python +contents = await myfile.read() +``` + +Normal bir `def` *path operation function* içindeyseniz `UploadFile.file`’a doğrudan erişebilirsiniz, örneğin: + +```Python +contents = myfile.file.read() +``` + +/// note | `async` Teknik Detaylar + +`async` method’ları kullandığınızda, **FastAPI** dosya method’larını bir threadpool içinde çalıştırır ve bunları await eder. + +/// + +/// note | Starlette Teknik Detaylar + +**FastAPI**’nin `UploadFile`’ı doğrudan **Starlette**’in `UploadFile`’ından türetilmiştir; ancak **Pydantic** ve FastAPI’nin diğer parçalarıyla uyumlu olması için bazı gerekli eklemeler yapar. + +/// + +## "Form Data" Nedir { #what-is-form-data } + +HTML formları (`
`) veriyi server’a gönderirken normalde JSON’dan farklı, veri için "özel" bir encoding kullanır. + +**FastAPI**, JSON yerine bu veriyi doğru yerden okuyacağından emin olur. + +/// note | Teknik Detaylar + +Formlardan gelen veri, dosya içermiyorsa normalde "media type" olarak `application/x-www-form-urlencoded` ile encode edilir. + +Ancak form dosya içeriyorsa `multipart/form-data` olarak encode edilir. `File` kullanırsanız, **FastAPI** dosyaları body’nin doğru kısmından alması gerektiğini bilir. + +Bu encoding’ler ve form alanları hakkında daha fazla okumak isterseniz MDN web dokümanlarındaki POST sayfasına bakın. + +/// + +/// warning | Uyarı + +Bir *path operation* içinde birden fazla `File` ve `Form` parametresi tanımlayabilirsiniz, ancak JSON olarak almayı beklediğiniz `Body` alanlarını ayrıca tanımlayamazsınız; çünkü request body `application/json` yerine `multipart/form-data` ile encode edilmiş olur. + +Bu, **FastAPI**’nin bir kısıtı değildir; HTTP protocol’ünün bir parçasıdır. + +/// + +## Opsiyonel Dosya Upload { #optional-file-upload } + +Standart type annotation’ları kullanıp varsayılan değeri `None` yaparak bir dosyayı opsiyonel hale getirebilirsiniz: + +{* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} + +## Ek Metadata ile `UploadFile` { #uploadfile-with-additional-metadata } + +Ek metadata ayarlamak için `UploadFile` ile birlikte `File()` da kullanabilirsiniz. Örneğin: + +{* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} + +## Birden Fazla Dosya Upload { #multiple-file-uploads } + +Aynı anda birden fazla dosya upload etmek mümkündür. + +Bu dosyalar, "form data" ile gönderilen aynı "form field" ile ilişkilendirilir. + +Bunu kullanmak için `bytes` veya `UploadFile` listesini tanımlayın: + +{* ../../docs_src/request_files/tutorial002_an_py39.py hl[10,15] *} + +Tanımladığınız gibi, `bytes` veya `UploadFile`’lardan oluşan bir `list` alırsınız. + +/// note | Teknik Detaylar + +`from starlette.responses import HTMLResponse` da kullanabilirsiniz. + +**FastAPI**, geliştiriciye kolaylık olsun diye `starlette.responses` modülünü `fastapi.responses` olarak da sağlar. Ancak mevcut response’ların çoğu doğrudan Starlette’ten gelir. + +/// + +### Ek Metadata ile Birden Fazla Dosya Upload { #multiple-file-uploads-with-additional-metadata } + +Daha önce olduğu gibi, `UploadFile` için bile ek parametreler ayarlamak amacıyla `File()` kullanabilirsiniz: + +{* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} + +## Özet { #recap } + +Request’te (form data olarak gönderilen) upload edilecek dosyaları tanımlamak için `File`, `bytes` ve `UploadFile` kullanın. diff --git a/docs/tr/docs/tutorial/request-form-models.md b/docs/tr/docs/tutorial/request-form-models.md new file mode 100644 index 000000000..c35f956fc --- /dev/null +++ b/docs/tr/docs/tutorial/request-form-models.md @@ -0,0 +1,78 @@ +# Form Model'leri { #form-models } + +FastAPI'de **form field**'larını tanımlamak için **Pydantic model**'lerini kullanabilirsiniz. + +/// info | Bilgi + +Form'ları kullanmak için önce `python-multipart`'ı yükleyin. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu etkinleştirdiğinizden ve ardından paketi kurduğunuzdan emin olun. Örneğin: + +```console +$ pip install python-multipart +``` + +/// + +/// note | Not + +Bu özellik FastAPI `0.113.0` sürümünden itibaren desteklenmektedir. 🤓 + +/// + +## Form'lar için Pydantic Model'leri { #pydantic-models-for-forms } + +Sadece, **form field** olarak almak istediğiniz alanlarla bir **Pydantic model** tanımlayın ve ardından parametreyi `Form` olarak bildirin: + +{* ../../docs_src/request_form_models/tutorial001_an_py39.py hl[9:11,15] *} + +**FastAPI**, request içindeki **form data**'dan **her bir field** için veriyi **çıkarır** ve size tanımladığınız Pydantic model'ini verir. + +## Dokümanları Kontrol Edin { #check-the-docs } + +Bunu `/docs` altındaki doküman arayüzünde doğrulayabilirsiniz: + +
+ +
+ +## Fazladan Form Field'larını Yasaklayın { #forbid-extra-form-fields } + +Bazı özel kullanım senaryolarında (muhtemelen çok yaygın değildir), form field'larını yalnızca Pydantic model'inde tanımlananlarla **sınırlamak** isteyebilirsiniz. Ve **fazladan** gelen field'ları **yasaklayabilirsiniz**. + +/// note | Not + +Bu özellik FastAPI `0.114.0` sürümünden itibaren desteklenmektedir. 🤓 + +/// + +Herhangi bir `extra` field'ı `forbid` etmek için Pydantic'in model konfigürasyonunu kullanabilirsiniz: + +{* ../../docs_src/request_form_models/tutorial002_an_py39.py hl[12] *} + +Bir client fazladan veri göndermeye çalışırsa, bir **error** response alır. + +Örneğin, client şu form field'larını göndermeye çalışırsa: + +* `username`: `Rick` +* `password`: `Portal Gun` +* `extra`: `Mr. Poopybutthole` + +`extra` field'ının izinli olmadığını söyleyen bir error response alır: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["body", "extra"], + "msg": "Extra inputs are not permitted", + "input": "Mr. Poopybutthole" + } + ] +} +``` + +## Özet { #summary } + +FastAPI'de form field'larını tanımlamak için Pydantic model'lerini kullanabilirsiniz. 😎 diff --git a/docs/tr/docs/tutorial/request-forms-and-files.md b/docs/tr/docs/tutorial/request-forms-and-files.md new file mode 100644 index 000000000..86d26b498 --- /dev/null +++ b/docs/tr/docs/tutorial/request-forms-and-files.md @@ -0,0 +1,41 @@ +# Request Forms ve Files { #request-forms-and-files } + +`File` ve `Form` kullanarak aynı anda hem dosyaları hem de form alanlarını tanımlayabilirsiniz. + +/// info | Bilgi + +Yüklenen dosyaları ve/veya form verisini almak için önce `python-multipart` paketini kurun. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu aktive ettiğinizden ve ardından paketi kurduğunuzdan emin olun, örneğin: + +```console +$ pip install python-multipart +``` + +/// + +## `File` ve `Form` Import Edin { #import-file-and-form } + +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[3] *} + +## `File` ve `Form` Parametrelerini Tanımlayın { #define-file-and-form-parameters } + +Dosya ve form parametrelerini, `Body` veya `Query` için yaptığınız şekilde oluşturun: + +{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[10:12] *} + +Dosyalar ve form alanları form data olarak upload edilir ve siz de dosyaları ve form alanlarını alırsınız. + +Ayrıca bazı dosyaları `bytes` olarak, bazılarını da `UploadFile` olarak tanımlayabilirsiniz. + +/// warning | Uyarı + +Bir *path operation* içinde birden fazla `File` ve `Form` parametresi tanımlayabilirsiniz; ancak request'in body'si `application/json` yerine `multipart/form-data` ile encode edileceği için, JSON olarak almayı beklediğiniz `Body` alanlarını aynı anda tanımlayamazsınız. + +Bu **FastAPI** kısıtı değildir; HTTP protokolünün bir parçasıdır. + +/// + +## Özet { #recap } + +Aynı request içinde hem veri hem de dosya almanız gerektiğinde `File` ve `Form`'u birlikte kullanın. diff --git a/docs/tr/docs/tutorial/response-model.md b/docs/tr/docs/tutorial/response-model.md new file mode 100644 index 000000000..f1d1f7d15 --- /dev/null +++ b/docs/tr/docs/tutorial/response-model.md @@ -0,0 +1,343 @@ +# Response Model - Dönüş Tipi { #response-model-return-type } + +*Path operation function* **dönüş tipini** (return type) type annotation ile belirtip response için kullanılacak tipi tanımlayabilirsiniz. + +Fonksiyon **parametreleri** için input data’da kullandığınız **type annotations** yaklaşımının aynısını burada da kullanabilirsiniz; Pydantic model’leri, list’ler, dict’ler, integer, boolean gibi skaler değerler vb. + +{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} + +FastAPI bu dönüş tipini şunlar için kullanır: + +* Dönen veriyi **doğrulamak** (validate). + * Veri geçersizse (ör. bir field eksikse), bu *sizin* uygulama kodunuzun bozuk olduğu, olması gerekeni döndürmediği anlamına gelir; bu yüzden yanlış veri döndürmek yerine server error döner. Böylece siz ve client’larınız, beklenen veri ve veri şeklinin geleceğinden emin olabilirsiniz. +* OpenAPI’deki *path operation* içine response için bir **JSON Schema** eklemek. + * Bu, **otomatik dokümantasyon** tarafından kullanılır. + * Ayrıca otomatik client code generation araçları tarafından da kullanılır. + +Ama en önemlisi: + +* Çıktı verisini, dönüş tipinde tanımlı olana göre **sınırlar ve filtreler**. + * Bu, özellikle **güvenlik** açısından önemlidir; aşağıda daha fazlasını göreceğiz. + +## `response_model` Parametresi { #response-model-parameter } + +Bazı durumlarda, tam olarak dönüş tipinin söylediği gibi olmayan bir veriyi döndürmeniz gerekebilir ya da isteyebilirsiniz. + +Örneğin, **bir dict** veya bir veritabanı objesi döndürmek isteyip, ama **onu bir Pydantic model olarak declare etmek** isteyebilirsiniz. Böylece Pydantic model, döndürdüğünüz obje (ör. dict veya veritabanı objesi) için dokümantasyon, doğrulama vb. işlerin tamamını yapar. + +Eğer dönüş tipi annotation’ını eklerseniz, araçlar ve editörler (doğru şekilde) fonksiyonunuzun, declare ettiğiniz tipten (ör. Pydantic model) farklı bir tip (ör. dict) döndürdüğünü söyleyip hata verir. + +Bu gibi durumlarda, dönüş tipi yerine *path operation decorator* parametresi olan `response_model`’i kullanabilirsiniz. + +`response_model` parametresini herhangi bir *path operation* içinde kullanabilirsiniz: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* vb. + +{* ../../docs_src/response_model/tutorial001_py310.py hl[17,22,24:27] *} + +/// note | Not + +`response_model`’in "decorator" metodunun (`get`, `post` vb.) bir parametresi olduğuna dikkat edin. Body ve diğer parametreler gibi, sizin *path operation function*’ınızın parametresi değildir. + +/// + +`response_model`, Pydantic model field’ı için declare edeceğiniz aynı tipi alır; yani bir Pydantic model olabilir ama örneğin `List[Item]` gibi Pydantic model’lerden oluşan bir `list` de olabilir. + +FastAPI bu `response_model`’i; dokümantasyon, doğrulama vb. her şey için ve ayrıca çıktı verisini **tip tanımına göre dönüştürmek ve filtrelemek** için kullanır. + +/// tip | İpucu + +Editörünüzde, mypy vb. ile sıkı type kontrolü yapıyorsanız, fonksiyon dönüş tipini `Any` olarak declare edebilirsiniz. + +Böylece editöre bilerek her şeyi döndürebileceğinizi söylemiş olursunuz. Ancak FastAPI, `response_model` ile dokümantasyon, doğrulama, filtreleme vb. işlemleri yine de yapar. + +/// + +### `response_model` Önceliği { #response-model-priority } + +Hem dönüş tipi hem de `response_model` declare ederseniz, FastAPI’de `response_model` önceliklidir ve o kullanılır. + +Böylece, response model’den farklı bir tip döndürdüğünüz durumlarda bile editör ve mypy gibi araçlar için fonksiyonlarınıza doğru type annotation’lar ekleyebilir, aynı zamanda FastAPI’nin `response_model` üzerinden veri doğrulama, dokümantasyon vb. yapmasını sağlayabilirsiniz. + +Ayrıca `response_model=None` kullanarak, ilgili *path operation* için response model oluşturulmasını devre dışı bırakabilirsiniz. Bu, Pydantic field’ı olarak geçerli olmayan şeyler için type annotation eklediğinizde gerekebilir; aşağıdaki bölümlerden birinde bunun örneğini göreceksiniz. + +## Aynı input verisini geri döndürmek { #return-the-same-input-data } + +Burada `UserIn` adında bir model declare ediyoruz; bu model plaintext bir password içerecek: + +{* ../../docs_src/response_model/tutorial002_py310.py hl[7,9] *} + +/// info | Bilgi + +`EmailStr` kullanmak için önce `email-validator` paketini kurun. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu aktive ettiğinizden emin olun ve ardından örneğin şöyle kurun: + +```console +$ pip install email-validator +``` + +veya şöyle: + +```console +$ pip install "pydantic[email]" +``` + +/// + +Bu model ile hem input’u declare ediyoruz hem de output’u aynı model ile declare ediyoruz: + +{* ../../docs_src/response_model/tutorial002_py310.py hl[16] *} + +Artık bir browser password ile user oluşturduğunda, API response içinde aynı password’ü geri döndürecek. + +Bu örnekte sorun olmayabilir; çünkü password’ü gönderen kullanıcı zaten aynı kişi. + +Ancak aynı modeli başka bir *path operation* için kullanırsak, kullanıcının password’lerini her client’a gönderiyor olabiliriz. + +/// danger + +Tüm riskleri bildiğinizden ve ne yaptığınızdan emin olmadığınız sürece, bir kullanıcının plain password’ünü asla saklamayın ve bu şekilde response içinde göndermeyin. + +/// + +## Bir output modeli ekleyin { #add-an-output-model } + +Bunun yerine, plaintext password içeren bir input modeli ve password’ü içermeyen bir output modeli oluşturabiliriz: + +{* ../../docs_src/response_model/tutorial003_py310.py hl[9,11,16] *} + +Burada *path operation function* password içeren aynı input user’ı döndürüyor olsa bile: + +{* ../../docs_src/response_model/tutorial003_py310.py hl[24] *} + +...`response_model` olarak, password’ü içermeyen `UserOut` modelimizi declare ettik: + +{* ../../docs_src/response_model/tutorial003_py310.py hl[22] *} + +Dolayısıyla **FastAPI**, output model’de declare edilmemiş tüm verileri (Pydantic kullanarak) filtrelemekle ilgilenir. + +### `response_model` mi Return Type mı? { #response-model-or-return-type } + +Bu durumda iki model farklı olduğu için fonksiyon dönüş tipini `UserOut` olarak annotate etseydik, editör ve araçlar farklı class’lar olduğu için geçersiz bir tip döndürdüğümüzü söyleyip hata verecekti. + +Bu yüzden bu örnekte `response_model` parametresinde declare etmek zorundayız. + +...ama bunu nasıl aşabileceğinizi görmek için aşağıyı okumaya devam edin. + +## Return Type ve Veri Filtreleme { #return-type-and-data-filtering } + +Önceki örnekten devam edelim. Fonksiyonu **tek bir tip ile annotate etmek** istiyoruz; ama fonksiyondan gerçekte **daha fazla veri** içeren bir şey döndürebilmek istiyoruz. + +FastAPI’nin response model’i kullanarak veriyi **filtrelemeye** devam etmesini istiyoruz. Yani fonksiyon daha fazla veri döndürse bile response, sadece response model’de declare edilmiş field’ları içersin. + +Önceki örnekte class’lar farklı olduğu için `response_model` parametresini kullanmak zorundaydık. Ancak bu, editör ve araçların fonksiyon dönüş tipi kontrolünden gelen desteğini alamadığımız anlamına da geliyor. + +Ama bu tarz durumların çoğunda modelin amacı, bu örnekteki gibi bazı verileri **filtrelemek/kaldırmak** olur. + +Bu gibi durumlarda class’lar ve inheritance kullanarak, fonksiyon **type annotations** sayesinde editör ve araçlarda daha iyi destek alabilir, aynı zamanda FastAPI’nin **veri filtrelemesini** de koruyabiliriz. + +{* ../../docs_src/response_model/tutorial003_01_py310.py hl[7:10,13:14,18] *} + +Bununla birlikte, code type’lar açısından doğru olduğu için editörler ve mypy araç desteği verir; ayrıca FastAPI’den veri filtrelemeyi de alırız. + +Bu nasıl çalışıyor? Bir bakalım. 🤓 + +### Type Annotations ve Araç Desteği { #type-annotations-and-tooling } + +Önce editörler, mypy ve diğer araçlar bunu nasıl görür, ona bakalım. + +`BaseUser` temel field’lara sahiptir. Ardından `UserIn`, `BaseUser`’dan miras alır ve `password` field’ını ekler; yani iki modelin field’larının tamamını içerir. + +Fonksiyonun dönüş tipini `BaseUser` olarak annotate ediyoruz ama gerçekte bir `UserIn` instance’ı döndürüyoruz. + +Editör, mypy ve diğer araçlar buna itiraz etmez; çünkü typing açısından `UserIn`, `BaseUser`’ın subclass’ıdır. Bu da, bir `BaseUser` bekleniyorken `UserIn`’in *geçerli* bir tip olduğu anlamına gelir. + +### FastAPI Veri Filtreleme { #fastapi-data-filtering } + +FastAPI açısından ise dönüş tipini görür ve döndürdüğünüz şeyin **yalnızca** tipte declare edilen field’ları içerdiğinden emin olur. + +FastAPI, Pydantic ile içeride birkaç işlem yapar; böylece class inheritance kurallarının dönen veri filtrelemede aynen kullanılmasına izin vermez. Aksi halde beklediğinizden çok daha fazla veriyi response’ta döndürebilirdiniz. + +Bu sayede iki dünyanın da en iyisini alırsınız: **araç desteği** veren type annotations ve **veri filtreleme**. + +## Dokümanlarda görün { #see-it-in-the-docs } + +Otomatik dokümanları gördüğünüzde, input model ve output model’in her birinin kendi JSON Schema’sına sahip olduğunu kontrol edebilirsiniz: + + + +Ve her iki model de etkileşimli API dokümantasyonunda kullanılır: + + + +## Diğer Return Type Annotation’ları { #other-return-type-annotations } + +Bazı durumlarda Pydantic field olarak geçerli olmayan bir şey döndürebilir ve bunu fonksiyonda annotate edebilirsiniz; amaç sadece araçların (editör, mypy vb.) sağladığı desteği almaktır. + +### Doğrudan Response Döndürmek { #return-a-response-directly } + +En yaygın durum, [ileri seviye dokümanlarda daha sonra anlatıldığı gibi doğrudan bir Response döndürmektir](../advanced/response-directly.md){.internal-link target=_blank}. + +{* ../../docs_src/response_model/tutorial003_02_py39.py hl[8,10:11] *} + +Bu basit durum FastAPI tarafından otomatik olarak ele alınır; çünkü dönüş tipi annotation’ı `Response` class’ıdır (veya onun bir subclass’ı). + +Araçlar da memnun olur; çünkü hem `RedirectResponse` hem `JSONResponse`, `Response`’un subclass’ıdır. Yani type annotation doğrudur. + +### Bir Response Subclass’ını Annotate Etmek { #annotate-a-response-subclass } + +Type annotation içinde `Response`’un bir subclass’ını da kullanabilirsiniz: + +{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *} + +Bu da çalışır; çünkü `RedirectResponse`, `Response`’un subclass’ıdır ve FastAPI bu basit durumu otomatik olarak yönetir. + +### Geçersiz Return Type Annotation’ları { #invalid-return-type-annotations } + +Ancak geçerli bir Pydantic tipi olmayan başka rastgele bir obje (ör. bir veritabanı objesi) döndürür ve fonksiyonu da öyle annotate ederseniz, FastAPI bu type annotation’dan bir Pydantic response model oluşturmaya çalışır ve başarısız olur. + +Aynı şey, farklı tipler arasında bir union kullandığınızda ve bu tiplerden biri veya birkaçı geçerli bir Pydantic tipi değilse de olur; örneğin şu kullanım patlar 💥: + +{* ../../docs_src/response_model/tutorial003_04_py310.py hl[8] *} + +...bu, type annotation Pydantic tipi olmadığı ve tek bir `Response` class’ı (veya subclass’ı) olmadığı için başarısız olur; bu, bir `Response` ile bir `dict` arasında union’dır (ikiden herhangi biri). + +### Response Model’i Devre Dışı Bırakmak { #disable-response-model } + +Yukarıdaki örnekten devam edersek; FastAPI’nin varsayılan olarak yaptığı veri doğrulama, dokümantasyon, filtreleme vb. işlemleri istemiyor olabilirsiniz. + +Ancak yine de editörler ve type checker’lar (ör. mypy) gibi araçların desteğini almak için fonksiyonda dönüş tipi annotation’ını korumak isteyebilirsiniz. + +Bu durumda `response_model=None` ayarlayarak response model üretimini devre dışı bırakabilirsiniz: + +{* ../../docs_src/response_model/tutorial003_05_py310.py hl[7] *} + +Bu, FastAPI’nin response model üretimini atlamasını sağlar; böylece FastAPI uygulamanızı etkilemeden ihtiyacınız olan herhangi bir return type annotation’ını kullanabilirsiniz. 🤓 + +## Response Model encoding parametreleri { #response-model-encoding-parameters } + +Response model’inizde şu şekilde default değerler olabilir: + +{* ../../docs_src/response_model/tutorial004_py310.py hl[9,11:12] *} + +* `description: Union[str, None] = None` (veya Python 3.10’da `str | None = None`) için default `None`’dır. +* `tax: float = 10.5` için default `10.5`’tir. +* `tags: List[str] = []` için default, boş bir list’tir: `[]`. + +Ancak gerçekte kaydedilmedilerse, bunları sonuçtan çıkarmak isteyebilirsiniz. + +Örneğin NoSQL veritabanında çok sayıda optional attribute içeren modelleriniz varsa, default değerlerle dolu çok uzun JSON response’ları göndermek istemeyebilirsiniz. + +### `response_model_exclude_unset` parametresini kullanın { #use-the-response-model-exclude-unset-parameter } + +*Path operation decorator* parametresi olarak `response_model_exclude_unset=True` ayarlayabilirsiniz: + +{* ../../docs_src/response_model/tutorial004_py310.py hl[22] *} + +böylece response’a default değerler dahil edilmez; yalnızca gerçekten set edilmiş değerler gelir. + +Dolayısıyla ID’si `foo` olan item için bu *path operation*’a request atarsanız, response (default değerler olmadan) şöyle olur: + +```JSON +{ + "name": "Foo", + "price": 50.2 +} +``` + +/// info | Bilgi + +Ayrıca şunları da kullanabilirsiniz: + +* `response_model_exclude_defaults=True` +* `response_model_exclude_none=True` + +Bunlar, `exclude_defaults` ve `exclude_none` için Pydantic dokümanlarında anlatıldığı gibidir. + +/// + +#### Default’u olan field’lar için değer içeren data { #data-with-values-for-fields-with-defaults } + +Ama data’nız modelde default değeri olan field’lar için değer içeriyorsa, örneğin ID’si `bar` olan item gibi: + +```Python hl_lines="3 5" +{ + "name": "Bar", + "description": "The bartenders", + "price": 62, + "tax": 20.2 +} +``` + +bunlar response’a dahil edilir. + +#### Default değerlerle aynı değerlere sahip data { #data-with-the-same-values-as-the-defaults } + +Eğer data, default değerlerle aynı değerlere sahipse, örneğin ID’si `baz` olan item gibi: + +```Python hl_lines="3 5-6" +{ + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [] +} +``` + +FastAPI yeterince akıllıdır (aslında Pydantic yeterince akıllıdır) ve `description`, `tax`, `tags` default ile aynı olsa bile bunların explicit olarak set edildiğini (default’tan alınmadığını) anlar. + +Bu yüzden JSON response içinde yer alırlar. + +/// tip | İpucu + +Default değerlerin yalnızca `None` olmak zorunda olmadığını unutmayın. + +Bir list (`[]`), `10.5` gibi bir `float` vb. olabilirler. + +/// + +### `response_model_include` ve `response_model_exclude` { #response-model-include-and-response-model-exclude } + +Ayrıca *path operation decorator* parametreleri `response_model_include` ve `response_model_exclude`’u da kullanabilirsiniz. + +Bunlar; dahil edilecek attribute isimlerini (geri kalanını atlayarak) ya da hariç tutulacak attribute isimlerini (geri kalanını dahil ederek) belirten `str` değerlerinden oluşan bir `set` alır. + +Tek bir Pydantic model’iniz varsa ve output’tan bazı verileri hızlıca çıkarmak istiyorsanız, bu yöntem pratik bir kısayol olabilir. + +/// tip | İpucu + +Ancak yine de, bu parametreler yerine yukarıdaki yaklaşımı (birden fazla class kullanmayı) tercih etmeniz önerilir. + +Çünkü `response_model_include` veya `response_model_exclude` ile bazı attribute’ları atlıyor olsanız bile, uygulamanızın OpenAPI’sinde (ve dokümanlarda) üretilen JSON Schema hâlâ tam modelin JSON Schema’sı olacaktır. + +Bu durum, benzer şekilde çalışan `response_model_by_alias` için de geçerlidir. + +/// + +{* ../../docs_src/response_model/tutorial005_py310.py hl[29,35] *} + +/// tip | İpucu + +`{"name", "description"}` sözdizimi, bu iki değere sahip bir `set` oluşturur. + +Bu, `set(["name", "description"])` ile eşdeğerdir. + +/// + +#### `set` yerine `list` kullanmak { #using-lists-instead-of-sets } + +Yanlışlıkla `set` yerine `list` veya `tuple` kullanırsanız, FastAPI bunu yine `set`’e çevirir ve doğru şekilde çalışır: + +{* ../../docs_src/response_model/tutorial006_py310.py hl[29,35] *} + +## Özet { #recap } + +Response model’leri tanımlamak ve özellikle private data’nın filtrelendiğinden emin olmak için *path operation decorator* parametresi `response_model`’i kullanın. + +Yalnızca explicit olarak set edilmiş değerleri döndürmek için `response_model_exclude_unset` kullanın. diff --git a/docs/tr/docs/tutorial/response-status-code.md b/docs/tr/docs/tutorial/response-status-code.md new file mode 100644 index 000000000..57ae7bde3 --- /dev/null +++ b/docs/tr/docs/tutorial/response-status-code.md @@ -0,0 +1,101 @@ +# Response Status Code { #response-status-code } + +Bir response model tanımlayabildiğiniz gibi, herhangi bir *path operation* içinde `status_code` parametresiyle response için kullanılacak HTTP status code'u da belirtebilirsiniz: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* vb. + +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} + +/// note | Not + +`status_code`'un, "decorator" metodunun (`get`, `post`, vb.) bir parametresi olduğuna dikkat edin. Tüm parametreler ve body gibi, sizin *path operation function*'ınızın bir parametresi değildir. + +/// + +`status_code` parametresi, HTTP status code'u içeren bir sayı alır. + +/// info | Bilgi + +Alternatif olarak `status_code`, Python'un `http.HTTPStatus`'ı gibi bir `IntEnum` da alabilir. + +/// + +Bu sayede: + +* Response'da o status code döner. +* OpenAPI şemasında (dolayısıyla kullanıcı arayüzlerinde de) bu şekilde dokümante edilir: + + + +/// note | Not + +Bazı response code'lar (bir sonraki bölümde göreceğiz) response'un bir body'ye sahip olmadığını belirtir. + +FastAPI bunu bilir ve response body olmadığını söyleyen OpenAPI dokümantasyonunu üretir. + +/// + +## HTTP status code'lar hakkında { #about-http-status-codes } + +/// note | Not + +HTTP status code'ların ne olduğunu zaten biliyorsanız, bir sonraki bölüme geçin. + +/// + +HTTP'de, response'un bir parçası olarak 3 basamaklı sayısal bir status code gönderirsiniz. + +Bu status code'ların tanınmalarını sağlayan bir isimleri de vardır; ancak önemli olan kısım sayıdır. + +Kısaca: + +* `100 - 199` "Information" içindir. Doğrudan nadiren kullanırsınız. Bu status code'lara sahip response'lar body içeremez. +* **`200 - 299`** "Successful" response'lar içindir. En sık kullanacağınız aralık budur. + * `200`, varsayılan status code'dur ve her şeyin "OK" olduğunu ifade eder. + * Başka bir örnek `201` ("Created") olabilir. Genellikle veritabanında yeni bir kayıt oluşturduktan sonra kullanılır. + * Özel bir durum ise `204` ("No Content")'tür. Client'a döndürülecek içerik olmadığında kullanılır; bu nedenle response body olmamalıdır. +* **`300 - 399`** "Redirection" içindir. Bu status code'lara sahip response'lar, `304` ("Not Modified") hariç, body içerebilir de içermeyebilir de; `304` kesinlikle body içermemelidir. +* **`400 - 499`** "Client error" response'ları içindir. Muhtemelen en sık kullanacağınız ikinci aralık budur. + * Örneğin `404`, "Not Found" response'u içindir. + * Client kaynaklı genel hatalar için doğrudan `400` kullanabilirsiniz. +* `500 - 599` server hataları içindir. Neredeyse hiç doğrudan kullanmazsınız. Uygulama kodunuzun bir bölümünde ya da server'da bir şeyler ters giderse, otomatik olarak bu status code'lardan biri döner. + +/// tip | İpucu + +Her bir status code hakkında daha fazla bilgi almak ve hangi kodun ne için kullanıldığını görmek için HTTP status code'lar hakkında MDN dokümantasyonuna göz atın. + +/// + +## İsimleri hatırlamak için kısayol { #shortcut-to-remember-the-names } + +Önceki örneğe tekrar bakalım: + +{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *} + +`201`, "Created" için kullanılan status code'dur. + +Ancak bu kodların her birinin ne anlama geldiğini ezberlemek zorunda değilsiniz. + +`fastapi.status` içindeki kolaylık değişkenlerini (convenience variables) kullanabilirsiniz. + +{* ../../docs_src/response_status_code/tutorial002_py39.py hl[1,6] *} + +Bunlar sadece kolaylık sağlar; aynı sayıyı taşırlar. Ancak bu şekilde editörün autocomplete özelliğiyle kolayca bulabilirsiniz: + + + +/// note | Teknik Detaylar + +`from starlette import status` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.status`'u `fastapi.status` olarak da sunar. Ancak bu aslında doğrudan Starlette'den gelir. + +/// + +## Varsayılanı değiştirmek { #changing-the-default } + +Daha sonra, [İleri Düzey Kullanıcı Kılavuzu](../advanced/response-change-status-code.md){.internal-link target=_blank} içinde, burada tanımladığınız varsayılanın dışında farklı bir status code nasıl döndüreceğinizi göreceksiniz. diff --git a/docs/tr/docs/tutorial/schema-extra-example.md b/docs/tr/docs/tutorial/schema-extra-example.md new file mode 100644 index 000000000..a91dda892 --- /dev/null +++ b/docs/tr/docs/tutorial/schema-extra-example.md @@ -0,0 +1,202 @@ +# Request Örnek Verilerini Tanımlama { #declare-request-example-data } + +Uygulamanızın alabileceği veriler için örnekler (examples) tanımlayabilirsiniz. + +Bunu yapmanın birkaç yolu var. + +## Pydantic modellerinde ek JSON Schema verisi { #extra-json-schema-data-in-pydantic-models } + +Oluşturulan JSON Schema’ya eklenecek şekilde bir Pydantic model için `examples` tanımlayabilirsiniz. + +{* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} + +Bu ek bilgi, o modelin çıktı **JSON Schema**’sına olduğu gibi eklenir ve API dokümanlarında kullanılır. + +Pydantic dokümanları: Configuration bölümünde anlatıldığı gibi, bir `dict` alan `model_config` niteliğini kullanabilirsiniz. + +Üretilen JSON Schema’da görünmesini istediğiniz (ör. `examples` dahil) her türlü ek veriyi içeren bir `dict` ile `"json_schema_extra"` ayarlayabilirsiniz. + +/// tip | İpucu + +Aynı tekniği JSON Schema’yı genişletmek ve kendi özel ek bilgilerinizi eklemek için de kullanabilirsiniz. + +Örneğin, bir frontend kullanıcı arayüzü için metadata eklemek vb. amaçlarla kullanılabilir. + +/// + +/// info | Bilgi + +OpenAPI 3.1.0 (FastAPI 0.99.0’dan beri kullanılıyor), **JSON Schema** standardının bir parçası olan `examples` için destek ekledi. + +Bundan önce yalnızca tek bir örnek için `example` anahtar kelimesini destekliyordu. Bu hâlâ OpenAPI 3.1.0 tarafından desteklenir; ancak artık deprecated durumdadır ve JSON Schema standardının parçası değildir. Bu nedenle `example` kullanımını `examples`’a taşımanız önerilir. 🤓 + +Daha fazlasını sayfanın sonunda okuyabilirsiniz. + +/// + +## `Field` ek argümanları { #field-additional-arguments } + +Pydantic modelleriyle `Field()` kullanırken ek `examples` de tanımlayabilirsiniz: + +{* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *} + +## JSON Schema - OpenAPI içinde `examples` { #examples-in-json-schema-openapi } + +Aşağıdakilerden herhangi birini kullanırken: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +OpenAPI içindeki **JSON Schema**’larına eklenecek ek bilgilerle birlikte bir `examples` grubu da tanımlayabilirsiniz. + +### `examples` ile `Body` { #body-with-examples } + +Burada `Body()` içinde beklenen veri için tek bir örnek içeren `examples` geçiriyoruz: + +{* ../../docs_src/schema_extra_example/tutorial003_an_py310.py hl[22:29] *} + +### Doküman arayüzünde örnek { #example-in-the-docs-ui } + +Yukarıdaki yöntemlerden herhangi biriyle `/docs` içinde şöyle görünür: + + + +### Birden fazla `examples` ile `Body` { #body-with-multiple-examples } + +Elbette birden fazla `examples` da geçebilirsiniz: + +{* ../../docs_src/schema_extra_example/tutorial004_an_py310.py hl[23:38] *} + +Bunu yaptığınızda, örnekler bu body verisi için dahili **JSON Schema**’nın bir parçası olur. + +Buna rağmen, time of writing this, doküman arayüzünü gösteren araç olan Swagger UI, **JSON Schema** içindeki veriler için birden fazla örneği göstermeyi desteklemiyor. Ancak aşağıda bir çözüm yolu var. + +### OpenAPI’ye özel `examples` { #openapi-specific-examples } + +**JSON Schema** `examples`’ı desteklemeden önce OpenAPI, yine `examples` adlı farklı bir alanı destekliyordu. + +Bu **OpenAPI’ye özel** `examples`, OpenAPI spesifikasyonunda başka bir bölümde yer alır. Her bir JSON Schema’nın içinde değil, **her bir *path operation* detayları** içinde bulunur. + +Swagger UI da bu özel `examples` alanını bir süredir destekliyor. Dolayısıyla bunu, **doküman arayüzünde** farklı **örnekleri göstermek** için kullanabilirsiniz. + +OpenAPI’ye özel bu `examples` alanının şekli, (bir `list` yerine) **birden fazla örnek** içeren bir `dict`’tir; her örnek ayrıca **OpenAPI**’ye eklenecek ekstra bilgiler içerir. + +Bu, OpenAPI’nin içerdiği JSON Schema’ların içine girmez; bunun yerine doğrudan *path operation* üzerinde, dışarıda yer alır. + +### `openapi_examples` Parametresini Kullanma { #using-the-openapi-examples-parameter } + +FastAPI’de OpenAPI’ye özel `examples`’ı, şu araçlar için `openapi_examples` parametresiyle tanımlayabilirsiniz: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +`dict`’in anahtarları her bir örneği tanımlar; her bir değer ise başka bir `dict`’tir. + +`examples` içindeki her bir örnek `dict`’i şunları içerebilir: + +* `summary`: Örnek için kısa açıklama. +* `description`: Markdown metni içerebilen uzun açıklama. +* `value`: Gösterilecek gerçek örnek (ör. bir `dict`). +* `externalValue`: `value`’a alternatif; örneğe işaret eden bir URL. Ancak bu, `value` kadar çok araç tarafından desteklenmiyor olabilir. + +Şöyle kullanabilirsiniz: + +{* ../../docs_src/schema_extra_example/tutorial005_an_py310.py hl[23:49] *} + +### Doküman Arayüzünde OpenAPI Örnekleri { #openapi-examples-in-the-docs-ui } + +`Body()`’ye `openapi_examples` eklendiğinde `/docs` şöyle görünür: + + + +## Teknik Detaylar { #technical-details } + +/// tip | İpucu + +Zaten **FastAPI** sürümü **0.99.0 veya üzerini** kullanıyorsanız, büyük olasılıkla bu detayları **atlanabilirsiniz**. + +Bunlar daha çok OpenAPI 3.1.0’ın henüz mevcut olmadığı eski sürümler için geçerlidir. + +Bunu kısa bir OpenAPI ve JSON Schema **tarih dersi** gibi düşünebilirsiniz. 🤓 + +/// + +/// warning | Uyarı + +Bunlar **JSON Schema** ve **OpenAPI** standartları hakkında oldukça teknik detaylardır. + +Yukarıdaki fikirler sizin için zaten çalışıyorsa bu kadarı yeterli olabilir; muhtemelen bu detaylara ihtiyacınız yoktur, gönül rahatlığıyla atlayabilirsiniz. + +/// + +OpenAPI 3.1.0’dan önce OpenAPI, **JSON Schema**’nın daha eski ve değiştirilmiş bir sürümünü kullanıyordu. + +JSON Schema’da `examples` yoktu; bu yüzden OpenAPI, değiştirilmiş sürümüne kendi `example` alanını ekledi. + +OpenAPI ayrıca spesifikasyonun diğer bölümlerine de `example` ve `examples` alanlarını ekledi: + +* `Parameter Object` (spesifikasyonda) — FastAPI’de şunlar tarafından kullanılıyordu: + * `Path()` + * `Query()` + * `Header()` + * `Cookie()` +* `Request Body Object`; `content` alanında, `Media Type Object` üzerinde (spesifikasyonda) — FastAPI’de şunlar tarafından kullanılıyordu: + * `Body()` + * `File()` + * `Form()` + +/// info | Bilgi + +Bu eski OpenAPI’ye özel `examples` parametresi, FastAPI `0.103.0` sürümünden beri `openapi_examples` olarak kullanılıyor. + +/// + +### JSON Schema’nın `examples` alanı { #json-schemas-examples-field } + +Sonrasında JSON Schema, spesifikasyonun yeni bir sürümüne `examples` alanını ekledi. + +Ardından yeni OpenAPI 3.1.0, bu yeni `examples` alanını içeren en güncel sürümü (JSON Schema 2020-12) temel aldı. + +Ve artık, deprecated olan eski tekil (ve özel) `example` alanına kıyasla bu yeni `examples` alanı önceliklidir. + +JSON Schema’daki bu yeni `examples` alanı, OpenAPI’de başka yerlerde kullanılan (yukarıda anlatılan) metadata’lı `dict` yapısından farklı olarak **sadece örneklerden oluşan bir `list`**’tir. + +/// info | Bilgi + +OpenAPI 3.1.0, JSON Schema ile bu yeni ve daha basit entegrasyonla yayımlandıktan sonra bile bir süre, otomatik dokümantasyonu sağlayan araç Swagger UI OpenAPI 3.1.0’ı desteklemiyordu (5.0.0 sürümünden beri destekliyor 🎉). + +Bu nedenle, FastAPI’nin 0.99.0 öncesi sürümleri OpenAPI 3.1.0’dan daha düşük sürümleri kullanmaya devam etti. + +/// + +### Pydantic ve FastAPI `examples` { #pydantic-and-fastapi-examples } + +Bir Pydantic modelinin içine `schema_extra` ya da `Field(examples=["something"])` kullanarak `examples` eklediğinizde, bu örnek o Pydantic modelinin **JSON Schema**’sına eklenir. + +Ve Pydantic modelinin bu **JSON Schema**’sı, API’nizin **OpenAPI**’sine dahil edilir; ardından doküman arayüzünde kullanılır. + +FastAPI 0.99.0’dan önceki sürümlerde (0.99.0 ve üzeri daha yeni OpenAPI 3.1.0’ı kullanır) `Query()`, `Body()` vb. diğer araçlarla `example` veya `examples` kullandığınızda, bu örnekler o veriyi tanımlayan JSON Schema’ya (OpenAPI’nin kendi JSON Schema sürümüne bile) eklenmiyordu; bunun yerine doğrudan OpenAPI’deki *path operation* tanımına ekleniyordu (JSON Schema kullanan OpenAPI bölümlerinin dışında). + +Ancak artık FastAPI 0.99.0 ve üzeri OpenAPI 3.1.0 kullandığı (JSON Schema 2020-12) ve Swagger UI 5.0.0 ve üzeriyle birlikte, her şey daha tutarlı ve örnekler JSON Schema’ya dahil ediliyor. + +### Swagger UI ve OpenAPI’ye özel `examples` { #swagger-ui-and-openapi-specific-examples } + +Swagger UI (2023-08-26 itibarıyla) birden fazla JSON Schema örneğini desteklemediği için, kullanıcıların dokümanlarda birden fazla örnek göstermesi mümkün değildi. + +Bunu çözmek için FastAPI `0.103.0`, yeni `openapi_examples` parametresiyle aynı eski **OpenAPI’ye özel** `examples` alanını tanımlamayı **desteklemeye başladı**. 🤓 + +### Özet { #summary } + +Eskiden tarihten pek hoşlanmadığımı söylerdim... şimdi bakın, "teknoloji tarihi" dersi anlatıyorum. 😅 + +Kısacası, **FastAPI 0.99.0 veya üzerine yükseltin**; her şey çok daha **basit, tutarlı ve sezgisel** olur ve bu tarihsel detayların hiçbirini bilmeniz gerekmez. 😎 diff --git a/docs/tr/docs/tutorial/security/first-steps.md b/docs/tr/docs/tutorial/security/first-steps.md new file mode 100644 index 000000000..7e0a70a02 --- /dev/null +++ b/docs/tr/docs/tutorial/security/first-steps.md @@ -0,0 +1,203 @@ +# Güvenlik - İlk Adımlar { #security-first-steps } + +**backend** API’nizin bir domain’de olduğunu düşünelim. + +Ve başka bir domain’de ya da aynı domain’in farklı bir path’inde (veya bir mobil uygulamada) bir **frontend**’iniz var. + +Ve frontend’in, **username** ve **password** kullanarak backend ile kimlik doğrulaması yapabilmesini istiyorsunuz. + +Bunu **FastAPI** ile **OAuth2** kullanarak oluşturabiliriz. + +Ama ihtiyacınız olan küçük bilgi parçalarını bulmak için uzun spesifikasyonun tamamını okuma zahmetine girmeyelim. + +Güvenliği yönetmek için **FastAPI**’nin sunduğu araçları kullanalım. + +## Nasıl Görünüyor { #how-it-looks } + +Önce kodu kullanıp nasıl çalıştığına bakalım, sonra neler olup bittiğini anlamak için geri döneriz. + +## `main.py` Oluşturun { #create-main-py } + +Örneği `main.py` adlı bir dosyaya kopyalayın: + +{* ../../docs_src/security/tutorial001_an_py39.py *} + +## Çalıştırın { #run-it } + +/// info | Bilgi + +`python-multipart` paketi, `pip install "fastapi[standard]"` komutunu çalıştırdığınızda **FastAPI** ile birlikte otomatik olarak kurulur. + +Ancak `pip install fastapi` komutunu kullanırsanız, `python-multipart` paketi varsayılan olarak dahil edilmez. + +Elle kurmak için bir [virtual environment](../../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu aktive ettiğinizden emin olun ve ardından şununla kurun: + +```console +$ pip install python-multipart +``` + +Bunun nedeni, **OAuth2**’nin `username` ve `password` göndermek için "form data" kullanmasıdır. + +/// + +Örneği şu şekilde çalıştırın: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +## Kontrol Edin { #check-it } + +Etkileşimli dokümantasyona gidin: http://127.0.0.1:8000/docs. + +Şuna benzer bir şey göreceksiniz: + + + +/// check | Authorize butonu! + +Artık parıl parıl yeni bir "Authorize" butonunuz var. + +Ayrıca *path operation*’ınızın sağ üst köşesinde tıklayabileceğiniz küçük bir kilit simgesi de bulunuyor. + +/// + +Ve ona tıklarsanız, `username` ve `password` (ve diğer opsiyonel alanları) girebileceğiniz küçük bir yetkilendirme formu görürsünüz: + + + +/// note | Not + +Formda ne yazdığınızın önemi yok; şimdilik çalışmayacak. Ama birazdan oraya da geleceğiz. + +/// + +Bu, elbette son kullanıcılar için bir frontend değil; ancak tüm API’nizi etkileşimli şekilde belgelemek için harika bir otomatik araçtır. + +Frontend ekibi tarafından kullanılabilir (bu ekip siz de olabilirsiniz). + +Üçüncü taraf uygulamalar ve sistemler tarafından kullanılabilir. + +Ve aynı uygulamayı debug etmek, kontrol etmek ve test etmek için sizin tarafınızdan da kullanılabilir. + +## `password` Flow { #the-password-flow } + +Şimdi biraz geri dönüp bunların ne olduğuna bakalım. + +`password` "flow"u, OAuth2’de güvenlik ve authentication’ı yönetmek için tanımlanmış yöntemlerden ("flow"lardan) biridir. + +OAuth2, backend’in veya API’nin, kullanıcıyı authenticate eden server’dan bağımsız olabilmesi için tasarlanmıştır. + +Ancak bu örnekte, aynı **FastAPI** uygulaması hem API’yi hem de authentication’ı yönetecek. + +O yüzden basitleştirilmiş bu bakış açısından üzerinden geçelim: + +* Kullanıcı frontend’de `username` ve `password` yazar ve `Enter`’a basar. +* Frontend (kullanıcının browser’ında çalışır), bu `username` ve `password` değerlerini API’mizdeki belirli bir URL’ye gönderir (`tokenUrl="token"` ile tanımlanan). +* API, `username` ve `password` değerlerini kontrol eder ve bir "token" ile response döner (henüz bunların hiçbirini implement etmedik). + * "Token", daha sonra bu kullanıcıyı doğrulamak için kullanabileceğimiz içerik taşıyan bir string’dir. + * Normalde token’ın bir süre sonra süresi dolacak şekilde ayarlanması beklenir. + * Böylece kullanıcının bir noktada tekrar giriş yapması gerekir. + * Ayrıca token çalınırsa risk daha düşük olur. Çoğu durumda, sonsuza kadar çalışacak kalıcı bir anahtar gibi değildir. +* Frontend bu token’ı geçici olarak bir yerde saklar. +* Kullanıcı frontend’de tıklayarak web uygulamasının başka bir bölümüne gider. +* Frontend’in API’den daha fazla veri alması gerekir. + * Ancak o endpoint için authentication gereklidir. + * Bu yüzden API’mizle authenticate olmak için `Authorization` header’ını, `Bearer ` + token değeriyle gönderir. + * Token `foobar` içeriyorsa `Authorization` header’ının içeriği `Bearer foobar` olur. + +## **FastAPI**’nin `OAuth2PasswordBearer`’ı { #fastapis-oauth2passwordbearer } + +**FastAPI**, bu güvenlik özelliklerini implement etmek için farklı soyutlama seviyelerinde çeşitli araçlar sağlar. + +Bu örnekte **OAuth2**’yi, **Password** flow ile, **Bearer** token kullanarak uygulayacağız. Bunu `OAuth2PasswordBearer` sınıfı ile yaparız. + +/// info | Bilgi + +"Bearer" token tek seçenek değildir. + +Ama bizim kullanım senaryomuz için en iyi seçenek odur. + +Ayrıca bir OAuth2 uzmanı değilseniz ve ihtiyaçlarınıza daha uygun başka bir seçeneğin neden gerekli olduğunu net olarak bilmiyorsanız, çoğu kullanım senaryosu için de en uygun seçenek olacaktır. + +Bu durumda bile **FastAPI**, onu oluşturabilmeniz için gereken araçları sunar. + +/// + +`OAuth2PasswordBearer` sınıfının bir instance’ını oluştururken `tokenUrl` parametresini veririz. Bu parametre, client’ın (kullanıcının browser’ında çalışan frontend’in) token almak için `username` ve `password` göndereceği URL’yi içerir. + +{* ../../docs_src/security/tutorial001_an_py39.py hl[8] *} + +/// tip | İpucu + +Burada `tokenUrl="token"`, henüz oluşturmadığımız göreli bir URL olan `token`’ı ifade eder. Göreli URL olduğu için `./token` ile eşdeğerdir. + +Göreli URL kullandığımız için, API’niz `https://example.com/` adresinde olsaydı `https://example.com/token` anlamına gelirdi. Ama API’niz `https://example.com/api/v1/` adresinde olsaydı, bu kez `https://example.com/api/v1/token` anlamına gelirdi. + +Göreli URL kullanmak, [Behind a Proxy](../../advanced/behind-a-proxy.md){.internal-link target=_blank} gibi daha ileri kullanım senaryolarında bile uygulamanızın çalışmaya devam etmesini garanti etmek açısından önemlidir. + +/// + +Bu parametre o endpoint’i / *path operation*’ı oluşturmaz; fakat `/token` URL’sinin client’ın token almak için kullanması gereken URL olduğunu bildirir. Bu bilgi OpenAPI’de, dolayısıyla etkileşimli API dokümantasyon sistemlerinde kullanılır. + +Birazdan gerçek path operation’ı da oluşturacağız. + +/// info | Teknik Detaylar + +Eğer çok katı bir "Pythonista" iseniz, `token_url` yerine `tokenUrl` şeklindeki parametre adlandırma stilini sevmeyebilirsiniz. + +Bunun nedeni, OpenAPI spesifikasyonundaki isimle aynı adın kullanılmasıdır. Böylece bu güvenlik şemalarından herhangi biri hakkında daha fazla araştırma yapmanız gerekirse, adı kopyalayıp yapıştırarak kolayca daha fazla bilgi bulabilirsiniz. + +/// + +`oauth2_scheme` değişkeni, `OAuth2PasswordBearer`’ın bir instance’ıdır; ama aynı zamanda "callable"dır. + +Şu şekilde çağrılabilir: + +```Python +oauth2_scheme(some, parameters) +``` + +Dolayısıyla `Depends` ile kullanılabilir. + +### Kullanın { #use-it } + +Artık `Depends` ile bir dependency olarak `oauth2_scheme`’i geçebilirsiniz. + +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} + +Bu dependency, *path operation function* içindeki `token` parametresine atanacak bir `str` sağlar. + +**FastAPI**, bu dependency’yi OpenAPI şemasında (ve otomatik API dokümanlarında) bir "security scheme" tanımlamak için kullanabileceğini bilir. + +/// info | Teknik Detaylar + +**FastAPI**, bir dependency içinde tanımlanan `OAuth2PasswordBearer` sınıfını OpenAPI’de security scheme tanımlamak için kullanabileceğini bilir; çünkü bu sınıf `fastapi.security.oauth2.OAuth2`’den kalıtım alır, o da `fastapi.security.base.SecurityBase`’den kalıtım alır. + +OpenAPI (ve otomatik API dokümanları) ile entegre olan tüm security araçları `SecurityBase`’den kalıtım alır; **FastAPI** bu sayede onları OpenAPI’ye nasıl entegre edeceğini anlayabilir. + +/// + +## Ne Yapar { #what-it-does } + +Request içinde `Authorization` header’ını arar, değerin `Bearer ` + bir token olup olmadığını kontrol eder ve token’ı `str` olarak döndürür. + +Eğer `Authorization` header’ını görmezse ya da değer `Bearer ` token’ı içermiyorsa, doğrudan 401 status code hatasıyla (`UNAUTHORIZED`) response döner. + +Token’ın var olup olmadığını kontrol edip ayrıca hata döndürmenize bile gerek yoktur. Fonksiyonunuz çalışıyorsa, token içinde bir `str` olacağından emin olabilirsiniz. + +Bunu şimdiden etkileşimli dokümanlarda deneyebilirsiniz: + + + +Henüz token’ın geçerliliğini doğrulamıyoruz, ama başlangıç için bu bile yeterli. + +## Özet { #recap } + +Yani sadece 3 veya 4 ekstra satırla, şimdiden ilkel de olsa bir güvenlik katmanı elde etmiş oldunuz. diff --git a/docs/tr/docs/tutorial/security/get-current-user.md b/docs/tr/docs/tutorial/security/get-current-user.md new file mode 100644 index 000000000..9f56c7628 --- /dev/null +++ b/docs/tr/docs/tutorial/security/get-current-user.md @@ -0,0 +1,105 @@ +# Mevcut Kullanıcıyı Alma { #get-current-user } + +Önceki bölümde güvenlik sistemi (dependency injection sistemine dayanır) *path operation function*'a `str` olarak bir `token` veriyordu: + +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} + +Ancak bu hâlâ pek kullanışlı değil. + +Bize mevcut kullanıcıyı verecek şekilde düzenleyelim. + +## Bir kullanıcı modeli oluşturun { #create-a-user-model } + +Önce bir Pydantic kullanıcı modeli oluşturalım. + +Body'leri bildirmek için Pydantic'i nasıl kullanıyorsak, aynı şekilde onu başka her yerde de kullanabiliriz: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *} + +## `get_current_user` dependency'si oluşturun { #create-a-get-current-user-dependency } + +Bir `get_current_user` dependency'si oluşturalım. + +Dependency'lerin alt dependency'leri olabileceğini hatırlıyor musunuz? + +`get_current_user`, daha önce oluşturduğumuz `oauth2_scheme` ile aynı dependency'yi kullanacak. + +Daha önce *path operation* içinde doğrudan yaptığımız gibi, yeni dependency'miz `get_current_user`, alt dependency olan `oauth2_scheme` üzerinden `str` olarak bir `token` alacak: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *} + +## Kullanıcıyı alın { #get-the-user } + +`get_current_user`, oluşturduğumuz (sahte) bir yardımcı (utility) fonksiyonu kullanacak; bu fonksiyon `str` olarak bir token alır ve Pydantic `User` modelimizi döndürür: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *} + +## Mevcut kullanıcıyı enjekte edin { #inject-the-current-user } + +Artık *path operation* içinde `get_current_user` ile aynı `Depends` yaklaşımını kullanabiliriz: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} + +`current_user` tipini Pydantic `User` modeli olarak belirttiğimize dikkat edin. + +Bu sayede fonksiyonun içinde otomatik tamamlama ve tip kontrollerinin tamamından faydalanırız. + +/// tip | İpucu + +Request body'lerinin de Pydantic modelleri ile bildirildiğini hatırlıyor olabilirsiniz. + +Burada `Depends` kullandığınız için **FastAPI** karışıklık yaşamaz. + +/// + +/// check | Ek bilgi + +Bu dependency sisteminin tasarımı, hepsi `User` modeli döndüren farklı dependency'lere (farklı "dependable"lara) sahip olmamıza izin verir. + +Bu tipte veri döndürebilen yalnızca tek bir dependency ile sınırlı değiliz. + +/// + +## Diğer modeller { #other-models } + +Artık *path operation function* içinde mevcut kullanıcıyı doğrudan alabilir ve güvenlik mekanizmalarını `Depends` kullanarak **Dependency Injection** seviyesinde yönetebilirsiniz. + +Ayrıca güvenlik gereksinimleri için herhangi bir model veya veri kullanabilirsiniz (bu örnekte bir Pydantic `User` modeli). + +Ancak belirli bir data model, class ya da type kullanmak zorunda değilsiniz. + +Modelinizde bir `id` ve `email` olsun, ama `username` olmasın mı istiyorsunuz? Elbette. Aynı araçları kullanabilirsiniz. + +Sadece bir `str` mı istiyorsunuz? Ya da sadece bir `dict`? Veya doğrudan bir veritabanı class model instance'ı? Hepsi aynı şekilde çalışır. + +Uygulamanıza giriş yapan kullanıcılar yok da robotlar, botlar veya yalnızca bir access token'a sahip başka sistemler mi var? Yine, her şey aynı şekilde çalışır. + +Uygulamanız için neye ihtiyacınız varsa o türden bir model, class ve veritabanı kullanın. **FastAPI**, dependency injection sistemiyle bunları destekler. + +## Kod boyutu { #code-size } + +Bu örnek biraz uzun görünebilir. Güvenlik, data model'ler, utility fonksiyonlar ve *path operation*'ları aynı dosyada bir araya getirdiğimizi unutmayın. + +Ama kritik nokta şu: + +Güvenlik ve dependency injection tarafını bir kez yazarsınız. + +İstediğiniz kadar karmaşık hâle getirebilirsiniz. Yine de hepsi tek bir yerde ve sadece bir kez yazılmış olur. Üstelik tüm esneklikle. + +Sonrasında aynı güvenlik sistemini kullanan binlerce endpoint (*path operation*) olabilir. + +Ve bunların hepsi (ya da istediğiniz bir kısmı) bu dependency'leri veya oluşturacağınız başka dependency'leri yeniden kullanmaktan faydalanabilir. + +Hatta bu binlerce *path operation* 3 satır kadar kısa olabilir: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} + +## Özet { #recap } + +Artık *path operation function* içinde mevcut kullanıcıyı doğrudan alabilirsiniz. + +Yolun yarısına geldik. + +Kullanıcının/istemcinin gerçekten `username` ve `password` göndermesini sağlayacak bir *path operation* eklememiz gerekiyor. + +Sırada bu var. diff --git a/docs/tr/docs/tutorial/security/index.md b/docs/tr/docs/tutorial/security/index.md new file mode 100644 index 000000000..a592db6df --- /dev/null +++ b/docs/tr/docs/tutorial/security/index.md @@ -0,0 +1,106 @@ +# Güvenlik { #security } + +Güvenlik, authentication ve authorization’ı ele almanın birçok yolu vardır. + +Ve bu konu genellikle karmaşık ve "zor"dur. + +Birçok framework ve sistemde yalnızca security ve authentication’ı yönetmek bile ciddi miktarda emek ve kod gerektirir (çoğu durumda yazılan toplam kodun %50’si veya daha fazlası olabilir). + +**FastAPI**, tüm security spesifikasyonlarını baştan sona inceleyip öğrenmek zorunda kalmadan **Security** konusunu kolay, hızlı ve standart bir şekilde ele almanıza yardımcı olacak çeşitli araçlar sunar. + +Ama önce, küçük birkaç kavrama bakalım. + +## Acelem var? { #in-a-hurry } + +Bu terimlerin hiçbirini umursamıyorsanız ve sadece kullanıcı adı ve parola tabanlı authentication ile security’yi *hemen şimdi* eklemeniz gerekiyorsa, bir sonraki bölümlere geçin. + +## OAuth2 { #oauth2 } + +OAuth2, authentication ve authorization’ı yönetmek için çeşitli yöntemleri tanımlayan bir spesifikasyondur. + +Oldukça kapsamlı bir spesifikasyondur ve birkaç karmaşık use case’i kapsar. + +"Üçüncü taraf" kullanarak authentication yapmanın yollarını da içerir. + +"Facebook, Google, X (Twitter), GitHub ile giriş yap" bulunan sistemlerin arka planda kullandığı şey de budur. + +### OAuth 1 { #oauth-1 } + +OAuth 1 de vardı; OAuth2’den çok farklıdır ve daha karmaşıktır, çünkü iletişimi nasıl şifreleyeceğinize dair doğrudan spesifikasyonlar içeriyordu. + +Günümüzde pek popüler değildir veya pek kullanılmaz. + +OAuth2 ise iletişimin nasıl şifreleneceğini belirtmez; uygulamanızın HTTPS ile sunulmasını bekler. + +/// tip | İpucu + +**deployment** bölümünde Traefik ve Let's Encrypt kullanarak ücretsiz şekilde HTTPS’i nasıl kuracağınızı göreceksiniz. + +/// + +## OpenID Connect { #openid-connect } + +OpenID Connect, **OAuth2** tabanlı başka bir spesifikasyondur. + +OAuth2’de nispeten belirsiz kalan bazı noktaları tanımlayarak onu daha birlikte çalışabilir (interoperable) hâle getirmeye çalışır. + +Örneğin, Google ile giriş OpenID Connect kullanır (arka planda OAuth2 kullanır). + +Ancak Facebook ile giriş OpenID Connect’i desteklemez. Kendine özgü bir OAuth2 çeşidi vardır. + +### OpenID ("OpenID Connect" değil) { #openid-not-openid-connect } + +Bir de "OpenID" spesifikasyonu vardı. Bu da **OpenID Connect** ile aynı problemi çözmeye çalışıyordu ama OAuth2 tabanlı değildi. + +Dolayısıyla tamamen ayrı, ek bir sistemdi. + +Günümüzde pek popüler değildir veya pek kullanılmaz. + +## OpenAPI { #openapi } + +OpenAPI (önceden Swagger olarak biliniyordu), API’ler inşa etmek için açık spesifikasyondur (artık Linux Foundation’ın bir parçası). + +**FastAPI**, **OpenAPI** tabanlıdır. + +Bu sayede birden fazla otomatik etkileşimli dokümantasyon arayüzü, code generation vb. mümkün olur. + +OpenAPI, birden fazla security "scheme" tanımlamanın bir yolunu sunar. + +Bunları kullanarak, etkileşimli dokümantasyon sistemleri de dahil olmak üzere tüm bu standart tabanlı araçlardan faydalanabilirsiniz. + +OpenAPI şu security scheme’lerini tanımlar: + +* `apiKey`: uygulamaya özel bir anahtar; şuradan gelebilir: + * Bir query parameter. + * Bir header. + * Bir cookie. +* `http`: standart HTTP authentication sistemleri, örneğin: + * `bearer`: `Authorization` header’ı; değeri `Bearer ` + bir token olacak şekilde. Bu, OAuth2’den gelir. + * HTTP Basic authentication. + * HTTP Digest, vb. +* `oauth2`: OAuth2 ile security’yi yönetmenin tüm yolları ("flow" olarak adlandırılır). + * Bu flow’ların birçoğu, bir OAuth 2.0 authentication provider (Google, Facebook, X (Twitter), GitHub vb.) oluşturmak için uygundur: + * `implicit` + * `clientCredentials` + * `authorizationCode` + * Ancak, aynı uygulamanın içinde doğrudan authentication yönetmek için mükemmel şekilde kullanılabilecek özel bir "flow" vardır: + * `password`: sonraki bazı bölümlerde bunun örnekleri ele alınacak. +* `openIdConnect`: OAuth2 authentication verisinin otomatik olarak nasıl keşfedileceğini tanımlamanın bir yolunu sunar. + * Bu otomatik keşif, OpenID Connect spesifikasyonunda tanımlanan şeydir. + + +/// tip | İpucu + +Google, Facebook, X (Twitter), GitHub vb. gibi diğer authentication/authorization provider’larını entegre etmek de mümkündür ve nispeten kolaydır. + +En karmaşık kısım, bu tür bir authentication/authorization provider’ı inşa etmektir; ancak **FastAPI** ağır işleri sizin yerinize yaparken bunu kolayca yapabilmeniz için araçlar sunar. + +/// + +## **FastAPI** yardımcı araçları { #fastapi-utilities } + +FastAPI, `fastapi.security` modülünde bu security scheme’lerinin her biri için, bu mekanizmaları kullanmayı kolaylaştıran çeşitli araçlar sağlar. + +Sonraki bölümlerde, **FastAPI**’nin sunduğu bu araçları kullanarak API’nize nasıl security ekleyeceğinizi göreceksiniz. + +Ayrıca bunun etkileşimli dokümantasyon sistemine nasıl otomatik olarak entegre edildiğini de göreceksiniz. diff --git a/docs/tr/docs/tutorial/security/oauth2-jwt.md b/docs/tr/docs/tutorial/security/oauth2-jwt.md new file mode 100644 index 000000000..716761157 --- /dev/null +++ b/docs/tr/docs/tutorial/security/oauth2-jwt.md @@ -0,0 +1,273 @@ +# Password ile OAuth2 (ve hashing), JWT token'ları ile Bearer { #oauth2-with-password-and-hashing-bearer-with-jwt-tokens } + +Artık tüm security flow elimizde olduğuna göre, uygulamayı gerçekten güvenli hâle getirelim: JWT token'ları ve güvenli password hashing kullanacağız. + +Bu kodu uygulamanızda gerçekten kullanabilirsiniz; password hash'lerini veritabanınıza kaydedebilirsiniz, vb. + +Bir önceki bölümde bıraktığımız yerden başlayıp üzerine ekleyerek ilerleyeceğiz. + +## JWT Hakkında { #about-jwt } + +JWT, "JSON Web Tokens" anlamına gelir. + +Bir JSON nesnesini, boşluk içermeyen uzun ve yoğun bir string'e kodlamak için kullanılan bir standarttır. Şuna benzer: + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c +``` + +Şifrelenmiş değildir; yani herkes içeriğindeki bilgiyi geri çıkarabilir. + +Ancak imzalanmıştır. Bu yüzden, sizin ürettiğiniz bir token'ı aldığınızda, gerçekten onu sizin ürettiğinizi doğrulayabilirsiniz. + +Bu şekilde, örneğin 1 haftalık süre sonu (expiration) olan bir token oluşturabilirsiniz. Sonra kullanıcı ertesi gün token ile geri geldiğinde, kullanıcının hâlâ sisteminizde oturum açmış olduğunu bilirsiniz. + +Bir hafta sonra token'ın süresi dolar; kullanıcı yetkilendirilmez ve yeni bir token almak için tekrar giriş yapmak zorunda kalır. Ayrıca kullanıcı (veya üçüncü bir taraf) token'ı değiştirip süre sonunu farklı göstermek isterse bunu tespit edebilirsiniz; çünkü imzalar eşleşmez. + +JWT token'larıyla oynayıp nasıl çalıştıklarını görmek isterseniz https://jwt.io adresine bakın. + +## `PyJWT` Kurulumu { #install-pyjwt } + +Python'da JWT token'larını üretmek ve doğrulamak için `PyJWT` kurmamız gerekiyor. + +Bir [virtual environment](../../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan emin olun, aktif edin ve ardından `pyjwt` kurun: + +
+ +```console +$ pip install pyjwt + +---> 100% +``` + +
+ +/// info | Bilgi + +RSA veya ECDSA gibi dijital imza algoritmaları kullanmayı planlıyorsanız, `pyjwt[crypto]` bağımlılığı olan `cryptography` kütüphanesini kurmalısınız. + +Daha fazla bilgi için PyJWT Installation docs sayfasını okuyabilirsiniz. + +/// + +## Password hashing { #password-hashing } + +"Hashing", bazı içerikleri (bu örnekte bir password) anlamsız görünen bir bayt dizisine (pratikte bir string) dönüştürmek demektir. + +Aynı içeriği (aynı password'ü) her seferinde verirseniz, her seferinde aynı anlamsız çıktıyı elde edersiniz. + +Ancak bu anlamsız çıktıdan geri password'e dönüştürme yapılamaz. + +### Neden password hashing kullanılır { #why-use-password-hashing } + +Veritabanınız çalınırsa, hırsız kullanıcılarınızın düz metin (plaintext) password'lerini değil, sadece hash'leri elde eder. + +Dolayısıyla, o password'ü başka bir sistemde denemek kolay olmaz (pek çok kullanıcı her yerde aynı password'ü kullandığı için bu tehlikeli olurdu). + +## `pwdlib` Kurulumu { #install-pwdlib } + +pwdlib, password hash'leriyle çalışmak için çok iyi bir Python paketidir. + +Birçok güvenli hashing algoritmasını ve bunlarla çalışmak için yardımcı araçları destekler. + +Önerilen algoritma "Argon2"dir. + +Bir [virtual environment](../../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan emin olun, aktif edin ve sonra Argon2 ile birlikte pwdlib'i kurun: + +
+ +```console +$ pip install "pwdlib[argon2]" + +---> 100% +``` + +
+ +/// tip | İpucu + +`pwdlib` ile, **Django** tarafından oluşturulmuş password'leri, bir **Flask** security eklentisinin ürettiklerini veya başka birçok kaynaktan gelen password'leri okuyabilecek şekilde bile yapılandırabilirsiniz. + +Böylece örneğin bir Django uygulamasındaki verileri aynı veritabanında bir FastAPI uygulamasıyla paylaşabilirsiniz. Ya da aynı veritabanını kullanarak bir Django uygulamasını kademeli şekilde taşıyabilirsiniz. + +Ayrıca kullanıcılarınız, aynı anda hem Django uygulamanızdan hem de **FastAPI** uygulamanızdan login olabilir. + +/// + +## Password'leri hash'leme ve doğrulama { #hash-and-verify-the-passwords } + +Gerekli araçları `pwdlib` içinden import edelim. + +Önerilen ayarlarla bir PasswordHash instance'ı oluşturalım; bunu password'leri hash'lemek ve doğrulamak için kullanacağız. + +/// tip | İpucu + +pwdlib, bcrypt hashing algoritmasını da destekler; ancak legacy algoritmaları içermez. Eski hash'lerle çalışmak için passlib kütüphanesini kullanmanız önerilir. + +Örneğin, başka bir sistemin (Django gibi) ürettiği password'leri okuyup doğrulayabilir, ancak yeni password'leri Argon2 veya Bcrypt gibi farklı bir algoritmayla hash'leyebilirsiniz. + +Ve aynı anda hepsiyle uyumlu kalabilirsiniz. + +/// + +Kullanıcıdan gelen password'ü hash'lemek için bir yardımcı (utility) fonksiyon oluşturalım. + +Sonra, alınan password'ün kayıttaki hash ile eşleşip eşleşmediğini doğrulayan başka bir yardımcı fonksiyon yazalım. + +Bir tane de kullanıcıyı authenticate edip geri döndüren bir yardımcı fonksiyon ekleyelim. + +{* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,56:57,60:61,70:76] *} + +/// note | Not + +Yeni (sahte) veritabanı `fake_users_db`'ye bakarsanız, hash'lenmiş password'ün artık nasıl göründüğünü görebilirsiniz: `"$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc"`. + +/// + +## JWT token'larını yönetme { #handle-jwt-tokens } + +Kurulu modülleri import edelim. + +JWT token'larını imzalamak için kullanılacak rastgele bir secret key oluşturalım. + +Güvenli, rastgele bir secret key üretmek için şu komutu kullanın: + +
+ +```console +$ openssl rand -hex 32 + +09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7 +``` + +
+ +Çıktıyı `SECRET_KEY` değişkenine kopyalayın (örnektekini kullanmayın). + +JWT token'ını imzalamak için kullanılan algoritmayı tutacak `ALGORITHM` adlı bir değişken oluşturup değerini `"HS256"` yapın. + +Token'ın süre sonu (expiration) için bir değişken oluşturun. + +Response için token endpoint'inde kullanılacak bir Pydantic Model tanımlayın. + +Yeni bir access token üretmek için bir yardımcı fonksiyon oluşturun. + +{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *} + +## Dependency'leri güncelleme { #update-the-dependencies } + +`get_current_user` fonksiyonunu, öncekiyle aynı token'ı alacak şekilde güncelleyelim; ancak bu sefer JWT token'larını kullanacağız. + +Gelen token'ı decode edin, doğrulayın ve mevcut kullanıcıyı döndürün. + +Token geçersizse, hemen bir HTTP hatası döndürün. + +{* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *} + +## `/token` *path operation*'ını güncelleme { #update-the-token-path-operation } + +Token'ın süre sonu için bir `timedelta` oluşturun. + +Gerçek bir JWT access token üretip döndürün. + +{* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *} + +### JWT "subject" `sub` Hakkında Teknik Detaylar { #technical-details-about-the-jwt-subject-sub } + +JWT spesifikasyonu, token'ın konusu (subject) için `sub` adlı bir anahtar olduğunu söyler. + +Bunu kullanmak zorunlu değildir; ancak kullanıcı kimliğini koymak için uygun yer burasıdır, bu yüzden burada onu kullanıyoruz. + +JWT, sadece bir kullanıcıyı tanımlamak ve API'nizde doğrudan işlem yapmasına izin vermek dışında başka amaçlarla da kullanılabilir. + +Örneğin bir "araba"yı veya bir "blog post"u tanımlayabilirsiniz. + +Sonra o varlık için izinler ekleyebilirsiniz; örneğin (araba için) "drive" ya da (blog için) "edit". + +Ardından bu JWT token'ını bir kullanıcıya (veya bot'a) verebilirsiniz; onlar da, hesapları olmasına bile gerek kalmadan, sadece API'nizin bunun için ürettiği JWT token'ıyla bu aksiyonları gerçekleştirebilir (arabayı sürmek veya blog post'u düzenlemek gibi). + +Bu fikirlerle JWT, çok daha gelişmiş senaryolarda kullanılabilir. + +Bu durumlarda, birden fazla varlığın aynı ID'ye sahip olması mümkündür; örneğin `foo` (kullanıcı `foo`, araba `foo`, blog post `foo`). + +Dolayısıyla ID çakışmalarını önlemek için, kullanıcı için JWT token oluştururken `sub` anahtarının değerine bir önek ekleyebilirsiniz; örneğin `username:`. Bu örnekte `sub` değeri şöyle olabilirdi: `username:johndoe`. + +Unutmamanız gereken önemli nokta şudur: `sub` anahtarı, tüm uygulama genelinde benzersiz bir tanımlayıcı olmalı ve string olmalıdır. + +## Kontrol Edelim { #check-it } + +Server'ı çalıştırın ve docs'a gidin: http://127.0.0.1:8000/docs. + +Şuna benzer bir arayüz göreceksiniz: + + + +Uygulamayı, öncekiyle aynı şekilde authorize edin. + +Şu kimlik bilgilerini kullanarak: + +Username: `johndoe` +Password: `secret` + +/// check | Ek bilgi + +Kodun hiçbir yerinde düz metin password "`secret`" yok; sadece hash'lenmiş hâli var. + +/// + + + +`/users/me/` endpoint'ini çağırın; response şöyle olacaktır: + +```JSON +{ + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + "disabled": false +} +``` + + + +Developer tools'u açarsanız, gönderilen verinin sadece token'ı içerdiğini görebilirsiniz. Password sadece kullanıcıyı authenticate edip access token almak için yapılan ilk request'te gönderilir, sonrasında gönderilmez: + + + +/// note | Not + +`Authorization` header'ına dikkat edin; değeri `Bearer ` ile başlıyor. + +/// + +## `scopes` ile İleri Seviye Kullanım { #advanced-usage-with-scopes } + +OAuth2'nin "scopes" kavramı vardır. + +Bunları kullanarak bir JWT token'a belirli bir izin seti ekleyebilirsiniz. + +Sonra bu token'ı bir kullanıcıya doğrudan veya bir üçüncü tarafa verip, API'nizle belirli kısıtlarla etkileşime girmesini sağlayabilirsiniz. + +Nasıl kullanıldıklarını ve **FastAPI** ile nasıl entegre olduklarını, ileride **Advanced User Guide** içinde öğreneceksiniz. + +## Özet { #recap } + +Şimdiye kadar gördüklerinizle, OAuth2 ve JWT gibi standartları kullanarak güvenli bir **FastAPI** uygulaması kurabilirsiniz. + +Neredeyse her framework'te security'yi ele almak oldukça hızlı bir şekilde karmaşık bir konu hâline gelir. + +Bunu çok basitleştiren birçok paket, veri modeli, veritabanı ve mevcut özelliklerle ilgili pek çok ödün vermek zorunda kalır. Hatta bazıları işi aşırı basitleştirirken arka planda güvenlik açıkları da barındırır. + +--- + +**FastAPI**, hiçbir veritabanı, veri modeli veya araç konusunda ödün vermez. + +Projenize en uygun olanları seçebilmeniz için size tam esneklik sağlar. + +Ayrıca `pwdlib` ve `PyJWT` gibi iyi bakımı yapılan ve yaygın kullanılan paketleri doğrudan kullanabilirsiniz; çünkü **FastAPI**, haricî paketleri entegre etmek için karmaşık mekanizmalara ihtiyaç duymaz. + +Buna rağmen, esneklikten, sağlamlıktan veya güvenlikten ödün vermeden süreci mümkün olduğunca basitleştiren araçları sağlar. + +Ve OAuth2 gibi güvenli, standart protokolleri nispeten basit bir şekilde kullanabilir ve uygulayabilirsiniz. + +Aynı standartları izleyerek, daha ince taneli (fine-grained) bir izin sistemi için OAuth2 "scopes" kullanımını **Advanced User Guide** içinde daha detaylı öğrenebilirsiniz. Scopes'lu OAuth2; Facebook, Google, GitHub, Microsoft, X (Twitter) vb. pek çok büyük kimlik doğrulama sağlayıcısının, üçüncü taraf uygulamaların kullanıcıları adına API'leriyle etkileşebilmesine izin vermek için kullandığı mekanizmadır. diff --git a/docs/tr/docs/tutorial/security/simple-oauth2.md b/docs/tr/docs/tutorial/security/simple-oauth2.md new file mode 100644 index 000000000..88efd98e5 --- /dev/null +++ b/docs/tr/docs/tutorial/security/simple-oauth2.md @@ -0,0 +1,289 @@ +# Password ve Bearer ile Basit OAuth2 { #simple-oauth2-with-password-and-bearer } + +Şimdi önceki bölümün üzerine inşa edip, eksik parçaları ekleyerek tam bir güvenlik akışı oluşturalım. + +## `username` ve `password`’ü Alma { #get-the-username-and-password } + +`username` ve `password`’ü almak için **FastAPI** security yardımcı araçlarını kullanacağız. + +OAuth2, (bizim kullandığımız) "password flow" kullanılırken client/kullanıcının form verisi olarak `username` ve `password` alanlarını göndermesi gerektiğini belirtir. + +Ayrıca spesifikasyon, bu alanların adlarının tam olarak böyle olması gerektiğini söyler. Yani `user-name` veya `email` işe yaramaz. + +Ancak merak etmeyin, frontend’de son kullanıcılarınıza dilediğiniz gibi gösterebilirsiniz. + +Veritabanı model(ler)inizde de istediğiniz başka isimleri kullanabilirsiniz. + +Fakat login *path operation*’ı için, spesifikasyonla uyumlu olmak (ve örneğin entegre API dokümantasyon sistemini kullanabilmek) adına bu isimleri kullanmamız gerekiyor. + +Spesifikasyon ayrıca `username` ve `password`’ün form verisi olarak gönderilmesi gerektiğini de söyler (yani burada JSON yok). + +### `scope` { #scope } + +Spesifikasyon, client’ın "`scope`" adlı başka bir form alanı da gönderebileceğini söyler. + +Form alanının adı `scope`’tur (tekil), ama aslında boşluklarla ayrılmış "scope"’lardan oluşan uzun bir string’dir. + +Her bir "scope" sadece bir string’dir (boşluk içermez). + +Genelde belirli güvenlik izinlerini (permission) belirtmek için kullanılırlar, örneğin: + +* `users:read` veya `users:write` yaygın örneklerdir. +* `instagram_basic` Facebook / Instagram tarafından kullanılır. +* `https://www.googleapis.com/auth/drive` Google tarafından kullanılır. + +/// info | Bilgi + +OAuth2’de bir "scope", gerekli olan belirli bir izni ifade eden basit bir string’dir. + +`:` gibi başka karakterler içermesi veya URL olması önemli değildir. + +Bu detaylar implementasyon’a özeldir. + +OAuth2 açısından bunlar sadece string’lerdir. + +/// + +## `username` ve `password`’ü Almak İçin Kod { #code-to-get-the-username-and-password } + +Şimdi bunu yönetmek için **FastAPI**’nin sağladığı araçları kullanalım. + +### `OAuth2PasswordRequestForm` { #oauth2passwordrequestform } + +Önce `OAuth2PasswordRequestForm`’u import edin ve `/token` için *path operation* içinde `Depends` ile dependency olarak kullanın: + +{* ../../docs_src/security/tutorial003_an_py310.py hl[4,78] *} + +`OAuth2PasswordRequestForm`, şu alanları içeren bir form body tanımlayan bir class dependency’sidir: + +* `username`. +* `password`. +* Boşlukla ayrılmış string’lerden oluşan büyük bir string olarak opsiyonel `scope` alanı. +* Opsiyonel `grant_type`. + +/// tip | İpucu + +OAuth2 spesifikasyonu aslında `grant_type` alanını sabit bir `password` değeriyle *zorunlu kılar*, ancak `OAuth2PasswordRequestForm` bunu zorlamaz. + +Bunu zorlamak istiyorsanız, `OAuth2PasswordRequestForm` yerine `OAuth2PasswordRequestFormStrict` kullanın. + +/// + +* Opsiyonel `client_id` (bu örnekte ihtiyacımız yok). +* Opsiyonel `client_secret` (bu örnekte ihtiyacımız yok). + +/// info | Bilgi + +`OAuth2PasswordRequestForm`, `OAuth2PasswordBearer` gibi **FastAPI**’ye özel “özel bir sınıf” değildir. + +`OAuth2PasswordBearer`, bunun bir security scheme olduğunu **FastAPI**’ye bildirir. Bu yüzden OpenAPI’ye o şekilde eklenir. + +Ama `OAuth2PasswordRequestForm` sadece bir class dependency’dir; bunu kendiniz de yazabilirdiniz ya da doğrudan `Form` parametreleri tanımlayabilirdiniz. + +Fakat çok yaygın bir kullanım olduğu için **FastAPI** bunu işleri kolaylaştırmak adına doğrudan sağlar. + +/// + +### Form Verisini Kullanma { #use-the-form-data } + +/// tip | İpucu + +`OAuth2PasswordRequestForm` dependency class’ının instance’ında boşluklarla ayrılmış uzun string olarak bir `scope` attribute’u olmaz; bunun yerine gönderilen her scope için gerçek string listesini içeren `scopes` attribute’u olur. + +Bu örnekte `scopes` kullanmıyoruz, ama ihtiyacınız olursa bu özellik hazır. + +/// + +Şimdi form alanındaki `username`’i kullanarak (sahte) veritabanından kullanıcı verisini alın. + +Böyle bir kullanıcı yoksa, "Incorrect username or password" diyerek bir hata döndürelim. + +Hata için `HTTPException` exception’ını kullanıyoruz: + +{* ../../docs_src/security/tutorial003_an_py310.py hl[3,79:81] *} + +### Password’ü Kontrol Etme { #check-the-password } + +Bu noktada veritabanından kullanıcı verisine sahibiz, ancak password’ü henüz kontrol etmedik. + +Önce bu veriyi Pydantic `UserInDB` modeline koyalım. + +Asla düz metin (plaintext) password kaydetmemelisiniz; bu yüzden (sahte) password hashing sistemini kullanacağız. + +Password’ler eşleşmezse, aynı hatayı döndürürüz. + +#### Password hashing { #password-hashing } + +"Hashing" şudur: bir içeriği (bu örnekte password) anlaşılmaz görünen bayt dizisine (yani bir string’e) dönüştürmek. + +Aynı içeriği (aynı password’ü) her verdiğinizde, birebir aynı anlamsız görünen çıktıyı elde edersiniz. + +Ama bu anlamsız çıktıyı tekrar password’e geri çeviremezsiniz. + +##### Neden password hashing kullanılır { #why-use-password-hashing } + +Veritabanınız çalınırsa, hırsız kullanıcılarınızın düz metin password’lerine değil, sadece hash’lere sahip olur. + +Dolayısıyla hırsız, aynı password’leri başka bir sistemde denemeye çalışamaz (birçok kullanıcı her yerde aynı password’ü kullandığı için bu tehlikeli olurdu). + +{* ../../docs_src/security/tutorial003_an_py310.py hl[82:85] *} + +#### `**user_dict` Hakkında { #about-user-dict } + +`UserInDB(**user_dict)` şu anlama gelir: + +*`user_dict` içindeki key ve value’ları doğrudan key-value argümanları olarak geçir; şu ifadeyle eşdeğerdir:* + +```Python +UserInDB( + username = user_dict["username"], + email = user_dict["email"], + full_name = user_dict["full_name"], + disabled = user_dict["disabled"], + hashed_password = user_dict["hashed_password"], +) +``` + +/// info | Bilgi + +`**user_dict` için daha kapsamlı bir açıklama için [**Extra Models** dokümantasyonundaki ilgili bölüme](../extra-models.md#about-user-in-dict){.internal-link target=_blank} geri dönüp bakın. + +/// + +## Token’ı Döndürme { #return-the-token } + +`token` endpoint’inin response’u bir JSON object olmalıdır. + +Bir `token_type` içermelidir. Biz "Bearer" token’ları kullandığımız için token type "`bearer`" olmalıdır. + +Ayrıca `access_token` içermelidir; bunun değeri access token’ımızı içeren bir string olmalıdır. + +Bu basit örnekte tamamen güvensiz davranıp token olarak aynı `username`’i döndüreceğiz. + +/// tip | İpucu + +Bir sonraki bölümde, password hashing ve JWT token’ları ile gerçekten güvenli bir implementasyon göreceksiniz. + +Ama şimdilik ihtiyacımız olan spesifik detaylara odaklanalım. + +/// + +{* ../../docs_src/security/tutorial003_an_py310.py hl[87] *} + +/// tip | İpucu + +Spesifikasyona göre, bu örnekteki gibi `access_token` ve `token_type` içeren bir JSON döndürmelisiniz. + +Bunu kendi kodunuzda kendiniz yapmalı ve bu JSON key’lerini kullandığınızdan emin olmalısınız. + +Spesifikasyonlara uyum için, doğru yapmanız gereken neredeyse tek şey budur. + +Geri kalanını **FastAPI** sizin yerinize yönetir. + +/// + +## Dependency’leri Güncelleme { #update-the-dependencies } + +Şimdi dependency’lerimizi güncelleyeceğiz. + +`current_user`’ı *sadece* kullanıcı aktifse almak istiyoruz. + +Bu yüzden, `get_current_user`’ı dependency olarak kullanan ek bir dependency olan `get_current_active_user`’ı oluşturuyoruz. + +Bu iki dependency de kullanıcı yoksa veya pasifse sadece HTTP hatası döndürecek. + +Dolayısıyla endpoint’imizde kullanıcıyı ancak kullanıcı varsa, doğru şekilde authenticate edildiyse ve aktifse alacağız: + +{* ../../docs_src/security/tutorial003_an_py310.py hl[58:66,69:74,94] *} + +/// info | Bilgi + +Burada `Bearer` değerine sahip ek `WWW-Authenticate` header’ını döndürmemiz de spesifikasyonun bir parçasıdır. + +Herhangi bir HTTP (hata) durum kodu 401 "UNAUTHORIZED", ayrıca `WWW-Authenticate` header’ı da döndürmelidir. + +Bearer token’lar (bizim durumumuz) için bu header’ın değeri `Bearer` olmalıdır. + +Aslında bu ekstra header’ı atlayabilirsiniz, yine de çalışır. + +Ama spesifikasyonlara uyumlu olması için burada eklenmiştir. + +Ayrıca, bunu bekleyen ve kullanan araçlar olabilir (şimdi veya ileride) ve bu da sizin ya da kullanıcılarınız için faydalı olabilir. + +Standartların faydası da bu... + +/// + +## Çalışır Halini Görün { #see-it-in-action } + +Etkileşimli dokümanları açın: http://127.0.0.1:8000/docs. + +### Authenticate Olma { #authenticate } + +"Authorize" butonuna tıklayın. + +Şu bilgileri kullanın: + +User: `johndoe` + +Password: `secret` + + + +Sistemde authenticate olduktan sonra şöyle görürsünüz: + + + +### Kendi Kullanıcı Verinizi Alma { #get-your-own-user-data } + +Şimdi `/users/me` path’inde `GET` operasyonunu kullanın. + +Kullanıcınızın verisini şöyle alırsınız: + +```JSON +{ + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + "disabled": false, + "hashed_password": "fakehashedsecret" +} +``` + + + +Kilit ikonuna tıklayıp logout olursanız ve sonra aynı operasyonu tekrar denerseniz, şu şekilde bir HTTP 401 hatası alırsınız: + +```JSON +{ + "detail": "Not authenticated" +} +``` + +### Pasif Kullanıcı { #inactive-user } + +Şimdi pasif bir kullanıcıyla deneyin; şu bilgilerle authenticate olun: + +User: `alice` + +Password: `secret2` + +Ve `/users/me` path’inde `GET` operasyonunu kullanmayı deneyin. + +Şöyle bir "Inactive user" hatası alırsınız: + +```JSON +{ + "detail": "Inactive user" +} +``` + +## Özet { #recap } + +Artık API’niz için `username` ve `password` tabanlı, eksiksiz bir güvenlik sistemi implement etmek için gerekli araçlara sahipsiniz. + +Bu araçlarla güvenlik sistemini herhangi bir veritabanıyla ve herhangi bir user veya veri modeliyle uyumlu hale getirebilirsiniz. + +Eksik kalan tek detay, bunun henüz gerçekten "güvenli" olmamasıdır. + +Bir sonraki bölümde güvenli bir password hashing kütüphanesini ve JWT token’larını nasıl kullanacağınızı göreceksiniz. diff --git a/docs/tr/docs/tutorial/sql-databases.md b/docs/tr/docs/tutorial/sql-databases.md new file mode 100644 index 000000000..e1638cb04 --- /dev/null +++ b/docs/tr/docs/tutorial/sql-databases.md @@ -0,0 +1,357 @@ +# SQL (İlişkisel) Veritabanları { #sql-relational-databases } + +**FastAPI**, SQL (ilişkisel) bir veritabanı kullanmanızı zorunlu kılmaz. Ancak isterseniz **istediğiniz herhangi bir veritabanını** kullanabilirsiniz. + +Burada SQLModel kullanarak bir örnek göreceğiz. + +**SQLModel**, SQLAlchemy ve Pydantic’in üzerine inşa edilmiştir. **FastAPI**’nin yazarı tarafından, **SQL veritabanları** kullanması gereken FastAPI uygulamalarıyla mükemmel uyum sağlaması için geliştirilmiştir. + +/// tip | İpucu + +İstediğiniz başka bir SQL veya NoSQL veritabanı kütüphanesini kullanabilirsiniz (bazı durumlarda "ORMs" olarak adlandırılır). FastAPI sizi hiçbir şeye zorlamaz. + +/// + +SQLModel, SQLAlchemy tabanlı olduğu için SQLAlchemy’nin **desteklediği herhangi bir veritabanını** kolayca kullanabilirsiniz (bu da SQLModel tarafından da desteklendikleri anlamına gelir), örneğin: + +* PostgreSQL +* MySQL +* SQLite +* Oracle +* Microsoft SQL Server, vb. + +Bu örnekte **SQLite** kullanacağız; çünkü tek bir dosya kullanır ve Python’da yerleşik desteği vardır. Yani bu örneği kopyalayıp olduğu gibi çalıştırabilirsiniz. + +Daha sonra, production uygulamanız için **PostgreSQL** gibi bir veritabanı sunucusu kullanmak isteyebilirsiniz. + +/// tip | İpucu + +Frontend ve daha fazla araçla birlikte **FastAPI** + **PostgreSQL** içeren resmi bir proje oluşturucu (project generator) var: https://github.com/fastapi/full-stack-fastapi-template + +/// + +Bu çok basit ve kısa bir eğitimdir. Veritabanları genelinde, SQL hakkında veya daha ileri özellikler hakkında öğrenmek isterseniz SQLModel dokümantasyonuna gidin. + +## `SQLModel` Kurulumu { #install-sqlmodel } + +Önce [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan emin olun, aktive edin ve ardından `sqlmodel`’i yükleyin: + +
+ +```console +$ pip install sqlmodel +---> 100% +``` + +
+ +## Tek Model ile Uygulamayı Oluşturma { #create-the-app-with-a-single-model } + +Önce, tek bir **SQLModel** modeliyle uygulamanın en basit ilk sürümünü oluşturacağız. + +Aşağıda, **birden fazla model** kullanarak güvenliği ve esnekliği artırıp geliştireceğiz. + +### Modelleri Oluşturma { #create-models } + +`SQLModel`’i import edin ve bir veritabanı modeli oluşturun: + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:11] hl[7:11] *} + +`Hero` sınıfı, bir Pydantic modeline çok benzer (hatta altta aslında *bir Pydantic modelidir*). + +Birkaç fark var: + +* `table=True`, SQLModel’e bunun bir *table model* olduğunu söyler; SQL veritabanında bir **table**’ı temsil etmelidir, sadece bir *data model* değildir (diğer normal Pydantic sınıflarında olduğu gibi). + +* `Field(primary_key=True)`, SQLModel’e `id`’nin SQL veritabanındaki **primary key** olduğunu söyler (SQL primary key’leri hakkında daha fazlasını SQLModel dokümantasyonunda öğrenebilirsiniz). + + **Not:** primary key alanı için `int | None` kullanıyoruz; böylece Python kodunda *`id` olmadan bir nesne oluşturabiliriz* (`id=None`) ve veritabanının *kaydederken bunu üreteceğini* varsayarız. SQLModel, veritabanının `id` sağlayacağını anlar ve *veritabanı şemasında sütunu null olamayan bir `INTEGER`* olarak tanımlar. Detaylar için primary key’ler hakkında SQLModel dokümantasyonuna bakın. + +* `Field(index=True)`, SQLModel’e bu sütun için bir **SQL index** oluşturmasını söyler; bu da bu sütuna göre filtrelenmiş verileri okurken veritabanında daha hızlı arama yapılmasını sağlar. + + SQLModel, `str` olarak tanımlanan bir şeyin SQL tarafında `TEXT` (veya veritabanına bağlı olarak `VARCHAR`) tipinde bir sütun olacağını bilir. + +### Engine Oluşturma { #create-an-engine } + +Bir SQLModel `engine`’i (altta aslında bir SQLAlchemy `engine`’idir) veritabanına olan **bağlantıları tutan** yapıdır. + +Tüm kodunuzun aynı veritabanına bağlanması için **tek bir `engine` nesnesi** kullanırsınız. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *} + +`check_same_thread=False` kullanmak, FastAPI’nin aynı SQLite veritabanını farklı thread’lerde kullanmasına izin verir. Bu gereklidir; çünkü **tek bir request** **birden fazla thread** kullanabilir (örneğin dependency’lerde). + +Merak etmeyin; kodun yapısı gereği, ileride **her request için tek bir SQLModel *session*** kullandığımızdan emin olacağız. Zaten `check_same_thread` de temelde bunu mümkün kılmaya çalışır. + +### Table’ları Oluşturma { #create-the-tables } + +Sonra `SQLModel.metadata.create_all(engine)` kullanan bir fonksiyon ekleyerek tüm *table model*’ler için **table’ları oluştururuz**. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *} + +### Session Dependency’si Oluşturma { #create-a-session-dependency } + +Bir **`Session`**, **nesneleri memory’de** tutar ve verideki gerekli değişiklikleri takip eder; ardından veritabanıyla iletişim kurmak için **`engine` kullanır**. + +`yield` ile, her request için yeni bir `Session` sağlayacak bir FastAPI **dependency** oluşturacağız. Bu da her request’te tek session kullanmamızı garanti eder. + +Ardından bu dependency’yi kullanacak kodun geri kalanını sadeleştirmek için `Annotated` ile `SessionDep` dependency’sini oluştururuz. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *} + +### Startup’ta Veritabanı Table’larını Oluşturma { #create-database-tables-on-startup } + +Uygulama başlarken veritabanı table’larını oluşturacağız. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[32:37] hl[35:37] *} + +Burada bir uygulama startup event’inde table’ları oluşturuyoruz. + +Production’da büyük ihtimalle uygulamayı başlatmadan önce çalışan bir migration script’i kullanırsınız. + +/// tip | İpucu + +SQLModel, Alembic’i saran migration araçlarına sahip olacak; ancak şimdilik Alembic’i doğrudan kullanabilirsiniz. + +/// + +### Hero Oluşturma { #create-a-hero } + +Her SQLModel modeli aynı zamanda bir Pydantic modeli olduğu için, Pydantic modelleriyle kullanabildiğiniz **type annotation**’larda aynı şekilde kullanabilirsiniz. + +Örneğin `Hero` tipinde bir parametre tanımlarsanız, bu parametre **JSON body**’den okunur. + +Aynı şekilde, bunu fonksiyonun **return type**’ı olarak da tanımlayabilirsiniz; böylece verinin şekli otomatik API docs arayüzünde görünür. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *} + +Burada `SessionDep` dependency’sini (bir `Session`) kullanarak yeni `Hero`’yu `Session` instance’ına ekliyoruz, değişiklikleri veritabanına commit ediyoruz, `hero` içindeki veriyi refresh ediyoruz ve sonra geri döndürüyoruz. + +### Hero’ları Okuma { #read-heroes } + +`select()` kullanarak veritabanından `Hero`’ları **okuyabiliriz**. Sonuçları sayfalama (pagination) yapmak için `limit` ve `offset` ekleyebiliriz. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *} + +### Tek Bir Hero Okuma { #read-one-hero } + +Tek bir `Hero` **okuyabiliriz**. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *} + +### Hero Silme { #delete-a-hero } + +Bir `Hero`’yu **silebiliriz**. + +{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *} + +### Uygulamayı Çalıştırma { #run-the-app } + +Uygulamayı çalıştırabilirsiniz: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Sonra `/docs` arayüzüne gidin; **FastAPI**’nin API’yi **dokümante etmek** için bu **modelleri** kullandığını göreceksiniz. Ayrıca veriyi **serialize** ve **validate** etmek için de onları kullanacaktır. + +
+ +
+ +## Birden Fazla Model ile Uygulamayı Güncelleme { #update-the-app-with-multiple-models } + +Şimdi bu uygulamayı biraz **refactor** edelim ve **güvenliği** ile **esnekliği** artıralım. + +Önceki uygulamaya bakarsanız, UI’da şu ana kadar client’ın oluşturulacak `Hero`’nun `id` değerini belirlemesine izin verdiğini görebilirsiniz. + +Buna izin vermemeliyiz; DB’de zaten atanmış bir `id`’yi ezebilirler. `id` belirlemek **client** tarafından değil, **backend** veya **veritabanı** tarafından yapılmalıdır. + +Ayrıca hero için bir `secret_name` oluşturuyoruz ama şimdiye kadar her yerde geri döndürüyoruz; bu pek de **secret** sayılmaz... + +Bunları birkaç **ek model** ekleyerek düzelteceğiz. SQLModel’in parlayacağı yer de burası. + +### Birden Fazla Model Oluşturma { #create-multiple-models } + +**SQLModel**’de, `table=True` olan herhangi bir model sınıfı bir **table model**’dir. + +`table=True` olmayan her model sınıfı ise bir **data model**’dir; bunlar aslında sadece Pydantic modelleridir (bazı küçük ek özelliklerle). + +SQLModel ile **inheritance** kullanarak her durumda tüm alanları tekrar tekrar yazmaktan **kaçınabiliriz**. + +#### `HeroBase` - temel sınıf { #herobase-the-base-class } + +Önce tüm modeller tarafından **paylaşılan alanları** içeren bir `HeroBase` modeliyle başlayalım: + +* `name` +* `age` + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *} + +#### `Hero` - *table model* { #hero-the-table-model } + +Sonra gerçek *table model* olan `Hero`’yu, diğer modellerde her zaman bulunmayan **ek alanlarla** oluşturalım: + +* `id` +* `secret_name` + +`Hero`, `HeroBase`’ten miras aldığı için `HeroBase`’te tanımlanan alanlara da sahiptir. Dolayısıyla `Hero` için tüm alanlar: + +* `id` +* `name` +* `age` +* `secret_name` + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *} + +#### `HeroPublic` - public *data model* { #heropublic-the-public-data-model } + +Sonraki adımda `HeroPublic` modelini oluştururuz; bu model API client’larına **geri döndürülecek** modeldir. + +`HeroBase` ile aynı alanlara sahiptir; dolayısıyla `secret_name` içermez. + +Sonunda kahramanlarımızın kimliği korunmuş oldu! + +Ayrıca `id: int` alanını yeniden tanımlar. Bunu yaparak API client’larıyla bir **contract** (sözleşme) oluşturmuş oluruz; böylece `id` alanının her zaman var olacağını ve `int` olacağını (asla `None` olmayacağını) bilirler. + +/// tip | İpucu + +Return model’in bir değerin her zaman mevcut olduğunu ve her zaman `int` olduğunu (`None` değil) garanti etmesi API client’ları için çok faydalıdır; bu kesinlik sayesinde daha basit kod yazabilirler. + +Ayrıca **otomatik üretilen client**’ların arayüzleri de daha basit olur; böylece API’nizle çalışan geliştiriciler için süreç çok daha rahat olur. + +/// + +`HeroPublic` içindeki tüm alanlar `HeroBase` ile aynıdır; tek fark `id`’nin `int` olarak tanımlanmasıdır (`None` değil): + +* `id` +* `name` +* `age` + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *} + +#### `HeroCreate` - hero oluşturmak için *data model* { #herocreate-the-data-model-to-create-a-hero } + +Şimdi `HeroCreate` modelini oluştururuz; bu model client’tan gelen veriyi **validate** etmek için kullanılır. + +`HeroBase` ile aynı alanlara sahiptir ve ek olarak `secret_name` içerir. + +Artık client’lar **yeni bir hero oluştururken** `secret_name` gönderecek; bu değer veritabanında saklanacak, ancak API response’larında client’a geri döndürülmeyecek. + +/// tip | İpucu + +**Password**’ları bu şekilde ele alırsınız: alırsınız ama API’de geri döndürmezsiniz. + +Ayrıca password değerlerini saklamadan önce **hash** etmelisiniz; **asla plain text olarak saklamayın**. + +/// + +`HeroCreate` alanları: + +* `name` +* `age` +* `secret_name` + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *} + +#### `HeroUpdate` - hero güncellemek için *data model* { #heroupdate-the-data-model-to-update-a-hero } + +Uygulamanın önceki sürümünde bir hero’yu **güncellemenin** bir yolu yoktu; ancak artık **birden fazla model** ile bunu yapabiliriz. + +`HeroUpdate` *data model* biraz özeldir: yeni bir hero oluşturmak için gereken alanların **tamamına** sahiptir, ancak tüm alanlar **opsiyoneldir** (hepsinin bir default değeri vardır). Bu sayede hero güncellerken sadece güncellemek istediğiniz alanları gönderebilirsiniz. + +Tüm **alanlar aslında değiştiği** için (tip artık `None` içeriyor ve default değerleri `None` oluyor), onları **yeniden tanımlamamız** gerekir. + +Aslında `HeroBase`’ten miras almamız gerekmiyor; çünkü tüm alanları yeniden tanımlıyoruz. Tutarlılık için miras almayı bırakıyorum ama bu gerekli değil. Daha çok kişisel tercih meselesi. + +`HeroUpdate` alanları: + +* `name` +* `age` +* `secret_name` + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *} + +### `HeroCreate` ile Oluşturma ve `HeroPublic` Döndürme { #create-with-herocreate-and-return-a-heropublic } + +Artık **birden fazla model** olduğuna göre, onları kullanan uygulama kısımlarını güncelleyebiliriz. + +Request’te bir `HeroCreate` *data model* alırız ve bundan bir `Hero` *table model* oluştururuz. + +Bu yeni *table model* `Hero`, client’ın gönderdiği alanlara sahip olur ve ayrıca veritabanının ürettiği bir `id` alır. + +Sonra fonksiyondan bu *table model* `Hero`’yu olduğu gibi döndürürüz. Ancak `response_model`’i `HeroPublic` *data model* olarak belirlediğimiz için **FastAPI**, veriyi validate ve serialize etmek için `HeroPublic` kullanır. + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *} + +/// tip | İpucu + +Burada **return type annotation** `-> HeroPublic` yerine `response_model=HeroPublic` kullanıyoruz; çünkü gerçekte döndürdüğümüz değer *bir* `HeroPublic` değil. + +Eğer `-> HeroPublic` yazsaydık, editörünüz ve linter’ınız (haklı olarak) `HeroPublic` yerine `Hero` döndürdüğünüz için şikayet edecekti. + +Bunu `response_model` içinde belirterek **FastAPI**’ye işini yapmasını söylüyoruz; type annotation’lara ve editörünüzün/diğer araçların sağladığı desteğe karışmamış oluyoruz. + +/// + +### `HeroPublic` ile Hero’ları Okuma { #read-heroes-with-heropublic } + +Daha öncekiyle aynı şekilde `Hero`’ları **okuyabiliriz**; yine `response_model=list[HeroPublic]` kullanarak verinin doğru biçimde validate ve serialize edilmesini garanti ederiz. + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *} + +### `HeroPublic` ile Tek Bir Hero Okuma { #read-one-hero-with-heropublic } + +Tek bir hero’yu **okuyabiliriz**: + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *} + +### `HeroUpdate` ile Hero Güncelleme { #update-a-hero-with-heroupdate } + +Bir hero’yu **güncelleyebiliriz**. Bunun için HTTP `PATCH` operasyonu kullanırız. + +Kodda, client’ın gönderdiği tüm verilerle bir `dict` alırız; **yalnızca client’ın gönderdiği veriler**, yani sadece default değer oldukları için orada bulunan değerler hariç. Bunu yapmak için `exclude_unset=True` kullanırız. Asıl numara bu. + +Sonra `hero_db.sqlmodel_update(hero_data)` ile `hero_db`’yi `hero_data` içindeki verilerle güncelleriz. + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *} + +### Hero’yu Tekrar Silme { #delete-a-hero-again } + +Bir hero’yu **silmek** büyük ölçüde aynı kalıyor. + +Bu örnekte her şeyi refactor etme isteğimizi bastıracağız. + +{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *} + +### Uygulamayı Tekrar Çalıştırma { #run-the-app-again } + +Uygulamayı tekrar çalıştırabilirsiniz: + +
+ +```console +$ fastapi dev main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +`/docs` API UI’a giderseniz artık güncellendiğini göreceksiniz; hero oluştururken client’tan `id` beklemeyecek, vb. + +
+ +
+ +## Özet { #recap } + +Bir SQL veritabanıyla etkileşim kurmak için **SQLModel** kullanabilir ve *data model* ile *table model* yaklaşımıyla kodu sadeleştirebilirsiniz. + +**SQLModel** dokümantasyonunda çok daha fazlasını öğrenebilirsiniz; **FastAPI** ile SQLModel kullanımı için daha uzun bir mini tutorial da bulunuyor. diff --git a/docs/tr/docs/tutorial/testing.md b/docs/tr/docs/tutorial/testing.md new file mode 100644 index 000000000..887156606 --- /dev/null +++ b/docs/tr/docs/tutorial/testing.md @@ -0,0 +1,190 @@ +# Test Etme { #testing } + +Starlette sayesinde **FastAPI** uygulamalarını test etmek kolay ve keyiflidir. + +Temelde HTTPX üzerine kuruludur; HTTPX de Requests’i temel alarak tasarlandığı için oldukça tanıdık ve sezgiseldir. + +Bununla birlikte **FastAPI** ile pytest'i doğrudan kullanabilirsiniz. + +## `TestClient` Kullanımı { #using-testclient } + +/// info | Bilgi + +`TestClient` kullanmak için önce `httpx`'i kurun. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu aktifleştirdiğinizden ve sonra kurulumu yaptığınızdan emin olun; örneğin: + +```console +$ pip install httpx +``` + +/// + +`TestClient`'ı import edin. + +**FastAPI** uygulamanızı ona vererek bir `TestClient` oluşturun. + +Adı `test_` ile başlayan fonksiyonlar oluşturun (bu, `pytest`'in standart konvansiyonudur). + +`TestClient` nesnesini `httpx` ile kullandığınız şekilde kullanın. + +Kontrol etmeniz gereken şeyler için standart Python ifadeleriyle basit `assert` satırları yazın (bu da `pytest` standardıdır). + +{* ../../docs_src/app_testing/tutorial001_py39.py hl[2,12,15:18] *} + +/// tip | İpucu + +Test fonksiyonlarının `async def` değil, normal `def` olduğuna dikkat edin. + +Client'a yapılan çağrılar da `await` kullanılmadan, normal çağrılardır. + +Bu sayede `pytest`'i ek bir karmaşıklık olmadan doğrudan kullanabilirsiniz. + +/// + +/// note | Teknik Detaylar + +İsterseniz `from starlette.testclient import TestClient` da kullanabilirsiniz. + +**FastAPI**, geliştirici olarak size kolaylık olsun diye `starlette.testclient`'ı `fastapi.testclient` üzerinden de sunar. Ancak asıl kaynak doğrudan Starlette'tır. + +/// + +/// tip | İpucu + +FastAPI uygulamanıza request göndermenin dışında testlerinizde `async` fonksiyonlar çağırmak istiyorsanız (örn. asenkron veritabanı fonksiyonları), ileri seviye bölümdeki [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} dokümanına göz atın. + +/// + +## Testleri Ayırma { #separating-tests } + +Gerçek bir uygulamada testlerinizi büyük ihtimalle farklı bir dosyada tutarsınız. + +Ayrıca **FastAPI** uygulamanız birden fazla dosya/modül vb. ile de oluşturulmuş olabilir. + +### **FastAPI** Uygulama Dosyası { #fastapi-app-file } + +[Bigger Applications](bigger-applications.md){.internal-link target=_blank}'te anlatılan şekilde bir dosya yapınız olduğunu varsayalım: + +``` +. +├── app +│   ├── __init__.py +│   └── main.py +``` + +`main.py` dosyasında **FastAPI** uygulamanız bulunuyor olsun: + +{* ../../docs_src/app_testing/app_a_py39/main.py *} + +### Test Dosyası { #testing-file } + +Sonra testlerinizin olduğu bir `test_main.py` dosyanız olabilir. Bu dosya aynı Python package içinde (yani `__init__.py` dosyası olan aynı dizinde) durabilir: + +``` hl_lines="5" +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Bu dosya aynı package içinde olduğu için, `main` modülünden (`main.py`) `app` nesnesini import etmek üzere relative import kullanabilirsiniz: + +{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *} + +...ve test kodunu da öncekiyle aynı şekilde yazabilirsiniz. + +## Test Etme: Genişletilmiş Örnek { #testing-extended-example } + +Şimdi bu örneği genişletelim ve farklı parçaların nasıl test edildiğini görmek için daha fazla detay ekleyelim. + +### Genişletilmiş **FastAPI** Uygulama Dosyası { #extended-fastapi-app-file } + +Aynı dosya yapısıyla devam edelim: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Diyelim ki **FastAPI** uygulamanızın bulunduğu `main.py` dosyasında artık başka **path operations** da var. + +Hata döndürebilecek bir `GET` operation'ı var. + +Birden fazla farklı hata döndürebilecek bir `POST` operation'ı var. + +Her iki *path operation* da `X-Token` header'ını gerektiriyor. + +{* ../../docs_src/app_testing/app_b_an_py310/main.py *} + +### Genişletilmiş Test Dosyası { #extended-testing-file } + +Sonrasında `test_main.py` dosyanızı genişletilmiş testlerle güncelleyebilirsiniz: + +{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *} + +Client'ın request içinde bir bilgi göndermesi gerektiğinde ve bunu nasıl yapacağınızı bilemediğinizde, `httpx` ile nasıl yapılacağını aratabilirsiniz (Google) ya da HTTPX’in tasarımı Requests’e dayandığı için `requests` ile nasıl yapıldığını da arayabilirsiniz. + +Sonra testlerinizde aynısını uygularsınız. + +Örn.: + +* Bir *path* veya *query* parametresi geçirmek için, URL’nin kendisine ekleyin. +* JSON body göndermek için, `json` parametresine bir Python nesnesi (örn. bir `dict`) verin. +* JSON yerine *Form Data* göndermeniz gerekiyorsa, bunun yerine `data` parametresini kullanın. +* *headers* göndermek için, `headers` parametresine bir `dict` verin. +* *cookies* için, `cookies` parametresine bir `dict` verin. + +Backend'e veri geçme hakkında daha fazla bilgi için (`httpx` veya `TestClient` kullanarak) HTTPX dokümantasyonu'na bakın. + +/// info | Bilgi + +`TestClient`'ın Pydantic model'lerini değil, JSON'a dönüştürülebilen verileri aldığını unutmayın. + +Testinizde bir Pydantic model'iniz varsa ve test sırasında verisini uygulamaya göndermek istiyorsanız, [JSON Compatible Encoder](encoder.md){.internal-link target=_blank} içinde açıklanan `jsonable_encoder`'ı kullanabilirsiniz. + +/// + +## Çalıştırma { #run-it } + +Bundan sonra yapmanız gereken tek şey `pytest`'i kurmaktır. + +Bir [virtual environment](../virtual-environments.md){.internal-link target=_blank} oluşturduğunuzdan, onu aktifleştirdiğinizden ve sonra kurulumu yaptığınızdan emin olun; örneğin: + +
+ +```console +$ pip install pytest + +---> 100% +``` + +
+ +Dosyaları ve testleri otomatik olarak bulur, çalıştırır ve sonuçları size raporlar. + +Testleri şu şekilde çalıştırın: + +
+ +```console +$ pytest + +================ test session starts ================ +platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 +rootdir: /home/user/code/superawesome-cli/app +plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 +collected 6 items + +---> 100% + +test_main.py ...... [100%] + +================= 1 passed in 0.03s ================= +``` + +
diff --git a/docs/tr/docs/virtual-environments.md b/docs/tr/docs/virtual-environments.md new file mode 100644 index 000000000..cf7fab778 --- /dev/null +++ b/docs/tr/docs/virtual-environments.md @@ -0,0 +1,862 @@ +# Virtual Environments { #virtual-environments } + +Python projeleriyle çalışırken, her proje için kurduğunuz package'leri birbirinden izole etmek adına büyük ihtimalle bir **virtual environment** (veya benzer bir mekanizma) kullanmalısınız. + +/// info | Bilgi + +Virtual environment'leri, nasıl oluşturulduklarını ve nasıl kullanıldıklarını zaten biliyorsanız bu bölümü atlamak isteyebilirsiniz. 🤓 + +/// + +/// tip | İpucu + +**Virtual environment**, **environment variable** ile aynı şey değildir. + +**Environment variable**, sistemde bulunan ve programların kullanabildiği bir değişkendir. + +**Virtual environment** ise içinde bazı dosyalar bulunan bir klasördür. + +/// + +/// info | Bilgi + +Bu sayfada **virtual environment**'leri nasıl kullanacağınızı ve nasıl çalıştıklarını öğreneceksiniz. + +Eğer Python'ı kurmak dahil her şeyi sizin yerinize yöneten bir **tool** kullanmaya hazırsanız, uv'yi deneyin. + +/// + +## Proje Oluşturun { #create-a-project } + +Önce projeniz için bir klasör oluşturun. + +Ben genelde home/user klasörümün içinde `code` adlı bir klasör oluştururum. + +Sonra bunun içinde her proje için ayrı bir klasör oluştururum. + +
+ +```console +// Go to the home directory +$ cd +// Create a directory for all your code projects +$ mkdir code +// Enter into that code directory +$ cd code +// Create a directory for this project +$ mkdir awesome-project +// Enter into that project directory +$ cd awesome-project +``` + +
+ +## Virtual Environment Oluşturun { #create-a-virtual-environment } + +Bir Python projesi üzerinde **ilk kez** çalışmaya başladığınızda, projenizin içinde **virtual environment** oluşturun. + +/// tip | İpucu + +Bunu her çalıştığınızda değil, **proje başına sadece bir kez** yapmanız yeterlidir. + +/// + +//// tab | `venv` + +Bir virtual environment oluşturmak için, Python ile birlikte gelen `venv` modülünü kullanabilirsiniz. + +
+ +```console +$ python -m venv .venv +``` + +
+ +/// details | Bu komut ne anlama geliyor + +* `python`: `python` adlı programı kullan +* `-m`: bir modülü script gibi çalıştır; bir sonraki kısımda hangi modül olduğunu söyleyeceğiz +* `venv`: normalde Python ile birlikte kurulu gelen `venv` modülünü kullan +* `.venv`: virtual environment'i yeni `.venv` klasörünün içine oluştur + +/// + +//// + +//// tab | `uv` + +Eğer `uv` kuruluysa, onunla da virtual environment oluşturabilirsiniz. + +
+ +```console +$ uv venv +``` + +
+ +/// tip | İpucu + +Varsayılan olarak `uv`, `.venv` adlı bir klasörde virtual environment oluşturur. + +Ancak ek bir argümanla klasör adını vererek bunu özelleştirebilirsiniz. + +/// + +//// + +Bu komut `.venv` adlı bir klasörün içinde yeni bir virtual environment oluşturur. + +/// details | `.venv` veya başka bir ad + +Virtual environment'i başka bir klasörde de oluşturabilirsiniz; ancak buna `.venv` demek yaygın bir konvansiyondur. + +/// + +## Virtual Environment'i Aktif Edin { #activate-the-virtual-environment } + +Oluşturduğunuz virtual environment'i aktif edin; böylece çalıştırdığınız her Python komutu veya kurduğunuz her package onu kullanır. + +/// tip | İpucu + +Projede çalışmak için **yeni bir terminal oturumu** başlattığınız **her seferinde** bunu yapın. + +/// + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Ya da Windows'ta Bash kullanıyorsanız (örn. Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +/// tip | İpucu + +Bu environment'e **yeni bir package** kurduğunuz her seferinde environment'i yeniden **aktif edin**. + +Böylece, o package'in kurduğu bir **terminal (CLI) programı** kullanıyorsanız, global olarak kurulu (ve muhtemelen ihtiyacınız olandan farklı bir versiyona sahip) başka bir program yerine, virtual environment'inizdeki programı kullanmış olursunuz. + +/// + +## Virtual Environment'in Aktif Olduğunu Kontrol Edin { #check-the-virtual-environment-is-active } + +Virtual environment'in aktif olduğunu (bir önceki komutun çalıştığını) kontrol edin. + +/// tip | İpucu + +Bu **opsiyoneldir**; ancak her şeyin beklendiği gibi çalıştığını ve hedeflediğiniz virtual environment'i kullandığınızı **kontrol etmek** için iyi bir yöntemdir. + +/// + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +Eğer `python` binary'sini projenizin içinde (bu örnekte `awesome-project`) `.venv/bin/python` yolunda gösteriyorsa, tamamdır. 🎉 + +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +Eğer `python` binary'sini projenizin içinde (bu örnekte `awesome-project`) `.venv\Scripts\python` yolunda gösteriyorsa, tamamdır. 🎉 + +//// + +## `pip`'i Yükseltin { #upgrade-pip } + +/// tip | İpucu + +`uv` kullanıyorsanız, `pip` yerine onunla kurulum yaparsınız; dolayısıyla `pip`'i yükseltmeniz gerekmez. 😎 + +/// + +Package'leri kurmak için `pip` kullanıyorsanız (Python ile varsayılan olarak gelir), en güncel sürüme **yükseltmeniz** gerekir. + +Bir package kurarken görülen birçok garip hata, önce `pip`'i yükseltince çözülür. + +/// tip | İpucu + +Bunu genelde virtual environment'i oluşturduktan hemen sonra **bir kez** yaparsınız. + +/// + +Virtual environment'in aktif olduğundan emin olun (yukarıdaki komutla) ve sonra şunu çalıştırın: + +
+ +```console +$ python -m pip install --upgrade pip + +---> 100% +``` + +
+ +/// tip | İpucu + +Bazen pip'i yükseltmeye çalışırken **`No module named pip`** hatası alabilirsiniz. + +Böyle olursa, aşağıdaki komutla pip'i kurup yükseltin: + +
+ +```console +$ python -m ensurepip --upgrade + +---> 100% +``` + +
+ +Bu komut pip kurulu değilse kurar ve ayrıca kurulu pip sürümünün `ensurepip` içinde bulunan sürüm kadar güncel olmasını garanti eder. + +/// + +## `.gitignore` Ekleyin { #add-gitignore } + +**Git** kullanıyorsanız (kullanmalısınız), `.venv` içindeki her şeyi Git'ten hariç tutmak için bir `.gitignore` dosyası ekleyin. + +/// tip | İpucu + +Virtual environment'i `uv` ile oluşturduysanız, bunu zaten sizin için yaptı; bu adımı atlayabilirsiniz. 😎 + +/// + +/// tip | İpucu + +Bunu virtual environment'i oluşturduktan hemen sonra **bir kez** yapın. + +/// + +
+ +```console +$ echo "*" > .venv/.gitignore +``` + +
+ +/// details | Bu komut ne anlama geliyor + +* `echo "*"`: terminale `*` metnini "yazar" (sonraki kısım bunu biraz değiştiriyor) +* `>`: `>` işaretinin solundaki komutun terminale yazdıracağı çıktı, ekrana basılmak yerine sağ taraftaki dosyaya yazılsın +* `.gitignore`: metnin yazılacağı dosyanın adı + +Git'te `*` "her şey" demektir. Yani `.venv` klasörü içindeki her şeyi ignore eder. + +Bu komut, içeriği şu olan bir `.gitignore` dosyası oluşturur: + +```gitignore +* +``` + +/// + +## Package'leri Kurun { #install-packages } + +Environment'i aktif ettikten sonra, içine package kurabilirsiniz. + +/// tip | İpucu + +Projede ihtiyaç duyduğunuz package'leri ilk kez kurarken veya yükseltirken bunu **bir kez** yapın. + +Bir sürümü yükseltmeniz veya yeni bir package eklemeniz gerekirse **tekrar** yaparsınız. + +/// + +### Package'leri Doğrudan Kurun { #install-packages-directly } + +Acele ediyorsanız ve projenizin package gereksinimlerini bir dosyada belirtmek istemiyorsanız, doğrudan kurabilirsiniz. + +/// tip | İpucu + +Programınızın ihtiyaç duyduğu package'leri ve versiyonlarını bir dosyada tutmak (ör. `requirements.txt` veya `pyproject.toml`) (çok) iyi bir fikirdir. + +/// + +//// tab | `pip` + +
+ +```console +$ pip install "fastapi[standard]" + +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +Eğer `uv` varsa: + +
+ +```console +$ uv pip install "fastapi[standard]" +---> 100% +``` + +
+ +//// + +### `requirements.txt`'ten Kurun { #install-from-requirements-txt } + +Bir `requirements.txt` dosyanız varsa, içindeki package'leri kurmak için artık onu kullanabilirsiniz. + +//// tab | `pip` + +
+ +```console +$ pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +Eğer `uv` varsa: + +
+ +```console +$ uv pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +/// details | `requirements.txt` + +Bazı package'ler içeren bir `requirements.txt` şöyle görünebilir: + +```requirements.txt +fastapi[standard]==0.113.0 +pydantic==2.8.0 +``` + +/// + +## Programınızı Çalıştırın { #run-your-program } + +Virtual environment'i aktif ettikten sonra programınızı çalıştırabilirsiniz; program, virtual environment'in içindeki Python'ı ve oraya kurduğunuz package'leri kullanır. + +
+ +```console +$ python main.py + +Hello World +``` + +
+ +## Editörünüzü Yapılandırın { #configure-your-editor } + +Muhtemelen bir editör kullanırsınız; otomatik tamamlamayı ve satır içi hataları alabilmek için, editörünüzü oluşturduğunuz aynı virtual environment'i kullanacak şekilde yapılandırdığınızdan emin olun (muhtemelen otomatik algılar). + +Örneğin: + +* VS Code +* PyCharm + +/// tip | İpucu + +Bunu genelde yalnızca **bir kez**, virtual environment'i oluşturduğunuzda yapmanız gerekir. + +/// + +## Virtual Environment'i Devre Dışı Bırakın { #deactivate-the-virtual-environment } + +Projeniz üzerinde işiniz bittiğinde virtual environment'i **deactivate** edebilirsiniz. + +
+ +```console +$ deactivate +``` + +
+ +Böylece `python` çalıştırdığınızda, o virtual environment içinden (ve oraya kurulu package'lerle) çalıştırmaya çalışmaz. + +## Çalışmaya Hazırsınız { #ready-to-work } + +Artık projeniz üzerinde çalışmaya başlayabilirsiniz. + +/// tip | İpucu + +Yukarıdaki her şeyin aslında ne olduğunu anlamak ister misiniz? + +Okumaya devam edin. 👇🤓 + +/// + +## Neden Virtual Environment { #why-virtual-environments } + +FastAPI ile çalışmak için Python kurmanız gerekir. + +Sonrasında FastAPI'yi ve kullanmak istediğiniz diğer tüm **package**'leri **kurmanız** gerekir. + +Package kurmak için genelde Python ile gelen `pip` komutunu (veya benzeri alternatifleri) kullanırsınız. + +Ancak `pip`'i doğrudan kullanırsanız, package'ler **global Python environment**'ınıza (Python'ın global kurulumuna) yüklenir. + +### Problem { #the-problem } + +Peki package'leri global Python environment'a kurmanın sorunu ne? + +Bir noktada, muhtemelen **farklı package**'lere bağımlı birçok farklı program yazacaksınız. Ayrıca üzerinde çalıştığınız bazı projeler, aynı package'in **farklı versiyonlarına** ihtiyaç duyacak. 😱 + +Örneğin `philosophers-stone` adında bir proje oluşturduğunuzu düşünün; bu program, `harry` adlı başka bir package'e **`1` versiyonu ile** bağlı. Yani `harry`'yi kurmanız gerekir. + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` + +Sonra daha ileri bir zamanda `prisoner-of-azkaban` adlı başka bir proje oluşturuyorsunuz; bu proje de `harry`'ye bağlı, fakat bu proje **`harry` versiyon `3`** istiyor. + +```mermaid +flowchart LR + azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3] +``` + +Şimdi sorun şu: package'leri local bir **virtual environment** yerine global (global environment) olarak kurarsanız, `harry`'nin hangi versiyonunu kuracağınıza karar vermek zorunda kalırsınız. + +`philosophers-stone`'u çalıştırmak istiyorsanız önce `harry` versiyon `1`'i kurmanız gerekir; örneğin: + +
+ +```console +$ pip install "harry==1" +``` + +
+ +Sonuç olarak global Python environment'ınızda `harry` versiyon `1` kurulu olur. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -->|requires| harry-1 + end +``` + +Fakat `prisoner-of-azkaban`'ı çalıştırmak istiyorsanız, `harry` versiyon `1`'i kaldırıp `harry` versiyon `3`'ü kurmanız gerekir (ya da sadece `3`'ü kurmak, otomatik olarak `1`'i kaldırabilir). + +
+ +```console +$ pip install "harry==3" +``` + +
+ +Sonuç olarak global Python environment'ınızda `harry` versiyon `3` kurulu olur. + +Ve `philosophers-stone`'u tekrar çalıştırmaya kalkarsanız, `harry` versiyon `1`'e ihtiyaç duyduğu için **çalışmama** ihtimali vardır. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + style harry-1 fill:#ccc,stroke-dasharray: 5 5 + harry-3[harry v3] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -.-x|⛔️| harry-1 + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --> |requires| harry-3 + end +``` + +/// tip | İpucu + +Python package'lerinde **yeni versiyonlarda** **breaking change**'lerden kaçınmak oldukça yaygındır; ancak yine de daha güvenlisi, yeni versiyonları bilinçli şekilde kurmak ve mümkünse test'leri çalıştırıp her şeyin doğru çalıştığını doğrulamaktır. + +/// + +Şimdi bunu, **projelerinizin bağımlı olduğu** daha **birçok** başka **package** ile birlikte düşünün. Yönetmesi epey zorlaşır. Sonunda bazı projeleri package'lerin **uyumsuz versiyonlarıyla** çalıştırıp, bir şeylerin neden çalışmadığını anlamamak gibi durumlara düşebilirsiniz. + +Ayrıca işletim sisteminize (örn. Linux, Windows, macOS) bağlı olarak Python zaten kurulu gelmiş olabilir. Bu durumda, sisteminizin **ihtiyaç duyduğu** bazı package'ler belirli versiyonlarla önceden kurulu olabilir. Global Python environment'a package kurarsanız, işletim sistemiyle gelen bazı programları **bozma** ihtimaliniz olabilir. + +## Package'ler Nereye Kuruluyor { #where-are-packages-installed } + +Python'ı kurduğunuzda, bilgisayarınızda bazı dosyalar içeren klasörler oluşturulur. + +Bu klasörlerin bir kısmı, kurduğunuz tüm package'leri barındırmaktan sorumludur. + +Şunu çalıştırdığınızda: + +
+ +```console +// Don't run this now, it's just an example 🤓 +$ pip install "fastapi[standard]" +---> 100% +``` + +
+ +Bu, FastAPI kodunu içeren sıkıştırılmış bir dosyayı genellikle PyPI'dan indirir. + +Ayrıca FastAPI'nin bağımlı olduğu diğer package'ler için de dosyaları **indirir**. + +Sonra tüm bu dosyaları **açar (extract)** ve bilgisayarınızdaki bir klasöre koyar. + +Varsayılan olarak bu indirilip çıkarılan dosyaları, Python kurulumunuzla birlikte gelen klasöre yerleştirir; yani **global environment**'a. + +## Virtual Environment Nedir { #what-are-virtual-environments } + +Global environment'da tüm package'leri bir arada tutmanın sorunlarına çözüm, çalıştığınız her proje için ayrı bir **virtual environment** kullanmaktır. + +Virtual environment, global olana çok benzeyen bir **klasördür**; bir projenin ihtiyaç duyduğu package'leri buraya kurarsınız. + +Böylece her projenin kendi virtual environment'i (`.venv` klasörü) ve kendi package'leri olur. + +```mermaid +flowchart TB + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) --->|requires| harry-1 + subgraph venv1[.venv] + harry-1[harry v1] + end + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --->|requires| harry-3 + subgraph venv2[.venv] + harry-3[harry v3] + end + end + stone-project ~~~ azkaban-project +``` + +## Virtual Environment'i Aktif Etmek Ne Demek { #what-does-activating-a-virtual-environment-mean } + +Bir virtual environment'i örneğin şununla aktif ettiğinizde: + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Ya da Windows'ta Bash kullanıyorsanız (örn. Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +Bu komut, sonraki komutlarda kullanılabilecek bazı [environment variable](environment-variables.md){.internal-link target=_blank}'ları oluşturur veya değiştirir. + +Bunlardan biri `PATH` değişkenidir. + +/// tip | İpucu + +`PATH` environment variable hakkında daha fazla bilgiyi [Environment Variables](environment-variables.md#path-environment-variable){.internal-link target=_blank} bölümünde bulabilirsiniz. + +/// + +Bir virtual environment'i aktive etmek, onun `.venv/bin` (Linux ve macOS'ta) veya `.venv\Scripts` (Windows'ta) yolunu `PATH` environment variable'ına ekler. + +Diyelim ki environment'i aktive etmeden önce `PATH` değişkeni şöyleydi: + +//// tab | Linux, macOS + +```plaintext +/usr/bin:/bin:/usr/sbin:/sbin +``` + +Bu, sistemin programları şu klasörlerde arayacağı anlamına gelir: + +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Windows\System32 +``` + +Bu, sistemin programları şurada arayacağı anlamına gelir: + +* `C:\Windows\System32` + +//// + +Virtual environment'i aktive ettikten sonra `PATH` değişkeni şuna benzer hale gelir: + +//// tab | Linux, macOS + +```plaintext +/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +Bu, sistemin artık programları önce şurada aramaya başlayacağı anlamına gelir: + +```plaintext +/home/user/code/awesome-project/.venv/bin +``` + +diğer klasörlere bakmadan önce. + +Dolayısıyla terminale `python` yazdığınızda, sistem Python programını şurada bulur: + +```plaintext +/home/user/code/awesome-project/.venv/bin/python +``` + +ve onu kullanır. + +//// + +//// tab | Windows + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 +``` + +Bu, sistemin artık programları önce şurada aramaya başlayacağı anlamına gelir: + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts +``` + +diğer klasörlere bakmadan önce. + +Dolayısıyla terminale `python` yazdığınızda, sistem Python programını şurada bulur: + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +ve onu kullanır. + +//// + +Önemli bir detay: virtual environment yolu `PATH` değişkeninin **en başına** eklenir. Sistem, mevcut başka herhangi bir Python'ı bulmadan **önce** bunu bulur. Böylece `python` çalıştırdığınızda, başka bir `python` (örneğin global environment'tan gelen `python`) yerine **virtual environment'taki** Python kullanılır. + +Virtual environment'i aktive etmek birkaç şeyi daha değiştirir; ancak yaptığı en önemli işlerden biri budur. + +## Virtual Environment'i Kontrol Etmek { #checking-a-virtual-environment } + +Bir virtual environment'in aktif olup olmadığını örneğin şununla kontrol ettiğinizde: + +//// 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 +``` + +
+ +//// + +Bu, kullanılacak `python` programının **virtual environment'in içindeki** Python olduğu anlamına gelir. + +Linux ve macOS'ta `which`, Windows PowerShell'de ise `Get-Command` kullanırsınız. + +Bu komutun çalışma mantığı şudur: `PATH` environment variable içindeki **her yolu sırayla** dolaşır, `python` adlı programı arar. Bulduğunda, size o programın **dosya yolunu** gösterir. + +En önemli kısım şu: `python` dediğinizde çalışacak olan "`python`" tam olarak budur. + +Yani doğru virtual environment'da olup olmadığınızı doğrulayabilirsiniz. + +/// tip | İpucu + +Bir virtual environment'i aktive etmek kolaydır; sonra o Python ile kalıp **başka bir projeye geçmek** de kolaydır. + +Bu durumda ikinci proje, başka bir projenin virtual environment'ından gelen **yanlış Python**'ı kullandığınız için **çalışmayabilir**. + +Hangi `python`'ın kullanıldığını kontrol edebilmek bu yüzden faydalıdır. 🤓 + +/// + +## Neden Virtual Environment'i Deactivate Edelim { #why-deactivate-a-virtual-environment } + +Örneğin `philosophers-stone` projesi üzerinde çalışıyor olabilirsiniz; **o virtual environment'i aktive eder**, package kurar ve o environment ile çalışırsınız. + +Sonra **başka bir proje** olan `prisoner-of-azkaban` üzerinde çalışmak istersiniz. + +O projeye gidersiniz: + +
+ +```console +$ cd ~/code/prisoner-of-azkaban +``` + +
+ +Eğer `philosophers-stone` için olan virtual environment'i deactivate etmezseniz, terminalde `python` çalıştırdığınızda `philosophers-stone`'dan gelen Python'ı kullanmaya çalışır. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +$ python main.py + +// Error importing sirius, it's not installed 😱 +Traceback (most recent call last): + File "main.py", line 1, in + import sirius +``` + +
+ +Ama virtual environment'i deactivate edip `prisoner-of-askaban` için yeni olanı aktive ederseniz, `python` çalıştırdığınızda `prisoner-of-azkaban` içindeki virtual environment'dan gelen Python kullanılır. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +// You don't need to be in the old directory to deactivate, you can do it wherever you are, even after going to the other project 😎 +$ deactivate + +// Activate the virtual environment in prisoner-of-azkaban/.venv 🚀 +$ source .venv/bin/activate + +// Now when you run python, it will find the package sirius installed in this virtual environment ✨ +$ python main.py + +I solemnly swear 🐺 +``` + +
+ +## Alternatifler { #alternatives } + +Bu, başlamanız için basit bir rehber ve alttaki mekanizmaların nasıl çalıştığını öğretmeyi amaçlıyor. + +Virtual environment'leri, package bağımlılıklarını (requirements) ve projeleri yönetmek için birçok **alternatif** vardır. + +Hazır olduğunuzda ve package bağımlılıkları, virtual environment'ler vb. dahil **tüm projeyi yönetmek** için bir tool kullanmak istediğinizde, uv'yi denemenizi öneririm. + +`uv` birçok şey yapabilir, örneğin: + +* Sizin için **Python kurabilir**, farklı sürümler dahil +* Projelerinizin **virtual environment**'ini yönetebilir +* **Package** kurabilir +* Projeniz için package **bağımlılıklarını ve versiyonlarını** yönetebilir +* Bağımlılıkları dahil, kurulacak package ve versiyonların **tam (exact)** bir setini garanti edebilir; böylece geliştirirken bilgisayarınızda çalıştırdığınız projeyi production'da da birebir aynı şekilde çalıştırabileceğinizden emin olursunuz; buna **locking** denir +* Ve daha birçok şey + +## Sonuç { #conclusion } + +Buradaki her şeyi okuduysanız ve anladıysanız, artık birçok geliştiriciden **çok daha fazla** virtual environment bilgisine sahipsiniz. 🤓 + +Bu detayları bilmek, ileride karmaşık görünen bir sorunu debug ederken büyük olasılıkla işinize yarayacak; çünkü **altta nasıl çalıştığını** biliyor olacaksınız. 😎 From c7682a198a68455d0004dd0e8e829eef807065e9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 15:44:05 +0000 Subject: [PATCH 195/243] =?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 c54875f06..930403210 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Update translations for tr (add-missing). PR [#14790](https://github.com/fastapi/fastapi/pull/14790) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for fr (update-outdated). PR [#14826](https://github.com/fastapi/fastapi/pull/14826) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for zh-hant (update-outdated). PR [#14825](https://github.com/fastapi/fastapi/pull/14825) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for uk (update-outdated). PR [#14822](https://github.com/fastapi/fastapi/pull/14822) by [@tiangolo](https://github.com/tiangolo). From dcfd432c3645055ab6f23227c26f56aa182358ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 07:47:42 -0800 Subject: [PATCH 196/243] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20tr?= =?UTF-8?q?anslations=20(#14830)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/contributing.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 5e6d821b3..af7944e75 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -179,19 +179,23 @@ as Uvicorn by default will use the port `8000`, the documentation on port `8008` Help with translations is VERY MUCH appreciated! And it can't be done without the help from the community. 🌎 🚀 -Here are the steps to help with translations. - -#### Translation PRs - Translation pull requests are made by LLMs guided with prompts designed by the FastAPI team together with the community of native speakers for each supported language. -These translations are normally still reviewed by native speakers, and here's where you can help! +#### LLM Prompt per Language -* Check the currently existing pull requests for your language. You can filter the pull requests by the ones with the label for your language. For example, for Spanish, the label is `lang-es`. +Each language has a directory: https://github.com/fastapi/fastapi/tree/master/docs, in it you can see a file `llm-prompt.md` with the prompt specific for that language. -* When reviewing a pull request, it's better not to suggest changes in the same pull request, because it is LLM generated, and it won't be possible to make sure that small individual changes are replicated in other similar sections, or that they are preserved when translating the same content again. +For example, for Spanish, the prompt is at: `docs/es/llm-prompt.md`. -* Instead of adding suggestions to the translation PR, make the suggestions to the LLM prompt file for that language, in a new PR. For example, for Spanish, the LLM prompt file is at: `docs/es/llm-prompt.md`. +If you see mistakes in your language, you can make suggestions to the prompt in that file for your language, and request the specific pages you would like to re-generate after the changes. + +#### Reviewing Translation PRs + +You can also check the currently existing pull requests for your language. You can filter the pull requests by the ones with the label for your language. For example, for Spanish, the label is `lang-es`. + +When reviewing a pull request, it's better not to suggest changes in the same pull request, because it is LLM generated, and it won't be possible to make sure that small individual changes are replicated in other similar sections, or that they are preserved when translating the same content again. + +Instead of adding suggestions to the translation PR, make the suggestions to the LLM prompt file for that language, in a new PR. For example, for Spanish, the LLM prompt file is at: `docs/es/llm-prompt.md`. /// tip @@ -201,9 +205,9 @@ Check the docs about GitHub Discussion to coordinate translations for your language. You can subscribe to it, and when there's a new pull request to review, an automatic comment will be added to the discussion. +Check if there's a GitHub Discussion to coordinate translations for your language. You can subscribe to it, and when there's a new pull request to review, an automatic comment will be added to the discussion. -* To check the 2-letter code for the language you want to translate, you can use the table List of ISO 639-1 codes. +To check the 2-letter code for the language you want to translate, you can use the table List of ISO 639-1 codes. #### Request a New Language From 8488d31aff7da56915f424d0b16e2b1cad70ec2d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 15:48:06 +0000 Subject: [PATCH 197/243] =?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 930403210..a91ea5a2b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Update docs for translations. PR [#14830](https://github.com/fastapi/fastapi/pull/14830) by [@tiangolo](https://github.com/tiangolo). * 📝 Fix duplicate word in `advanced-dependencies.md`. PR [#14815](https://github.com/fastapi/fastapi/pull/14815) by [@Rayyan-Oumlil](https://github.com/Rayyan-Oumlil). ### Translations From da9e101d5007dbb35bfe4fa695bbc2e22b42d568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 07:56:49 -0800 Subject: [PATCH 198/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20tr=20(update-outdated)=20(#14831)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/tr/docs/advanced/wsgi.md | 22 +++++++++++++++++++--- docs/tr/docs/index.md | 18 ++++++------------ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/docs/tr/docs/advanced/wsgi.md b/docs/tr/docs/advanced/wsgi.md index 442f83a59..6f6b10b68 100644 --- a/docs/tr/docs/advanced/wsgi.md +++ b/docs/tr/docs/advanced/wsgi.md @@ -1,18 +1,34 @@ # WSGI'yi Dahil Etme - Flask, Django ve Diğerleri { #including-wsgi-flask-django-others } -WSGI uygulamalarını [Sub Applications - Mounts](sub-applications.md){.internal-link target=_blank}, [Behind a Proxy](behind-a-proxy.md){.internal-link target=_blank} bölümlerinde gördüğünüz gibi mount edebilirsiniz. +WSGI uygulamalarını [Alt Uygulamalar - Mount Etme](sub-applications.md){.internal-link target=_blank}, [Bir Proxy Arkasında](behind-a-proxy.md){.internal-link target=_blank} bölümlerinde gördüğünüz gibi mount edebilirsiniz. Bunun için `WSGIMiddleware`'ı kullanabilir ve bunu WSGI uygulamanızı (örneğin Flask, Django vb.) sarmalamak için kullanabilirsiniz. ## `WSGIMiddleware` Kullanımı { #using-wsgimiddleware } -`WSGIMiddleware`'ı import etmeniz gerekir. +/// info | Bilgi + +Bunun için `a2wsgi` kurulmalıdır; örneğin `pip install a2wsgi` ile. + +/// + +`WSGIMiddleware`'ı `a2wsgi` paketinden import etmeniz gerekir. Ardından WSGI (örn. Flask) uygulamasını middleware ile sarmalayın. Ve sonra bunu bir path'in altına mount edin. -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note | Not + +Önceden, `fastapi.middleware.wsgi` içindeki `WSGIMiddleware`'ın kullanılması öneriliyordu, ancak artık kullanımdan kaldırıldı. + +Bunun yerine `a2wsgi` paketini kullanmanız önerilir. Kullanım aynıdır. + +Sadece `a2wsgi` paketinin kurulu olduğundan emin olun ve `WSGIMiddleware`'ı `a2wsgi` içinden doğru şekilde import edin. + +/// ## Kontrol Edelim { #check-it } diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index 9cffd4274..16c425f5d 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Şu içerikle `main.py` adında bir dosya oluşturalım: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): Eğer kodunuz `async` / `await` kullanıyorsa, `async def` kullanın: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ Alternatif otomatik dokümantasyonu göreceksiniz ( Date: Thu, 5 Feb 2026 15:57:15 +0000 Subject: [PATCH 199/243] =?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 a91ea5a2b..e85e3aa87 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Translations +* 🌐 Update translations for tr (update-outdated). PR [#14831](https://github.com/fastapi/fastapi/pull/14831) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for tr (add-missing). PR [#14790](https://github.com/fastapi/fastapi/pull/14790) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for fr (update-outdated). PR [#14826](https://github.com/fastapi/fastapi/pull/14826) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for zh-hant (update-outdated). PR [#14825](https://github.com/fastapi/fastapi/pull/14825) by [@tiangolo](https://github.com/tiangolo). From 4fe06cc24a9e502be2e2aeef9eeb236cc959dea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 08:02:22 -0800 Subject: [PATCH 200/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20es=20(update-outdated)=20(#14832)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/es/docs/advanced/wsgi.md | 22 ++++++++++++++++--- docs/es/docs/deployment/docker.md | 6 ++--- docs/es/docs/index.md | 18 +++++---------- docs/es/docs/tutorial/body-multiple-params.md | 6 ++--- .../tutorial/path-operation-configuration.md | 4 ++-- 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/docs/es/docs/advanced/wsgi.md b/docs/es/docs/advanced/wsgi.md index d5113250a..ae31185ee 100644 --- a/docs/es/docs/advanced/wsgi.md +++ b/docs/es/docs/advanced/wsgi.md @@ -2,17 +2,33 @@ Puedes montar aplicaciones WSGI como viste con [Sub Aplicaciones - Mounts](sub-applications.md){.internal-link target=_blank}, [Detrás de un Proxy](behind-a-proxy.md){.internal-link target=_blank}. -Para eso, puedes usar `WSGIMiddleware` y usarlo para envolver tu aplicación WSGI, por ejemplo, Flask, Django, etc. +Para eso, puedes usar el `WSGIMiddleware` y usarlo para envolver tu aplicación WSGI, por ejemplo, Flask, Django, etc. ## Usando `WSGIMiddleware` { #using-wsgimiddleware } -Necesitas importar `WSGIMiddleware`. +/// info | Información + +Esto requiere instalar `a2wsgi`, por ejemplo con `pip install a2wsgi`. + +/// + +Necesitas importar `WSGIMiddleware` de `a2wsgi`. Luego envuelve la aplicación WSGI (p. ej., Flask) con el middleware. Y luego móntala bajo un path. -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note | Nota + +Anteriormente, se recomendaba usar `WSGIMiddleware` de `fastapi.middleware.wsgi`, pero ahora está deprecado. + +Se aconseja usar el paquete `a2wsgi` en su lugar. El uso sigue siendo el mismo. + +Solo asegúrate de tener instalado el paquete `a2wsgi` e importar `WSGIMiddleware` correctamente desde `a2wsgi`. + +/// ## Revisa { #check-it } diff --git a/docs/es/docs/deployment/docker.md b/docs/es/docs/deployment/docker.md index 114a62ec3..9a0b88955 100644 --- a/docs/es/docs/deployment/docker.md +++ b/docs/es/docs/deployment/docker.md @@ -145,8 +145,6 @@ Existen otros formatos y herramientas para definir e instalar dependencias de pa * Crea un archivo `main.py` con: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -158,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -572,7 +570,7 @@ Si tienes una configuración simple, con un **contenedor único** que luego inic ### Imagen Base de Docker { #base-docker-image } -Solía haber una imagen official de Docker de FastAPI: tiangolo/uvicorn-gunicorn-fastapi-docker. Pero ahora está obsoleta. ⛔️ +Solía haber una imagen official de Docker de FastAPI: tiangolo/uvicorn-gunicorn-fastapi. Pero ahora está obsoleta. ⛔️ Probablemente **no** deberías usar esta imagen base de Docker (o cualquier otra similar). diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 14fa07e41..ffea0ed54 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Crea un archivo `main.py` con: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): Si tu código usa `async` / `await`, usa `async def`: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ Ahora modifica el archivo `main.py` para recibir un body desde un request `PUT`. Declara el body usando tipos estándar de Python, gracias a Pydantic. -```Python hl_lines="4 9-12 25-27" -from typing import Union - +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -300,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -309,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} diff --git a/docs/es/docs/tutorial/body-multiple-params.md b/docs/es/docs/tutorial/body-multiple-params.md index 57cec1674..c52486f9b 100644 --- a/docs/es/docs/tutorial/body-multiple-params.md +++ b/docs/es/docs/tutorial/body-multiple-params.md @@ -101,13 +101,13 @@ Por supuesto, también puedes declarar parámetros adicionales de query siempre Como, por defecto, los valores singulares se interpretan como parámetros de query, no tienes que añadir explícitamente un `Query`, solo puedes hacer: ```Python -q: Union[str, None] = None +q: str | None = None ``` -O en Python 3.10 y superior: +O en Python 3.9: ```Python -q: str | None = None +q: Union[str, None] = None ``` Por ejemplo: diff --git a/docs/es/docs/tutorial/path-operation-configuration.md b/docs/es/docs/tutorial/path-operation-configuration.md index 945574b9e..f57b322fb 100644 --- a/docs/es/docs/tutorial/path-operation-configuration.md +++ b/docs/es/docs/tutorial/path-operation-configuration.md @@ -52,7 +52,7 @@ En estos casos, podría tener sentido almacenar las tags en un `Enum`. Puedes añadir un `summary` y `description`: -{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} ## Descripción desde docstring { #description-from-docstring } @@ -70,7 +70,7 @@ Será usado en la documentación interactiva: Puedes especificar la descripción del response con el parámetro `response_description`: -{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | Información From afb44f0ff83fef758144639a5e284e2f43d25eb9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 16:03:19 +0000 Subject: [PATCH 201/243] =?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 e85e3aa87..c9470e138 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Translations +* 🌐 Update translations for es (update-outdated). PR [#14832](https://github.com/fastapi/fastapi/pull/14832) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for tr (update-outdated). PR [#14831](https://github.com/fastapi/fastapi/pull/14831) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for tr (add-missing). PR [#14790](https://github.com/fastapi/fastapi/pull/14790) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for fr (update-outdated). PR [#14826](https://github.com/fastapi/fastapi/pull/14826) by [@tiangolo](https://github.com/tiangolo). From 136d5f0a56493e289b58838abc82479d50b61df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 08:04:47 -0800 Subject: [PATCH 202/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20ko=20(update-outdated)=20(#14835)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- .../ko/docs/advanced/advanced-dependencies.md | 8 ++--- docs/ko/docs/advanced/wsgi.md | 36 +++++++++++++------ docs/ko/docs/deployment/docker.md | 12 +++---- docs/ko/docs/index.md | 28 ++++++--------- docs/ko/docs/tutorial/body-multiple-params.md | 11 +++--- .../tutorial/path-operation-configuration.md | 8 ++--- 6 files changed, 56 insertions(+), 47 deletions(-) diff --git a/docs/ko/docs/advanced/advanced-dependencies.md b/docs/ko/docs/advanced/advanced-dependencies.md index 04e557d15..fe1606258 100644 --- a/docs/ko/docs/advanced/advanced-dependencies.md +++ b/docs/ko/docs/advanced/advanced-dependencies.md @@ -79,13 +79,13 @@ checker(q="somequery") ### `yield`와 `scope`가 있는 의존성 { #dependencies-with-yield-and-scope } -0.121.0 버전에서 FastAPI는 `yield`가 있는 의존성에 대해 `Depends(scope="function")` 지원을 추가했습니다. +0.121.0 버전에서 FastAPI는 `Depends(scope="function")` 지원을 추가했습니다. `Depends(scope="function")`를 사용하면, `yield` 이후의 종료 코드는 *경로 처리 함수*가 끝난 직후(클라이언트에 응답이 반환되기 전)에 실행됩니다. 그리고 `Depends(scope="request")`(기본값)를 사용하면, `yield` 이후의 종료 코드는 응답이 전송된 후에 실행됩니다. -자세한 내용은 [Dependencies with `yield` - Early exit and `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope) 문서를 참고하세요. +자세한 내용은 [`yield`가 있는 의존성 - 조기 종료와 `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope) 문서를 참고하세요. ### `yield`가 있는 의존성과 `StreamingResponse`, 기술 세부사항 { #dependencies-with-yield-and-streamingresponse-technical-details } @@ -133,7 +133,7 @@ SQLModel(또는 SQLAlchemy)을 사용하면서 이런 특정 사용 사례가 그러면 세션이 데이터베이스 연결을 해제하여, 다른 요청들이 이를 사용할 수 있게 됩니다. -`yield`가 있는 의존성에서 조기 종료가 필요한 다른 사용 사례가 있다면, 여러분의 구체적인 사용 사례와 `yield`가 있는 의존성에 대한 조기 종료가 어떤 점에서 이득이 되는지를 포함해 GitHub Discussion Question을 생성해 주세요. +`yield`가 있는 의존성에서 조기 종료가 필요한 다른 사용 사례가 있다면, 여러분의 구체적인 사용 사례와 `yield`가 있는 의존성에 대한 조기 종료가 어떤 점에서 이득이 되는지를 포함해 GitHub Discussions 질문을 생성해 주세요. `yield`가 있는 의존성에서 조기 종료에 대한 설득력 있는 사용 사례가 있다면, 조기 종료를 선택적으로 활성화할 수 있는 새로운 방법을 추가하는 것을 고려하겠습니다. @@ -145,7 +145,7 @@ FastAPI 0.110.0 이전에는 `yield`가 있는 의존성을 사용한 다음 그 ### 백그라운드 태스크와 `yield`가 있는 의존성, 기술 세부사항 { #background-tasks-and-dependencies-with-yield-technical-details } -FastAPI 0.106.0 이전에는 `yield` 이후에 예외를 발생시키는 것이 불가능했습니다. `yield`가 있는 의존성의 종료 코드는 응답이 전송된 *후에* 실행되었기 때문에, [Exception Handlers](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}가 이미 실행된 뒤였습니다. +FastAPI 0.106.0 이전에는 `yield` 이후에 예외를 발생시키는 것이 불가능했습니다. `yield`가 있는 의존성의 종료 코드는 응답이 전송된 *후에* 실행되었기 때문에, [예외 핸들러](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}가 이미 실행된 뒤였습니다. 이는 주로 백그라운드 태스크 안에서 의존성이 "yield"한 동일한 객체들을 사용할 수 있게 하기 위한 설계였습니다. 백그라운드 태스크가 끝난 뒤에 종료 코드가 실행되었기 때문입니다. diff --git a/docs/ko/docs/advanced/wsgi.md b/docs/ko/docs/advanced/wsgi.md index 89cf57cfe..5e0e87c5e 100644 --- a/docs/ko/docs/advanced/wsgi.md +++ b/docs/ko/docs/advanced/wsgi.md @@ -1,32 +1,48 @@ -# WSGI 포함하기 - Flask, Django 그 외 { #including-wsgi-flask-django-others } +# WSGI 포함하기 - Flask, Django 등 { #including-wsgi-flask-django-others } -[서브 응용 프로그램 - 마운트](sub-applications.md){.internal-link target=_blank}, [프록시 뒤편에서](behind-a-proxy.md){.internal-link target=_blank}에서 보았듯이 WSGI 응용 프로그램들을 마운트 할 수 있습니다. +[서브 애플리케이션 - 마운트](sub-applications.md){.internal-link target=_blank}, [프록시 뒤에서](behind-a-proxy.md){.internal-link target=_blank}에서 본 것처럼 WSGI 애플리케이션을 마운트할 수 있습니다. -이를 위해 `WSGIMiddleware`를 사용해 WSGI 응용 프로그램(예: Flask, Django 등)을 감쌀 수 있습니다. +이를 위해 `WSGIMiddleware`를 사용해 WSGI 애플리케이션(예: Flask, Django 등)을 감쌀 수 있습니다. ## `WSGIMiddleware` 사용하기 { #using-wsgimiddleware } -`WSGIMiddleware`를 불러와야 합니다. +/// info | 정보 -그런 다음, WSGI(예: Flask) 응용 프로그램을 미들웨어로 포장합니다. +이를 사용하려면 `a2wsgi`를 설치해야 합니다. 예: `pip install a2wsgi` -그 후, 해당 경로에 마운트합니다. +/// -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +`a2wsgi`에서 `WSGIMiddleware`를 import 해야 합니다. + +그런 다음, WSGI(예: Flask) 애플리케이션을 미들웨어로 감쌉니다. + +그리고 해당 경로에 마운트합니다. + +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note | 참고 + +이전에 `fastapi.middleware.wsgi`의 `WSGIMiddleware` 사용을 권장했지만 이제는 더 이상 권장되지 않습니다. + +대신 `a2wsgi` 패키지 사용을 권장합니다. 사용 방법은 동일합니다. + +단, `a2wsgi` 패키지가 설치되어 있고 `a2wsgi`에서 `WSGIMiddleware`를 올바르게 import 하는지만 확인하세요. + +/// ## 확인하기 { #check-it } -이제 `/v1/` 경로에 있는 모든 요청은 Flask 응용 프로그램에서 처리됩니다. +이제 `/v1/` 경로에 있는 모든 요청은 Flask 애플리케이션에서 처리됩니다. 그리고 나머지는 **FastAPI**에 의해 처리됩니다. -실행하면 http://localhost:8000/v1/으로 이동해서 Flask의 응답을 볼 수 있습니다: +실행하고 http://localhost:8000/v1/로 이동하면 Flask의 응답을 볼 수 있습니다: ```txt Hello, World from Flask! ``` -그리고 다음으로 이동하면 http://localhost:8000/v2 **FastAPI**의 응답을 볼 수 있습니다: +그리고 http://localhost:8000/v2로 이동하면 **FastAPI**의 응답을 볼 수 있습니다: ```JSON { diff --git a/docs/ko/docs/deployment/docker.md b/docs/ko/docs/deployment/docker.md index be04c923a..20e341c26 100644 --- a/docs/ko/docs/deployment/docker.md +++ b/docs/ko/docs/deployment/docker.md @@ -145,8 +145,6 @@ Successfully installed fastapi pydantic * 다음 내용으로 `main.py` 파일을 만듭니다: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -158,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -245,14 +243,14 @@ Docker 지시어 cluster를 사용한다면, 각 컨테이너에서(**워커를 사용하는 Uvicorn** 같은) **프로세스 매니저**를 쓰는 대신, **클러스터 레벨**에서 **복제를 처리**하고 싶을 가능성이 큽니다. +**Kubernetes**, Docker Swarm Mode, Nomad 등의 복잡한 시스템으로 여러 머신에 분산된 컨테이너를 관리하는 클러스터를 사용한다면, 각 컨테이너에서(**워커를 사용하는 Uvicorn** 같은) **프로세스 매니저**를 쓰는 대신, **클러스터 레벨**에서 **복제를 처리**하고 싶을 가능성이 큽니다. Kubernetes 같은 분산 컨테이너 관리 시스템은 보통 들어오는 요청에 대한 **로드 밸런싱**을 지원하면서도, **컨테이너 복제**를 처리하는 통합된 방법을 가지고 있습니다. 모두 **클러스터 레벨**에서요. @@ -580,7 +578,7 @@ Kubernetes를 사용한다면, 이는 아마도 자동완성. 적은 디버깅 시간. +* **직관적**: 훌륭한 편집기 지원. 모든 곳에서 자동완성. 적은 디버깅 시간. * **쉬움**: 쉽게 사용하고 배우도록 설계. 적은 문서 읽기 시간. * **짧음**: 코드 중복 최소화. 각 매개변수 선언의 여러 기능. 적은 버그. * **견고함**: 준비된 프로덕션 용 코드를 얻으십시오. 자동 대화형 문서와 함께. @@ -127,7 +127,7 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 -웹 API 대신 터미널에서 사용할 CLI 앱을 만들고 있다면, **Typer**를 확인해 보십시오. +웹 API 대신 터미널에서 사용할 CLI 앱을 만들고 있다면, **Typer**를 확인해 보십시오. **Typer**는 FastAPI의 동생입니다. 그리고 **CLI를 위한 FastAPI**가 되기 위해 생겼습니다. ⌨️ 🚀 @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" 다음 내용으로 `main.py` 파일을 만드십시오: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): 여러분의 코드가 `async` / `await`을 사용한다면, `async def`를 사용하십시오: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ INFO: Application startup complete. Pydantic 덕분에 표준 Python 타입을 사용해 본문을 선언합니다. -```Python hl_lines="4 9-12 25-27" -from typing import Union - +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -300,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -309,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -374,7 +368,7 @@ item: Item * 데이터 검증: * 데이터가 유효하지 않을 때 자동으로 생성하는 명확한 에러. * 깊이 중첩된 JSON 객체에 대한 유효성 검사. -* 입력 데이터 변환: 네트워크에서 파이썬 데이터 및 타입으로 전송. 읽을 수 있는 것들: +* 입력 데이터 변환: 네트워크에서 파이썬 데이터 및 타입으로 전송. 읽을 수 있는 것들: * JSON. * 경로 매개변수. * 쿼리 매개변수. @@ -382,7 +376,7 @@ item: Item * 헤더. * 폼(Forms). * 파일. -* 출력 데이터 변환: 파이썬 데이터 및 타입을 네트워크 데이터로 전환(JSON 형식으로): +* 출력 데이터 변환: 파이썬 데이터 및 타입을 네트워크 데이터로 전환(JSON 형식으로): * 파이썬 타입 변환 (`str`, `int`, `float`, `bool`, `list`, 등). * `datetime` 객체. * `UUID` 객체. @@ -445,7 +439,7 @@ item: Item * 서로 다른 장소에서 **매개변수** 선언: **헤더**, **쿠키**, **폼 필드** 그리고 **파일**. * `maximum_length` 또는 `regex`처럼 **유효성 제약**하는 방법. -* 강력하고 사용하기 쉬운 **의존성 주입** 시스템. +* 강력하고 사용하기 쉬운 **의존성 주입** 시스템. * **OAuth2** 지원을 포함한 **JWT tokens** 및 **HTTP Basic**을 갖는 보안과 인증. * (Pydantic 덕분에) **깊은 중첩 JSON 모델**을 선언하는데 더 진보한 (하지만 마찬가지로 쉬운) 기술. * Strawberry 및 기타 라이브러리와의 **GraphQL** 통합. diff --git a/docs/ko/docs/tutorial/body-multiple-params.md b/docs/ko/docs/tutorial/body-multiple-params.md index 701351e63..bebdffed8 100644 --- a/docs/ko/docs/tutorial/body-multiple-params.md +++ b/docs/ko/docs/tutorial/body-multiple-params.md @@ -102,15 +102,16 @@ 기본적으로 단일 값은 쿼리 매개변수로 해석되므로, 명시적으로 `Query`를 추가할 필요 없이 이렇게 하면 됩니다: +```Python +q: str | None = None +``` + +또는 Python 3.9에서는: + ```Python q: Union[str, None] = None ``` -또는 Python 3.10 이상에서는: - -```Python -q: str | None = None -``` 예를 들어: diff --git a/docs/ko/docs/tutorial/path-operation-configuration.md b/docs/ko/docs/tutorial/path-operation-configuration.md index b8a87667a..baef3fb40 100644 --- a/docs/ko/docs/tutorial/path-operation-configuration.md +++ b/docs/ko/docs/tutorial/path-operation-configuration.md @@ -52,11 +52,11 @@ `summary`와 `description`을 추가할 수 있습니다: -{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} ## 독스트링으로 만든 설명 { #description-from-docstring } -설명은 보통 길어지고 여러 줄에 걸쳐있기 때문에, *경로 처리* 설명을 함수 docstring에 선언할 수 있으며, **FastAPI**는 그곳에서 이를 읽습니다. +설명은 보통 길어지고 여러 줄에 걸쳐있기 때문에, *경로 처리* 설명을 함수 docstring에 선언할 수 있으며, **FastAPI**는 그곳에서 이를 읽습니다. 독스트링에는 Markdown을 작성할 수 있으며, (독스트링의 들여쓰기를 고려하여) 올바르게 해석되고 표시됩니다. @@ -70,7 +70,7 @@ `response_description` 매개변수로 응답에 관한 설명을 명시할 수 있습니다: -{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | 정보 @@ -90,7 +90,7 @@ OpenAPI는 각 *경로 처리*가 응답에 관한 설명을 요구할 것을 ## *경로 처리* 지원중단하기 { #deprecate-a-path-operation } -*경로 처리*를 제거하지 않고 deprecated로 표시해야 한다면, `deprecated` 매개변수를 전달하면 됩니다: +*경로 처리*를 제거하지 않고 deprecated로 표시해야 한다면, `deprecated` 매개변수를 전달하면 됩니다: {* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} From 2224b4e63b13a97a3132c7c1ff0cc2476d02527b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 08:05:03 -0800 Subject: [PATCH 203/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20pt=20(update-outdated)=20(#14833)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- .../pt/docs/advanced/advanced-dependencies.md | 2 +- docs/pt/docs/advanced/wsgi.md | 20 ++++++++++++-- docs/pt/docs/deployment/docker.md | 6 ++--- docs/pt/docs/index.md | 26 +++++++------------ docs/pt/docs/tutorial/body-multiple-params.md | 6 ++--- .../tutorial/path-operation-configuration.md | 4 +-- 6 files changed, 36 insertions(+), 28 deletions(-) diff --git a/docs/pt/docs/advanced/advanced-dependencies.md b/docs/pt/docs/advanced/advanced-dependencies.md index 1ad9ea617..959a7f3c2 100644 --- a/docs/pt/docs/advanced/advanced-dependencies.md +++ b/docs/pt/docs/advanced/advanced-dependencies.md @@ -148,7 +148,7 @@ Antes do FastAPI 0.106.0, lançar exceções após o `yield` não era possível, Isso foi projetado assim principalmente para permitir o uso dos mesmos objetos "yielded" por dependências dentro de tarefas em segundo plano, porque o código de saída seria executado depois que as tarefas em segundo plano fossem concluídas. -Isso foi alterado no FastAPI 0.106.0 com a intenção de não manter recursos enquanto se espera a resposta percorrer a rede. +Isso foi alterado no FastAPI 0.106.0 com a intenção de não manter recursos enquanto se espera a response percorrer a rede. /// tip | Dica diff --git a/docs/pt/docs/advanced/wsgi.md b/docs/pt/docs/advanced/wsgi.md index c3c21d430..99b783cdb 100644 --- a/docs/pt/docs/advanced/wsgi.md +++ b/docs/pt/docs/advanced/wsgi.md @@ -6,13 +6,29 @@ Para isso, você pode utilizar o `WSGIMiddleware` para encapsular a sua aplicaç ## Usando `WSGIMiddleware` { #using-wsgimiddleware } -Você precisa importar o `WSGIMiddleware`. +/// info | Informação + +Isso requer instalar `a2wsgi`, por exemplo com `pip install a2wsgi`. + +/// + +Você precisa importar o `WSGIMiddleware` de `a2wsgi`. Em seguida, encapsule a aplicação WSGI (e.g. Flask) com o middleware. E então monte isso sob um path. -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note | Nota + +Anteriormente, recomendava-se usar `WSGIMiddleware` de `fastapi.middleware.wsgi`, mas agora está descontinuado. + +É aconselhável usar o pacote `a2wsgi` em seu lugar. O uso permanece o mesmo. + +Apenas certifique-se de que o pacote `a2wsgi` está instalado e importe `WSGIMiddleware` corretamente de `a2wsgi`. + +/// ## Confira { #check-it } diff --git a/docs/pt/docs/deployment/docker.md b/docs/pt/docs/deployment/docker.md index b26a69b54..d47a15394 100644 --- a/docs/pt/docs/deployment/docker.md +++ b/docs/pt/docs/deployment/docker.md @@ -145,8 +145,6 @@ Há outros formatos e ferramentas para definir e instalar dependências de pacot * Crie um arquivo `main.py` com: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -158,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -572,7 +570,7 @@ Se você tiver uma configuração simples, com um **único contêiner** que ent ### Imagem Docker base { #base-docker-image } -Antes havia uma imagem oficial do FastAPI para Docker: tiangolo/uvicorn-gunicorn-fastapi. Mas agora ela está descontinuada. ⛔️ +Antes havia uma imagem oficial do FastAPI para Docker: tiangolo/uvicorn-gunicorn-fastapi-docker. Mas agora ela está descontinuada. ⛔️ Você provavelmente **não** deve usar essa imagem base do Docker (ou qualquer outra semelhante). diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index 4e3be586d..e61cc2e2f 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -33,7 +33,7 @@ --- -FastAPI é um moderno e rápido (alta performance) _framework web_ para construção de APIs com Python, baseado nos _type hints_ padrões do Python. +FastAPI é um moderno e rápido (alta performance) framework web para construção de APIs com Python, baseado nos type hints padrões do Python. Os recursos chave são: @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Crie um arquivo `main.py` com: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): Se seu código utiliza `async` / `await`, use `async def`: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ Agora modifique o arquivo `main.py` para receber um corpo de uma requisição `P Declare o corpo utilizando tipos padrão Python, graças ao Pydantic. -```Python hl_lines="4 9-12 25-27" -from typing import Union - +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -300,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -309,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} @@ -411,7 +405,7 @@ Voltando ao código do exemplo anterior, **FastAPI** irá: * Documentar tudo com OpenAPI, que poderá ser usado por: * Sistemas de documentação interativos. * Sistemas de clientes de geração de código automáticos, para muitas linguagens. -* Fornecer diretamente 2 interfaces _web_ de documentação interativa. +* Fornecer diretamente 2 interfaces web de documentação interativa. --- @@ -510,7 +504,7 @@ Siga os tutoriais do seu provedor de nuvem para implantar aplicações FastAPI c ## Performance { #performance } -Testes de performance da _Independent TechEmpower_ mostram aplicações **FastAPI** rodando sob Uvicorn como um dos _frameworks_ Python mais rápidos disponíveis, somente atrás de Starlette e Uvicorn (utilizados internamente pelo FastAPI). (*) +Testes de performance da Independent TechEmpower mostram aplicações **FastAPI** rodando sob Uvicorn como um dos frameworks Python mais rápidos disponíveis, somente atrás de Starlette e Uvicorn (utilizados internamente pelo FastAPI). (*) Para entender mais sobre isso, veja a seção Comparações. @@ -530,7 +524,7 @@ Utilizado pelo Starlette: * httpx - Obrigatório caso você queira utilizar o `TestClient`. * jinja2 - Obrigatório se você quer utilizar a configuração padrão de templates. -* python-multipart - Obrigatório se você deseja suporte a "parsing" de formulário, com `request.form()`. +* python-multipart - Obrigatório se você deseja suporte a "parsing" de formulário, com `request.form()`. Utilizado pelo FastAPI: diff --git a/docs/pt/docs/tutorial/body-multiple-params.md b/docs/pt/docs/tutorial/body-multiple-params.md index 3cba04912..24f35b210 100644 --- a/docs/pt/docs/tutorial/body-multiple-params.md +++ b/docs/pt/docs/tutorial/body-multiple-params.md @@ -101,13 +101,13 @@ Obviamente, você também pode declarar parâmetros de consulta assim que você Dado que, por padrão, valores singulares são interpretados como parâmetros de consulta, você não precisa explicitamente adicionar uma `Query`, você pode somente: ```Python -q: Union[str, None] = None +q: str | None = None ``` -Ou como em Python 3.10 e versões superiores: +Ou em Python 3.9: ```Python -q: str | None = None +q: Union[str, None] = None ``` Por exemplo: diff --git a/docs/pt/docs/tutorial/path-operation-configuration.md b/docs/pt/docs/tutorial/path-operation-configuration.md index 84eebc128..2527c2892 100644 --- a/docs/pt/docs/tutorial/path-operation-configuration.md +++ b/docs/pt/docs/tutorial/path-operation-configuration.md @@ -52,7 +52,7 @@ Nestes casos, pode fazer sentido armazenar as tags em um `Enum`. Você pode adicionar um `summary` e uma `description`: -{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} ## Descrição do docstring { #description-from-docstring } @@ -70,7 +70,7 @@ Ela será usada nas documentações interativas: Você pode especificar a descrição da resposta com o parâmetro `response_description`: -{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | Informação From 3578270af481d006bac53e1659640ce0bf257dba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 16:05:18 +0000 Subject: [PATCH 204/243] =?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 c9470e138..7da59e9f2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Translations +* 🌐 Update translations for ko (update-outdated). PR [#14835](https://github.com/fastapi/fastapi/pull/14835) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for es (update-outdated). PR [#14832](https://github.com/fastapi/fastapi/pull/14832) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for tr (update-outdated). PR [#14831](https://github.com/fastapi/fastapi/pull/14831) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for tr (add-missing). PR [#14790](https://github.com/fastapi/fastapi/pull/14790) by [@tiangolo](https://github.com/tiangolo). From b892c3126c270d5f2b695034703bdae2a2f148ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 08:06:07 -0800 Subject: [PATCH 205/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20de=20(update-outdated)=20(#14836)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/de/docs/advanced/wsgi.md | 20 +++++++++++++++++-- docs/de/docs/deployment/docker.md | 4 +--- docs/de/docs/index.md | 18 ++++++----------- docs/de/docs/tutorial/body-multiple-params.md | 8 ++++---- .../tutorial/path-operation-configuration.md | 4 ++-- 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/docs/de/docs/advanced/wsgi.md b/docs/de/docs/advanced/wsgi.md index 3cd776a6a..0090883ce 100644 --- a/docs/de/docs/advanced/wsgi.md +++ b/docs/de/docs/advanced/wsgi.md @@ -6,13 +6,29 @@ Dazu können Sie die `WSGIMiddleware` verwenden und damit Ihre WSGI-Anwendung wr ## `WSGIMiddleware` verwenden { #using-wsgimiddleware } -Sie müssen `WSGIMiddleware` importieren. +/// info | Info + +Dafür muss `a2wsgi` installiert sein, z. B. mit `pip install a2wsgi`. + +/// + +Sie müssen `WSGIMiddleware` aus `a2wsgi` importieren. Wrappen Sie dann die WSGI-Anwendung (z. B. Flask) mit der Middleware. Und dann mounten Sie das auf einem Pfad. -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note | Hinweis + +Früher wurde empfohlen, `WSGIMiddleware` aus `fastapi.middleware.wsgi` zu verwenden, dies ist jetzt deprecatet. + +Stattdessen wird empfohlen, das Paket `a2wsgi` zu verwenden. Die Nutzung bleibt gleich. + +Stellen Sie lediglich sicher, dass das Paket `a2wsgi` installiert ist und importieren Sie `WSGIMiddleware` korrekt aus `a2wsgi`. + +/// ## Es testen { #check-it } diff --git a/docs/de/docs/deployment/docker.md b/docs/de/docs/deployment/docker.md index d4b74635d..1e28efe52 100644 --- a/docs/de/docs/deployment/docker.md +++ b/docs/de/docs/deployment/docker.md @@ -145,8 +145,6 @@ Es gibt andere Formate und Tools zum Definieren und Installieren von Paketabhän * Erstellen Sie eine `main.py`-Datei mit: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -158,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index 11fb6c983..3ce5cb27f 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Erstellen Sie eine Datei `main.py` mit: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): Wenn Ihr Code `async` / `await` verwendet, benutzen Sie `async def`: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ Sie sehen die alternative automatische Dokumentation (bereitgestellt von Requestbody-Deklarationen an. +Nun, da wir gesehen haben, wie `Path` und `Query` verwendet werden, schauen wir uns fortgeschrittenere Verwendungsmöglichkeiten von Requestbody-Deklarationen an. ## `Path`-, `Query`- und Body-Parameter vermischen { #mix-path-query-and-body-parameters } @@ -101,13 +101,13 @@ Natürlich können Sie auch, wann immer Sie das brauchen, weitere Query-Paramete Da einfache Werte standardmäßig als Query-Parameter interpretiert werden, müssen Sie `Query` nicht explizit hinzufügen, Sie können einfach schreiben: ```Python -q: Union[str, None] = None +q: str | None = None ``` -Oder in Python 3.10 und darüber: +Oder in Python 3.9: ```Python -q: str | None = None +q: Union[str, None] = None ``` Zum Beispiel: diff --git a/docs/de/docs/tutorial/path-operation-configuration.md b/docs/de/docs/tutorial/path-operation-configuration.md index 3427b3052..a06c85e57 100644 --- a/docs/de/docs/tutorial/path-operation-configuration.md +++ b/docs/de/docs/tutorial/path-operation-configuration.md @@ -52,7 +52,7 @@ In diesem Fall macht es Sinn, die Tags in einem `Enum` zu speichern. Sie können eine `summary` und eine `description` hinzufügen: -{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} ## Beschreibung mittels Docstring { #description-from-docstring } @@ -70,7 +70,7 @@ Es wird in der interaktiven Dokumentation verwendet: Sie können die Response mit dem Parameter `response_description` beschreiben: -{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | Info From b82993643ecb709f3d732794c93f2562ae64731e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 16:06:09 +0000 Subject: [PATCH 206/243] =?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 7da59e9f2..52aa38b8b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Translations +* 🌐 Update translations for pt (update-outdated). PR [#14833](https://github.com/fastapi/fastapi/pull/14833) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for ko (update-outdated). PR [#14835](https://github.com/fastapi/fastapi/pull/14835) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for es (update-outdated). PR [#14832](https://github.com/fastapi/fastapi/pull/14832) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for tr (update-outdated). PR [#14831](https://github.com/fastapi/fastapi/pull/14831) by [@tiangolo](https://github.com/tiangolo). From 2838dcb4a832ef65c3a98463593dda0aea0b982c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 16:06:44 +0000 Subject: [PATCH 207/243] =?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 52aa38b8b..e4864ce0f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Translations +* 🌐 Update translations for de (update-outdated). PR [#14836](https://github.com/fastapi/fastapi/pull/14836) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for pt (update-outdated). PR [#14833](https://github.com/fastapi/fastapi/pull/14833) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for ko (update-outdated). PR [#14835](https://github.com/fastapi/fastapi/pull/14835) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for es (update-outdated). PR [#14832](https://github.com/fastapi/fastapi/pull/14832) by [@tiangolo](https://github.com/tiangolo). From 457cd75c234bce2c1ec53abe9a6a1f6def0fcd1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 08:11:07 -0800 Subject: [PATCH 208/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20fr=20(translate-page)=20(#14837)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/fr/docs/deployment/manually.md | 198 +++++++++++++--------------- 1 file changed, 94 insertions(+), 104 deletions(-) diff --git a/docs/fr/docs/deployment/manually.md b/docs/fr/docs/deployment/manually.md index 3f99ccd9f..c0c388b02 100644 --- a/docs/fr/docs/deployment/manually.md +++ b/docs/fr/docs/deployment/manually.md @@ -1,33 +1,82 @@ -# Exécuter un serveur manuellement - Uvicorn +# Exécuter un serveur manuellement { #run-a-server-manually } -La principale chose dont vous avez besoin pour exécuter une application **FastAPI** sur une machine serveur distante est un programme serveur ASGI tel que **Uvicorn**. +## Utiliser la commande `fastapi run` { #use-the-fastapi-run-command } -Il existe 3 principales alternatives : +En bref, utilisez `fastapi run` pour servir votre application FastAPI : + +
+ +```console +$ fastapi run main.py + + FastAPI Starting production 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://0.0.0.0:8000 + server Documentation at http://0.0.0.0:8000/docs + + Logs: + + INFO Started server process [2306215] + INFO Waiting for application startup. + INFO Application startup complete. + INFO Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C + to quit) +``` + +
+ +Cela fonctionnerait pour la plupart des cas. 😎 + +Vous pourriez utiliser cette commande par exemple pour démarrer votre application **FastAPI** dans un conteneur, sur un serveur, etc. + +## Serveurs ASGI { #asgi-servers } + +Allons un peu plus en détail. + +FastAPI utilise un standard pour construire des frameworks web Python et des serveurs appelé ASGI. FastAPI est un framework web ASGI. + +La principale chose dont vous avez besoin pour exécuter une application **FastAPI** (ou toute autre application ASGI) sur une machine serveur distante est un programme serveur ASGI comme **Uvicorn**, c'est celui utilisé par défaut par la commande `fastapi`. + +Il existe plusieurs alternatives, notamment : *
Uvicorn : un serveur ASGI haute performance. -* Hypercorn : un serveur - ASGI compatible avec HTTP/2 et Trio entre autres fonctionnalités. -* Daphne : le serveur ASGI - conçu pour Django Channels. +* Hypercorn : un serveur ASGI compatible avec HTTP/2 et Trio entre autres fonctionnalités. +* Daphne : le serveur ASGI conçu pour Django Channels. +* Granian : un serveur HTTP Rust pour les applications Python. +* NGINX Unit : NGINX Unit est un environnement d'exécution d'applications web léger et polyvalent. -## Machine serveur et programme serveur +## Machine serveur et programme serveur { #server-machine-and-server-program } Il y a un petit détail sur les noms à garder à l'esprit. 💡 -Le mot "**serveur**" est couramment utilisé pour désigner à la fois l'ordinateur distant/cloud (la machine physique ou virtuelle) et également le programme qui s'exécute sur cette machine (par exemple, Uvicorn). +Le mot « serveur » est couramment utilisé pour désigner à la fois l'ordinateur distant/cloud (la machine physique ou virtuelle) et également le programme qui s'exécute sur cette machine (par exemple, Uvicorn). -Gardez cela à l'esprit lorsque vous lisez "serveur" en général, cela pourrait faire référence à l'une de ces deux choses. +Gardez cela à l'esprit lorsque vous lisez « serveur » en général, cela pourrait faire référence à l'une de ces deux choses. -Lorsqu'on se réfère à la machine distante, il est courant de l'appeler **serveur**, mais aussi **machine**, **VM** (machine virtuelle), **nœud**. Tout cela fait référence à un type de machine distante, exécutant Linux, en règle générale, sur laquelle vous exécutez des programmes. +Lorsqu'on se réfère à la machine distante, il est courant de l'appeler **serveur**, mais aussi **machine**, **VM** (machine virtuelle), **nœud**. Tout cela fait référence à un type de machine distante, exécutant normalement Linux, sur laquelle vous exécutez des programmes. +## Installer le programme serveur { #install-the-server-program } -## Installer le programme serveur +Lorsque vous installez FastAPI, il est fourni avec un serveur de production, Uvicorn, et vous pouvez le démarrer avec la commande `fastapi run`. -Vous pouvez installer un serveur compatible ASGI avec : +Mais vous pouvez également installer un serveur ASGI manuellement. -//// tab | Uvicorn +Vous devez créer un [environnement virtuel](../virtual-environments.md){.internal-link target=_blank}, l'activer, puis vous pouvez installer l'application serveur. -* Uvicorn, un serveur ASGI rapide comme l'éclair, basé sur uvloop et httptools. +Par exemple, pour installer Uvicorn :
@@ -39,39 +88,21 @@ $ pip install "uvicorn[standard]"
+Un processus similaire s'appliquerait à tout autre programme de serveur ASGI. + /// tip | Astuce En ajoutant `standard`, Uvicorn va installer et utiliser quelques dépendances supplémentaires recommandées. -Cela inclut `uvloop`, le remplaçant performant de `asyncio`, qui fournit le gros gain de performance en matière de concurrence. +Cela inclut `uvloop`, le remplaçant hautes performances de `asyncio`, qui fournit le gros gain de performance en matière de concurrence. + +Lorsque vous installez FastAPI avec quelque chose comme `pip install "fastapi[standard]"`, vous obtenez déjà `uvicorn[standard]` aussi. /// -//// +## Exécuter le programme serveur { #run-the-server-program } -//// tab | Hypercorn - -* Hypercorn, un serveur ASGI également compatible avec HTTP/2. - -
- -```console -$ pip install hypercorn - ----> 100% -``` - -
- -...ou tout autre serveur ASGI. - -//// - -## Exécutez le programme serveur - -Vous pouvez ensuite exécuter votre application de la même manière que vous l'avez fait dans les tutoriels, mais sans l'option `--reload`, par exemple : - -//// tab | Uvicorn +Si vous avez installé un serveur ASGI manuellement, vous devrez normalement passer une chaîne d'import dans un format spécial pour qu'il importe votre application FastAPI :
@@ -83,85 +114,44 @@ $ uvicorn main:app --host 0.0.0.0 --port 80
-//// +/// note | Remarque -//// tab | Hypercorn +La commande `uvicorn main:app` fait référence à : -
+* `main` : le fichier `main.py` (le « module » Python). +* `app` : l'objet créé dans `main.py` avec la ligne `app = FastAPI()`. -```console -$ hypercorn main:app --bind 0.0.0.0:80 +C'est équivalent à : -Running on 0.0.0.0:8080 over http (CTRL + C to quit) +```Python +from main import app ``` -
- -//// - -/// warning - -N'oubliez pas de supprimer l'option `--reload` si vous l'utilisiez. - - L'option `--reload` consomme beaucoup plus de ressources, est plus instable, etc. - - Cela aide beaucoup pendant le **développement**, mais vous **ne devriez pas** l'utiliser en **production**. - /// -## Hypercorn avec Trio +Chaque programme de serveur ASGI alternatif aurait une commande similaire, vous pouvez en lire plus dans leur documentation respective. -Starlette et **FastAPI** sont basés sur -AnyIO, qui les rend -compatibles avec asyncio, de la bibliothèque standard Python et -Trio. +/// warning | Alertes -Néanmoins, Uvicorn n'est actuellement compatible qu'avec asyncio, et il utilise normalement `uvloop`, le remplaçant hautes performances de `asyncio`. +Uvicorn et d'autres serveurs prennent en charge une option `--reload` utile pendant le développement. -Mais si vous souhaitez utiliser directement **Trio**, vous pouvez utiliser **Hypercorn** car il le prend en charge. ✨ +L'option `--reload` consomme beaucoup plus de ressources, est plus instable, etc. -### Installer Hypercorn avec Trio +Cela aide beaucoup pendant le **développement**, mais vous **ne devriez pas** l'utiliser en **production**. -Vous devez d'abord installer Hypercorn avec le support Trio : +/// -
+## Concepts de déploiement { #deployment-concepts } -```console -$ pip install "hypercorn[trio]" ----> 100% -``` +Ces exemples exécutent le programme serveur (par exemple Uvicorn), en démarrant **un seul processus**, à l'écoute sur toutes les IP (`0.0.0.0`) sur un port prédéfini (par exemple `80`). -
+C'est l'idée de base. Mais vous voudrez probablement vous occuper de certaines choses supplémentaires, comme : -### Exécuter avec Trio +* Sécurité - HTTPS +* Exécution au démarrage +* Redémarrages +* Réplication (le nombre de processus en cours d'exécution) +* Mémoire +* Étapes précédant le démarrage -Ensuite, vous pouvez passer l'option de ligne de commande `--worker-class` avec la valeur `trio` : - -
- -```console -$ hypercorn main:app --worker-class trio -``` - -
- -Et cela démarrera Hypercorn avec votre application en utilisant Trio comme backend. - -Vous pouvez désormais utiliser Trio en interne dans votre application. Ou mieux encore, vous pouvez utiliser AnyIO pour que votre code reste compatible avec Trio et asyncio. 🎉 - -## Concepts de déploiement - -Ces exemples lancent le programme serveur (e.g. Uvicorn), démarrant **un seul processus**, sur toutes les IPs (`0.0. -0.0`) sur un port prédéfini (par example, `80`). - -C'est l'idée de base. Mais vous vous préoccuperez probablement de certains concepts supplémentaires, tels que ... : - -* la sécurité - HTTPS -* l'exécution au démarrage -* les redémarrages -* la réplication (le nombre de processus en cours d'exécution) -* la mémoire -* les étapes précédant le démarrage - -Je vous en dirai plus sur chacun de ces concepts, sur la façon de les aborder, et donnerai quelques exemples concrets avec des stratégies pour les traiter dans les prochains chapitres. 🚀 +Je vous en dirai plus sur chacun de ces concepts, sur la manière d'y réfléchir, et donnerai quelques exemples concrets avec des stratégies pour les gérer dans les prochains chapitres. 🚀 From 464c359bb03f7ba72330198cc9f44df36194837d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 16:11:33 +0000 Subject: [PATCH 209/243] =?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 e4864ce0f..2fea9e79c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Translations +* 🌐 Update translations for fr (translate-page). PR [#14837](https://github.com/fastapi/fastapi/pull/14837) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for de (update-outdated). PR [#14836](https://github.com/fastapi/fastapi/pull/14836) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for pt (update-outdated). PR [#14833](https://github.com/fastapi/fastapi/pull/14833) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for ko (update-outdated). PR [#14835](https://github.com/fastapi/fastapi/pull/14835) by [@tiangolo](https://github.com/tiangolo). From b49435becdf53942be248fc2ffc7f53df5ac313c Mon Sep 17 00:00:00 2001 From: Jonathan Fulton Date: Thu, 5 Feb 2026 13:23:16 -0500 Subject: [PATCH 210/243] =?UTF-8?q?=E2=9C=A8=20Allow=20`Response`=20type?= =?UTF-8?q?=20hint=20as=20dependency=20annotation=20(#14794)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 5 +- tests/test_response_dependency.py | 173 ++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 tests/test_response_dependency.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index fc5dfed85..3037b233b 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -449,7 +449,9 @@ def analyze_param( depends = dataclasses.replace(depends, dependency=type_annotation) # Handle non-param type annotations like Request - if lenient_issubclass( + # Only apply special handling when there's no explicit Depends - if there's a Depends, + # the dependency will be called and its return value used instead of the special injection + if depends is None and lenient_issubclass( type_annotation, ( Request, @@ -460,7 +462,6 @@ def analyze_param( SecurityScopes, ), ): - assert depends is None, f"Cannot specify `Depends` for type {type_annotation!r}" assert field_info is None, ( f"Cannot specify FastAPI annotation for type {type_annotation!r}" ) diff --git a/tests/test_response_dependency.py b/tests/test_response_dependency.py new file mode 100644 index 000000000..38c359594 --- /dev/null +++ b/tests/test_response_dependency.py @@ -0,0 +1,173 @@ +"""Test using special types (Response, Request, BackgroundTasks) as dependency annotations. + +These tests verify that special FastAPI types can be used with Depends() annotations +and that the dependency injection system properly handles them. +""" + +from typing import Annotated + +from fastapi import BackgroundTasks, Depends, FastAPI, Request, Response +from fastapi.responses import JSONResponse +from fastapi.testclient import TestClient + + +def test_response_with_depends_annotated(): + """Response type hint should work with Annotated[Response, Depends(...)].""" + app = FastAPI() + + def modify_response(response: Response) -> Response: + response.headers["X-Custom"] = "modified" + return response + + @app.get("/") + def endpoint(response: Annotated[Response, Depends(modify_response)]): + return {"status": "ok"} + + client = TestClient(app) + resp = client.get("/") + + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} + assert resp.headers.get("X-Custom") == "modified" + + +def test_response_with_depends_default(): + """Response type hint should work with Response = Depends(...).""" + app = FastAPI() + + def modify_response(response: Response) -> Response: + response.headers["X-Custom"] = "modified" + return response + + @app.get("/") + def endpoint(response: Response = Depends(modify_response)): + return {"status": "ok"} + + client = TestClient(app) + resp = client.get("/") + + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} + assert resp.headers.get("X-Custom") == "modified" + + +def test_response_without_depends(): + """Regular Response injection should still work.""" + app = FastAPI() + + @app.get("/") + def endpoint(response: Response): + response.headers["X-Direct"] = "set" + return {"status": "ok"} + + client = TestClient(app) + resp = client.get("/") + + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} + assert resp.headers.get("X-Direct") == "set" + + +def test_response_dependency_chain(): + """Response dependency should work in a chain of dependencies.""" + app = FastAPI() + + def first_modifier(response: Response) -> Response: + response.headers["X-First"] = "1" + return response + + def second_modifier( + response: Annotated[Response, Depends(first_modifier)], + ) -> Response: + response.headers["X-Second"] = "2" + return response + + @app.get("/") + def endpoint(response: Annotated[Response, Depends(second_modifier)]): + return {"status": "ok"} + + client = TestClient(app) + resp = client.get("/") + + assert resp.status_code == 200 + assert resp.headers.get("X-First") == "1" + assert resp.headers.get("X-Second") == "2" + + +def test_response_dependency_returns_different_response_instance(): + """Dependency that returns a different Response instance should work. + + When a dependency returns a new Response object (e.g., JSONResponse) instead + of modifying the injected one, the returned response should be used and any + modifications to it in the endpoint should be preserved. + """ + app = FastAPI() + + def default_response() -> Response: + response = JSONResponse(content={"status": "ok"}) + response.headers["X-Custom"] = "initial" + return response + + @app.get("/") + def endpoint(response: Annotated[Response, Depends(default_response)]): + response.headers["X-Custom"] = "modified" + return response + + client = TestClient(app) + resp = client.get("/") + + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} + assert resp.headers.get("X-Custom") == "modified" + + +# Tests for Request type hint with Depends +def test_request_with_depends_annotated(): + """Request type hint should work in dependency chain.""" + app = FastAPI() + + def extract_request_info(request: Request) -> dict: + return { + "path": request.url.path, + "user_agent": request.headers.get("user-agent", "unknown"), + } + + @app.get("/") + def endpoint( + info: Annotated[dict, Depends(extract_request_info)], + ): + return info + + client = TestClient(app) + resp = client.get("/", headers={"user-agent": "test-agent"}) + + assert resp.status_code == 200 + assert resp.json() == {"path": "/", "user_agent": "test-agent"} + + +# Tests for BackgroundTasks type hint with Depends +def test_background_tasks_with_depends_annotated(): + """BackgroundTasks type hint should work with Annotated[BackgroundTasks, Depends(...)].""" + app = FastAPI() + task_results = [] + + def background_task(message: str): + task_results.append(message) + + def add_background_task(background_tasks: BackgroundTasks) -> BackgroundTasks: + background_tasks.add_task(background_task, "from dependency") + return background_tasks + + @app.get("/") + def endpoint( + background_tasks: Annotated[BackgroundTasks, Depends(add_background_task)], + ): + background_tasks.add_task(background_task, "from endpoint") + return {"status": "ok"} + + client = TestClient(app) + resp = client.get("/") + + assert resp.status_code == 200 + assert "from dependency" in task_results + assert "from endpoint" in task_results From 0dd42b746e17d33a74ea267a62eace9ccdccf345 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 18:23:44 +0000 Subject: [PATCH 211/243] =?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 2fea9e79c..43600561b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* ✨ Allow `Response` type hint as dependency annotation. PR [#14794](https://github.com/fastapi/fastapi/pull/14794) by [@jonathan-fulton](https://github.com/jonathan-fulton). + ### Docs * 📝 Update docs for translations. PR [#14830](https://github.com/fastapi/fastapi/pull/14830) by [@tiangolo](https://github.com/tiangolo). From 97145588f5442aa7c3830173813d5d1a2c893521 Mon Sep 17 00:00:00 2001 From: Albin Skott Date: Thu, 5 Feb 2026 19:34:34 +0100 Subject: [PATCH 212/243] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20PEP69?= =?UTF-8?q?5=20`TypeAliasType`=20(#13920)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: lokidev Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Yurii Motov Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 4 ++++ pyproject.toml | 1 + tests/test_dependency_pep695.py | 27 +++++++++++++++++++++++++++ uv.lock | 2 ++ 4 files changed, 34 insertions(+) create mode 100644 tests/test_dependency_pep695.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 3037b233b..e5ac51d53 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -66,6 +66,7 @@ from starlette.requests import HTTPConnection, Request from starlette.responses import Response from starlette.websockets import WebSocket from typing_extensions import Literal, get_args, get_origin +from typing_inspection.typing_objects import is_typealiastype multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' @@ -370,6 +371,9 @@ def analyze_param( depends = None type_annotation: Any = Any use_annotation: Any = Any + if is_typealiastype(annotation): + # unpack in case PEP 695 type syntax is used + annotation = annotation.__value__ if annotation is not inspect.Signature.empty: use_annotation = annotation type_annotation = annotation diff --git a/pyproject.toml b/pyproject.toml index 0f6bf1e4a..e62baa5f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ dependencies = [ "starlette>=0.40.0,<0.51.0", "pydantic>=2.7.0", "typing-extensions>=4.8.0", + "typing-inspection>=0.4.2", "annotated-doc>=0.0.2", ] diff --git a/tests/test_dependency_pep695.py b/tests/test_dependency_pep695.py new file mode 100644 index 000000000..ef5636638 --- /dev/null +++ b/tests/test_dependency_pep695.py @@ -0,0 +1,27 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from typing_extensions import TypeAliasType + + +async def some_value() -> int: + return 123 + + +DependedValue = TypeAliasType( + "DependedValue", Annotated[int, Depends(some_value)], type_params=() +) + + +def test_pep695_type_dependencies(): + app = FastAPI() + + @app.get("/") + async def get_with_dep(value: DependedValue) -> str: # noqa + return f"value: {value}" + + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200 + assert response.text == '"value: 123"' diff --git a/uv.lock b/uv.lock index 931a27021..db5d2e9b1 100644 --- a/uv.lock +++ b/uv.lock @@ -1021,6 +1021,7 @@ dependencies = [ { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] [package.optional-dependencies] @@ -1202,6 +1203,7 @@ requires-dist = [ { name = "pyyaml", marker = "extra == 'all'", specifier = ">=5.3.1" }, { name = "starlette", specifier = ">=0.40.0,<0.51.0" }, { name = "typing-extensions", specifier = ">=4.8.0" }, + { name = "typing-inspection", specifier = ">=0.4.2" }, { name = "ujson", marker = "extra == 'all'", specifier = ">=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.12.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.12.0" }, From 54f8aeeb9a15e4d5a12401ec5549840966df0087 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 18:34:59 +0000 Subject: [PATCH 213/243] =?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 43600561b..3280ad699 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Features +* ✨ Add support for PEP695 `TypeAliasType`. PR [#13920](https://github.com/fastapi/fastapi/pull/13920) by [@cstruct](https://github.com/cstruct). * ✨ Allow `Response` type hint as dependency annotation. PR [#14794](https://github.com/fastapi/fastapi/pull/14794) by [@jonathan-fulton](https://github.com/jonathan-fulton). ### Docs From c5fd75a321496b1f8212744bde217ec8ea956154 Mon Sep 17 00:00:00 2001 From: Kanetsuna Masaya <156692516+mkanetsuna@users.noreply.github.com> Date: Fri, 6 Feb 2026 03:41:43 +0900 Subject: [PATCH 214/243] =?UTF-8?q?=F0=9F=90=9B=20Fix=20using=20`Json[list?= =?UTF-8?q?[str]]`=20type=20(issue=20#10997)=20(#14616)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 12 +++++-- tests/test_json_type.py | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 tests/test_json_type.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index e5ac51d53..b8f7f948c 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -51,7 +51,7 @@ from fastapi.logger import logger from fastapi.security.oauth2 import SecurityScopes from fastapi.types import DependencyCacheKey from fastapi.utils import create_model_field, get_path_param_names -from pydantic import BaseModel +from pydantic import BaseModel, Json from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool @@ -726,11 +726,19 @@ def _validate_value_with_model_field( return v_, [] +def _is_json_field(field: ModelField) -> bool: + return any(type(item) is Json for item in field.field_info.metadata) + + def _get_multidict_value( field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None ) -> Any: alias = alias or get_validation_alias(field) - if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)): + if ( + (not _is_json_field(field)) + and is_sequence_field(field) + and isinstance(values, (ImmutableMultiDict, Headers)) + ): value = values.getlist(alias) else: value = values.get(alias, None) diff --git a/tests/test_json_type.py b/tests/test_json_type.py new file mode 100644 index 000000000..3e213eaca --- /dev/null +++ b/tests/test_json_type.py @@ -0,0 +1,63 @@ +import json +from typing import Annotated + +from fastapi import Cookie, FastAPI, Form, Header, Query +from fastapi.testclient import TestClient +from pydantic import Json + +app = FastAPI() + + +@app.post("/form-json-list") +def form_json_list(items: Annotated[Json[list[str]], Form()]) -> list[str]: + return items + + +@app.get("/query-json-list") +def query_json_list(items: Annotated[Json[list[str]], Query()]) -> list[str]: + return items + + +@app.get("/header-json-list") +def header_json_list(x_items: Annotated[Json[list[str]], Header()]) -> list[str]: + return x_items + + +@app.get("/cookie-json-list") +def cookie_json_list(items: Annotated[Json[list[str]], Cookie()]) -> list[str]: + return items + + +client = TestClient(app) + + +def test_form_json_list(): + response = client.post( + "/form-json-list", data={"items": json.dumps(["abc", "def"])} + ) + assert response.status_code == 200, response.text + assert response.json() == ["abc", "def"] + + +def test_query_json_list(): + response = client.get( + "/query-json-list", params={"items": json.dumps(["abc", "def"])} + ) + assert response.status_code == 200, response.text + assert response.json() == ["abc", "def"] + + +def test_header_json_list(): + response = client.get( + "/header-json-list", headers={"x-items": json.dumps(["abc", "def"])} + ) + assert response.status_code == 200, response.text + assert response.json() == ["abc", "def"] + + +def test_cookie_json_list(): + client.cookies.set("items", json.dumps(["abc", "def"])) + response = client.get("/cookie-json-list") + assert response.status_code == 200, response.text + assert response.json() == ["abc", "def"] + client.cookies.clear() From 6646e2b94f13fe4145560d02cef05cfd3a6bc925 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 18:42:10 +0000 Subject: [PATCH 215/243] =?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 3280ad699..6ec199a16 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -12,6 +12,10 @@ hide: * ✨ Add support for PEP695 `TypeAliasType`. PR [#13920](https://github.com/fastapi/fastapi/pull/13920) by [@cstruct](https://github.com/cstruct). * ✨ Allow `Response` type hint as dependency annotation. PR [#14794](https://github.com/fastapi/fastapi/pull/14794) by [@jonathan-fulton](https://github.com/jonathan-fulton). +### Fixes + +* 🐛 Fix using `Json[list[str]]` type (issue #10997). PR [#14616](https://github.com/fastapi/fastapi/pull/14616) by [@mkanetsuna](https://github.com/mkanetsuna). + ### Docs * 📝 Update docs for translations. PR [#14830](https://github.com/fastapi/fastapi/pull/14830) by [@tiangolo](https://github.com/tiangolo). From 23ddf09dd197ba099edc1b06eba61f7fc20a5ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 11:25:49 -0800 Subject: [PATCH 216/243] =?UTF-8?q?=F0=9F=94=A8=20Add=20max=20pages=20to?= =?UTF-8?q?=20translate=20to=20configs=20(#14840)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/translate.yml | 6 ++++++ scripts/translate.py | 23 ++++++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index 83518614b..bb23fa32d 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -35,6 +35,11 @@ on: type: boolean required: false default: false + max: + description: Maximum number of items to translate (e.g. 10) + type: number + required: false + default: 10 jobs: langs: @@ -115,3 +120,4 @@ jobs: EN_PATH: ${{ github.event.inputs.en_path }} COMMAND: ${{ matrix.command }} COMMIT_IN_PLACE: ${{ github.event.inputs.commit_in_place }} + MAX: ${{ github.event.inputs.max }} diff --git a/scripts/translate.py b/scripts/translate.py index 31c44583d..ddcfa311d 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -347,9 +347,12 @@ def list_outdated(language: str) -> list[Path]: @app.command() -def update_outdated(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None: +def update_outdated( + language: Annotated[str, typer.Option(envvar="LANGUAGE")], + max: Annotated[int, typer.Option(envvar="MAX")] = 10, +) -> None: outdated_paths = list_outdated(language) - for path in outdated_paths: + for path in outdated_paths[:max]: print(f"Updating lang: {language} path: {path}") translate_page(language=language, en_path=path) print(f"Done updating: {path}") @@ -357,9 +360,12 @@ def update_outdated(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) - @app.command() -def add_missing(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None: +def add_missing( + language: Annotated[str, typer.Option(envvar="LANGUAGE")], + max: Annotated[int, typer.Option(envvar="MAX")] = 10, +) -> None: missing_paths = list_missing(language) - for path in missing_paths: + for path in missing_paths[:max]: print(f"Adding lang: {language} path: {path}") translate_page(language=language, en_path=path) print(f"Done adding: {path}") @@ -367,11 +373,14 @@ def add_missing(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> No @app.command() -def update_and_add(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None: +def update_and_add( + language: Annotated[str, typer.Option(envvar="LANGUAGE")], + max: Annotated[int, typer.Option(envvar="MAX")] = 10, +) -> None: print(f"Updating outdated translations for {language}") - update_outdated(language=language) + update_outdated(language=language, max=max) print(f"Adding missing translations for {language}") - add_missing(language=language) + add_missing(language=language, max=max) print(f"Done updating and adding for {language}") From 8bdbd3725f7c88f3071ed9623da0945a269d3706 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 19:26:13 +0000 Subject: [PATCH 217/243] =?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 6ec199a16..6a7fb926c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -35,6 +35,10 @@ hide: * 🌐 Update translations for uk (update-outdated). PR [#14822](https://github.com/fastapi/fastapi/pull/14822) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update docs and translations scripts, enable Turkish. PR [#14824](https://github.com/fastapi/fastapi/pull/14824) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* 🔨 Add max pages to translate to configs. PR [#14840](https://github.com/fastapi/fastapi/pull/14840) by [@tiangolo](https://github.com/tiangolo). + ## 0.128.1 ### Features From 72325f698f7ab55e7df2f7fc40cc52693eadd687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 11:27:51 -0800 Subject: [PATCH 218/243] =?UTF-8?q?=F0=9F=8C=90=20Enable=20French=20docs?= =?UTF-8?q?=20translations=20(#14841)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/mkdocs.yml | 2 ++ scripts/docs.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index f713f2b12..38fe5f9ce 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -317,6 +317,8 @@ extra: name: de - Deutsch - link: /es/ name: es - español + - link: /fr/ + name: fr - français - link: /ja/ name: ja - 日本語 - link: /ko/ diff --git a/scripts/docs.py b/scripts/docs.py index c1fb84bba..4c065f945 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -23,7 +23,7 @@ SUPPORTED_LANGS = { "de", "en", "es", - # "fr", + "fr", "ja", "ko", "pt", From 110b45d9b245d42a114a965bbd3039458afbf95e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 19:28:17 +0000 Subject: [PATCH 219/243] =?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 6a7fb926c..9e8760e99 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -23,6 +23,7 @@ hide: ### Translations +* 🌐 Enable French docs translations. PR [#14841](https://github.com/fastapi/fastapi/pull/14841) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for fr (translate-page). PR [#14837](https://github.com/fastapi/fastapi/pull/14837) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for de (update-outdated). PR [#14836](https://github.com/fastapi/fastapi/pull/14836) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for pt (update-outdated). PR [#14833](https://github.com/fastapi/fastapi/pull/14833) by [@tiangolo](https://github.com/tiangolo). From 570e592a03c37c0436fea3727bd1be873417d9fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 11:42:18 -0800 Subject: [PATCH 220/243] =?UTF-8?q?=F0=9F=8C=90=20Enable=20Traditional=20C?= =?UTF-8?q?hinese=20translations=20(#14842)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/mkdocs.yml | 2 ++ scripts/docs.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 38fe5f9ce..ccf5ffc7a 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -331,6 +331,8 @@ extra: name: tr - Türkçe - link: /uk/ name: uk - українська мова + - link: /zh-hant/ + name: zh-hant - 繁體中文 extra_css: - css/termynal.css - css/custom.css diff --git a/scripts/docs.py b/scripts/docs.py index 4c065f945..2c7d6d5e4 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -31,7 +31,7 @@ SUPPORTED_LANGS = { "tr", "uk", # "zh", - # "zh-hant", + "zh-hant", } From de56c96c640dddb224c0088ed6d949efbf86d853 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 19:42:42 +0000 Subject: [PATCH 221/243] =?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 9e8760e99..85a2157e0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -38,6 +38,7 @@ hide: ### Internal +* 🌐 Enable Traditional Chinese translations. PR [#14842](https://github.com/fastapi/fastapi/pull/14842) by [@tiangolo](https://github.com/tiangolo). * 🔨 Add max pages to translate to configs. PR [#14840](https://github.com/fastapi/fastapi/pull/14840) by [@tiangolo](https://github.com/tiangolo). ## 0.128.1 From 79406a4b04078155ccc03e2d48ca0799836cfb4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Feb 2026 20:46:37 +0100 Subject: [PATCH 222/243] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?8.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 +++- fastapi/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 85a2157e0..c1a563f18 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.128.2 + ### Features * ✨ Add support for PEP695 `TypeAliasType`. PR [#13920](https://github.com/fastapi/fastapi/pull/13920) by [@cstruct](https://github.com/cstruct). @@ -23,6 +25,7 @@ hide: ### Translations +* 🌐 Enable Traditional Chinese translations. PR [#14842](https://github.com/fastapi/fastapi/pull/14842) by [@tiangolo](https://github.com/tiangolo). * 🌐 Enable French docs translations. PR [#14841](https://github.com/fastapi/fastapi/pull/14841) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for fr (translate-page). PR [#14837](https://github.com/fastapi/fastapi/pull/14837) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update translations for de (update-outdated). PR [#14836](https://github.com/fastapi/fastapi/pull/14836) by [@tiangolo](https://github.com/tiangolo). @@ -38,7 +41,6 @@ hide: ### Internal -* 🌐 Enable Traditional Chinese translations. PR [#14842](https://github.com/fastapi/fastapi/pull/14842) by [@tiangolo](https://github.com/tiangolo). * 🔨 Add max pages to translate to configs. PR [#14840](https://github.com/fastapi/fastapi/pull/14840) by [@tiangolo](https://github.com/tiangolo). ## 0.128.1 diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 160e7235c..99b9630d2 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.128.1" +__version__ = "0.128.2" from starlette import status as status From 3b8b310eda78d075ab8dd919bc17a9dce1c21f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Feb 2026 05:45:18 -0800 Subject: [PATCH 223/243] =?UTF-8?q?=F0=9F=91=B7=20Run=20tests=20with=20low?= =?UTF-8?q?er=20bound=20uv=20sync,=20upgrade=20`fastapi[all]`=20minimum=20?= =?UTF-8?q?dependencies:=20`ujson=20>=3D5.8.0`,=20`orjson=20>=3D3.9.3`=20(?= =?UTF-8?q?#14846)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 11 +- pyproject.toml | 6 +- uv.lock | 1152 ++++++++++++++++++++---------------- 3 files changed, 657 insertions(+), 512 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5cbbde61f..70710e340 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,31 +49,40 @@ jobs: matrix: os: [ windows-latest, macos-latest ] python-version: [ "3.14" ] + uv-resolution: + - highest include: - os: ubuntu-latest python-version: "3.9" coverage: coverage + uv-resolution: lowest-direct - os: macos-latest python-version: "3.10" coverage: coverage + uv-resolution: highest - os: windows-latest python-version: "3.12" coverage: coverage + uv-resolution: lowest-direct - os: ubuntu-latest python-version: "3.13" coverage: coverage + uv-resolution: highest # Ubuntu with 3.13 needs coverage for CodSpeed benchmarks - os: ubuntu-latest python-version: "3.13" coverage: coverage + uv-resolution: highest codspeed: codspeed - os: ubuntu-latest python-version: "3.14" coverage: coverage + uv-resolution: highest fail-fast: false runs-on: ${{ matrix.os }} env: UV_PYTHON: ${{ matrix.python-version }} + UV_RESOLUTION: ${{ matrix.uv-resolution }} steps: - name: Dump GitHub context env: @@ -92,7 +101,7 @@ jobs: pyproject.toml uv.lock - name: Install Dependencies - run: uv sync --locked --no-dev --group tests --extra all + run: uv sync --no-dev --group tests --extra all - run: mkdir coverage - name: Test if: matrix.codspeed != 'codspeed' diff --git a/pyproject.toml b/pyproject.toml index e62baa5f8..ae9ae891e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,9 +108,9 @@ all = [ # For Starlette's schema generation, would not be used with FastAPI "pyyaml >=5.3.1", # For UJSONResponse - "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0", + "ujson >=5.8.0", # For ORJSONResponse - "orjson >=3.2.1", + "orjson >=3.9.3", # To validate email fields "email-validator >=2.0.0", # Uvicorn with uvloop @@ -167,7 +167,7 @@ tests = [ "anyio[trio]>=3.2.1,<5.0.0", "coverage[toml]>=6.5.0,<8.0", "dirty-equals==0.9.0", - "flask>=1.1.2,<4.0.0", + "flask>=3.0.0,<4.0.0", "inline-snapshot>=0.21.1", "mypy==1.14.1", "pwdlib[argon2]>=0.2.1", diff --git a/uv.lock b/uv.lock index db5d2e9b1..435dddec5 100644 --- a/uv.lock +++ b/uv.lock @@ -51,7 +51,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.75.0" +version = "0.78.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -63,9 +63,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/51/32849a48f9b1cfe80a508fd269b20bd8f0b1357c70ba092890fde5a6a10b/anthropic-0.78.0.tar.gz", hash = "sha256:55fd978ab9b049c61857463f4c4e9e092b24f892519c6d8078cee1713d8af06e", size = 509136, upload-time = "2026-02-05T17:52:04.986Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/2f50931a942e5e13f80e24d83406714672c57964be593fc046d81369335b/anthropic-0.78.0-py3-none-any.whl", hash = "sha256:2a9887d2e99d1b0f9fe08857a1e9fe5d2d4030455dbf9ac65aab052e2efaeac4", size = 405485, upload-time = "2026-02-05T17:52:03.674Z" }, ] [[package]] @@ -184,11 +184,11 @@ wheels = [ [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] @@ -256,21 +256,21 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.24" +version = "1.42.43" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/21/8be0e3685c3a4868be48d8d2f6e5b4641727e1d8a5d396b8b401d2b5f06e/boto3-1.42.24.tar.gz", hash = "sha256:c47a2f40df933e3861fc66fd8d6b87ee36d4361663a7e7ba39a87f5a78b2eae1", size = 112788, upload-time = "2026-01-07T20:30:51.019Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/47/29afb754de7df0a0ebceaa9d83e209136ef7b62744259a6c09862fef4765/boto3-1.42.43.tar.gz", hash = "sha256:01fc5501209b23849fb30b01c6c086583ac91c40842a76083662fbfb84a82491", size = 112844, upload-time = "2026-02-05T20:31:44.974Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/75/bbfccb268f9faa4f59030888e859dca9797a980b77d6a074113af73bd4bf/boto3-1.42.24-py3-none-any.whl", hash = "sha256:8ed6ad670a5a2d7f66c1b0d3362791b48392c7a08f78479f5d8ab319a4d9118f", size = 140572, upload-time = "2026-01-07T20:30:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/80/92/584447b14ae70f57f133a4bc64393902a72a3087486a7c09ce1bab25263c/boto3-1.42.43-py3-none-any.whl", hash = "sha256:44ddcaa37c350333c5a4799f533e786a595a97f1ee2fd7fc3e394cdebeb15e44", size = 140603, upload-time = "2026-02-05T20:31:43.698Z" }, ] [[package]] name = "botocore" -version = "1.42.24" +version = "1.42.43" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, @@ -278,9 +278,9 @@ dependencies = [ { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/d7/bb4a4e839b238ffb67b002d7326b328ebe5eb23ed5180f2ca10399a802de/botocore-1.42.24.tar.gz", hash = "sha256:be8d1bea64fb91eea08254a1e5fea057e4428d08e61f4e11083a02cafc1f8cc6", size = 14878455, upload-time = "2026-01-07T20:30:40.379Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/d6/def916ad1d13de5d511074afcde538a958e2e8a7c7020fb698d1f392f63b/botocore-1.42.43.tar.gz", hash = "sha256:41d04ead0b0862eec21f841811fb5764fe370a2df9b319e0d5297325c50fba1b", size = 14934077, upload-time = "2026-02-05T20:31:35.15Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/d4/f2655d777eed8b069ecab3761454cb83f830f8be8b5b0d292e4b3a980d00/botocore-1.42.24-py3-none-any.whl", hash = "sha256:8fca9781d7c84f7ad070fceffaff7179c4aa7a5ffb27b43df9d1d957801e0a8d", size = 14551806, upload-time = "2026-01-07T20:30:38.103Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a8/95656f91b795eb47b73a00d36c51c7a5729eafa632c7348caa068ff63e50/botocore-1.42.43-py3-none-any.whl", hash = "sha256:1c0e30f62e274978ac3bcab253e3a859febea634b72b5e343589db7d17f83cd6", size = 14610179, upload-time = "2026-02-05T20:31:32.727Z" }, ] [[package]] @@ -326,7 +326,8 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -553,7 +554,7 @@ wheels = [ [[package]] name = "cohere" -version = "5.20.1" +version = "5.20.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastavro" }, @@ -566,9 +567,9 @@ dependencies = [ { name = "types-requests", version = "2.32.4.20260107", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/ed/bb02083654bdc089ae4ef1cd7691fd2233f1fd9f32bcbfacc80ff57d9775/cohere-5.20.1.tar.gz", hash = "sha256:50973f63d2c6138ff52ce37d8d6f78ccc539af4e8c43865e960d68e0bf835b6f", size = 180820, upload-time = "2025-12-18T16:39:50.975Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/37/65af9c50b5d5772a5528c6a949799f98ae286b8ebb924e0fac0619b3ae88/cohere-5.20.4.tar.gz", hash = "sha256:3b3017048ff5d5b4f113180947d538ca3d0f274de5875f0345be4c8cb3d5119a", size = 180737, upload-time = "2026-02-05T14:47:54.639Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/e3/94eb11ac3ebaaa3a6afb5d2ff23db95d58bc468ae538c388edf49f2f20b5/cohere-5.20.1-py3-none-any.whl", hash = "sha256:d230fd13d95ba92ae927fce3dd497599b169883afc7954fe29b39fb8d5df5fc7", size = 318973, upload-time = "2025-12-18T16:39:49.504Z" }, + { url = "https://files.pythonhosted.org/packages/6d/14/5c5077c6b01aed7a18dfc5ab775a35c7a6cb118e5bc1dafcfc06abdb9d9e/cohere-5.20.4-py3-none-any.whl", hash = "sha256:9cc6ebb0adac3d9f96ac0efffde6a2484534fb0aec3684a62c250d49da958f29", size = 318987, upload-time = "2026-02-05T14:47:53.505Z" }, ] [[package]] @@ -701,105 +702,105 @@ toml = [ [[package]] name = "coverage" -version = "7.13.1" +version = "7.13.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.10' and python_full_version < '3.14'", ] -sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, - { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, - { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, - { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, - { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, - { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, - { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, - { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, - { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, - { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, - { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, - { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, - { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, - { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, - { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, - { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, - { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, - { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, - { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, - { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, - { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, - { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, - { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, - { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, - { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, - { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, - { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, - { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, - { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, - { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, - { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, - { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, - { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, - { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, - { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, + { url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" }, + { url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663, upload-time = "2026-02-03T13:59:41.951Z" }, + { url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424, upload-time = "2026-02-03T13:59:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228, upload-time = "2026-02-03T13:59:44.816Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103, upload-time = "2026-02-03T13:59:46.271Z" }, + { url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107, upload-time = "2026-02-03T13:59:48.53Z" }, + { url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143, upload-time = "2026-02-03T13:59:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148, upload-time = "2026-02-03T13:59:52.025Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414, upload-time = "2026-02-03T13:59:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023, upload-time = "2026-02-03T13:59:55.454Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751, upload-time = "2026-02-03T13:59:57.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686, upload-time = "2026-02-03T13:59:58.825Z" }, + { url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" }, + { url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cc/560ad6f12010344d0778e268df5ba9aa990aacccc310d478bf82bf3d302c/coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", size = 252111, upload-time = "2026-02-03T14:00:05.639Z" }, + { url = "https://files.pythonhosted.org/packages/f0/66/3193985fb2c58e91f94cfbe9e21a6fdf941e9301fe2be9e92c072e9c8f8c/coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", size = 254217, upload-time = "2026-02-03T14:00:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c5/78/f0f91556bf1faa416792e537c523c5ef9db9b1d32a50572c102b3d7c45b3/coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", size = 250318, upload-time = "2026-02-03T14:00:09.224Z" }, + { url = "https://files.pythonhosted.org/packages/6f/aa/fc654e45e837d137b2c1f3a2cc09b4aea1e8b015acd2f774fa0f3d2ddeba/coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", size = 251909, upload-time = "2026-02-03T14:00:10.712Z" }, + { url = "https://files.pythonhosted.org/packages/73/4d/ab53063992add8a9ca0463c9d92cce5994a29e17affd1c2daa091b922a93/coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", size = 249971, upload-time = "2026-02-03T14:00:12.402Z" }, + { url = "https://files.pythonhosted.org/packages/29/25/83694b81e46fcff9899694a1b6f57573429cdd82b57932f09a698f03eea5/coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", size = 249692, upload-time = "2026-02-03T14:00:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ef/d68fc304301f4cb4bf6aefa0045310520789ca38dabdfba9dbecd3f37919/coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", size = 250597, upload-time = "2026-02-03T14:00:15.461Z" }, + { url = "https://files.pythonhosted.org/packages/8d/85/240ad396f914df361d0f71e912ddcedb48130c71b88dc4193fe3c0306f00/coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb", size = 221773, upload-time = "2026-02-03T14:00:17.462Z" }, + { url = "https://files.pythonhosted.org/packages/2f/71/165b3a6d3d052704a9ab52d11ea64ef3426745de517dda44d872716213a7/coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", size = 222711, upload-time = "2026-02-03T14:00:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/51/d0/0ddc9c5934cdd52639c5df1f1eb0fdab51bb52348f3a8d1c7db9c600d93a/coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", size = 221377, upload-time = "2026-02-03T14:00:20.968Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" }, + { url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/62/71/a18a53d1808e09b2e9ebd6b47dad5e92daf4c38b0686b4c4d1b2f3e42b7f/coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23", size = 254051, upload-time = "2026-02-03T14:00:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/4a/0a/eb30f6455d04c5a3396d0696cad2df0269ae7444bb322f86ffe3376f7bf9/coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34", size = 255160, upload-time = "2026-02-03T14:00:29.024Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/a45baac86274ce3ed842dbb84f14560c673ad30535f397d89164ec56c5df/coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8", size = 251709, upload-time = "2026-02-03T14:00:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/df/dd0dc12f30da11349993f3e218901fdf82f45ee44773596050c8f5a1fb25/coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a", size = 253083, upload-time = "2026-02-03T14:00:32.14Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/fc764c8389a8ce95cb90eb97af4c32f392ab0ac23ec57cadeefb887188d3/coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4", size = 251227, upload-time = "2026-02-03T14:00:34.721Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/d025e9da8f06f24c34d2da9873957cfc5f7e0d67802c3e34d0caa8452130/coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7", size = 250794, upload-time = "2026-02-03T14:00:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/45/c7/76bf35d5d488ec8f68682eb8e7671acc50a6d2d1c1182de1d2b6d4ffad3b/coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0", size = 252671, upload-time = "2026-02-03T14:00:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/bf/10/1921f1a03a7c209e1cb374f81a6b9b68b03cdb3ecc3433c189bc90e2a3d5/coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1", size = 221986, upload-time = "2026-02-03T14:00:40.442Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7c/f5d93297f8e125a80c15545edc754d93e0ed8ba255b65e609b185296af01/coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d", size = 222793, upload-time = "2026-02-03T14:00:42.106Z" }, + { url = "https://files.pythonhosted.org/packages/43/59/c86b84170015b4555ebabca8649bdf9f4a1f737a73168088385ed0f947c4/coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f", size = 221410, upload-time = "2026-02-03T14:00:43.726Z" }, + { url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468, upload-time = "2026-02-03T14:00:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839, upload-time = "2026-02-03T14:00:47.407Z" }, + { url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828, upload-time = "2026-02-03T14:00:49.029Z" }, + { url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432, upload-time = "2026-02-03T14:00:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672, upload-time = "2026-02-03T14:00:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050, upload-time = "2026-02-03T14:00:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801, upload-time = "2026-02-03T14:00:56.121Z" }, + { url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763, upload-time = "2026-02-03T14:00:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587, upload-time = "2026-02-03T14:01:01.197Z" }, + { url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358, upload-time = "2026-02-03T14:01:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007, upload-time = "2026-02-03T14:01:04.876Z" }, + { url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812, upload-time = "2026-02-03T14:01:07.296Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433, upload-time = "2026-02-03T14:01:09.156Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162, upload-time = "2026-02-03T14:01:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510, upload-time = "2026-02-03T14:01:13.038Z" }, + { url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801, upload-time = "2026-02-03T14:01:14.674Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882, upload-time = "2026-02-03T14:01:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306, upload-time = "2026-02-03T14:01:18.02Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051, upload-time = "2026-02-03T14:01:19.757Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868, upload-time = "2026-02-03T14:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498, upload-time = "2026-02-03T14:01:23.097Z" }, + { url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394, upload-time = "2026-02-03T14:01:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579, upload-time = "2026-02-03T14:01:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679, upload-time = "2026-02-03T14:01:28.553Z" }, + { url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740, upload-time = "2026-02-03T14:01:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996, upload-time = "2026-02-03T14:01:32.495Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513, upload-time = "2026-02-03T14:01:34.29Z" }, + { url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850, upload-time = "2026-02-03T14:01:35.984Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886, upload-time = "2026-02-03T14:01:38.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393, upload-time = "2026-02-03T14:01:40.118Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740, upload-time = "2026-02-03T14:01:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905, upload-time = "2026-02-03T14:01:44.163Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753, upload-time = "2026-02-03T14:01:45.974Z" }, + { url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716, upload-time = "2026-02-03T14:01:48.844Z" }, + { url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530, upload-time = "2026-02-03T14:01:50.793Z" }, + { url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186, upload-time = "2026-02-03T14:01:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253, upload-time = "2026-02-03T14:01:55.071Z" }, + { url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069, upload-time = "2026-02-03T14:01:56.95Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633, upload-time = "2026-02-03T14:01:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243, upload-time = "2026-02-03T14:02:01.109Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515, upload-time = "2026-02-03T14:02:02.862Z" }, + { url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874, upload-time = "2026-02-03T14:02:04.727Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004, upload-time = "2026-02-03T14:02:06.634Z" }, + { url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408, upload-time = "2026-02-03T14:02:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977, upload-time = "2026-02-03T14:02:11.755Z" }, + { url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868, upload-time = "2026-02-03T14:02:13.565Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474, upload-time = "2026-02-03T14:02:16.069Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317, upload-time = "2026-02-03T14:02:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635, upload-time = "2026-02-03T14:02:20.405Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035, upload-time = "2026-02-03T14:02:22.323Z" }, + { url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142, upload-time = "2026-02-03T14:02:24.143Z" }, + { url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166, upload-time = "2026-02-03T14:02:26.005Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, ] [package.optional-dependencies] @@ -809,79 +810,74 @@ toml = [ [[package]] name = "cross-web" -version = "0.4.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/4f/bdb62e969649ee76d4741ef8eee34384ec2bc21cc66eb7fd244e6ad62be8/cross_web-0.4.0.tar.gz", hash = "sha256:4ae65619ddfcd06d6803432c0366342d7e8aeba10194b4e144d73a662e75370c", size = 157111, upload-time = "2025-12-25T20:45:21.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/58/e688e99d1493c565d1587e64b499268d0a3129ae59f4efe440aac395f803/cross_web-0.4.1.tar.gz", hash = "sha256:0466295028dcae98c9ab3d18757f90b0e74fac2ff90efbe87e74657546d9993d", size = 157385, upload-time = "2026-01-09T18:17:41.534Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/d6/6c6a036655e5091b26b9f350dcf43821895325aa4727396b14c67679a957/cross_web-0.4.0-py3-none-any.whl", hash = "sha256:0c675bd26e91428cab31e3e927929b42da94aa96da92974e57c78f9a732d0e9b", size = 14200, upload-time = "2025-12-25T20:45:23.075Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/92b46b6e65f09b717a66c4e5a9bc47a45ebc83dd0e0ed126f8258363479d/cross_web-0.4.1-py3-none-any.whl", hash = "sha256:41b07c3a38253c517ec0603c1a366353aff77538946092b0f9a2235033f192c2", size = 14320, upload-time = "2026-01-09T18:17:40.325Z" }, ] [[package]] name = "cryptography" -version = "46.0.3" +version = "46.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, - { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, - { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, - { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, - { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, - { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, - { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, - { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, - { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, - { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, - { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, - { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" }, + { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, + { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" }, + { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, + { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, + { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, + { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" }, + { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" }, + { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" }, + { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" }, + { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" }, + { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" }, + { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, + { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, + { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, + { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" }, + { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, + { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, + { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, + { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" }, + { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, + { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, + { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, + { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, + { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" }, ] [[package]] @@ -1031,12 +1027,13 @@ all = [ { name = "httpx" }, { name = "itsdangerous" }, { name = "jinja2" }, - { name = "orjson" }, + { name = "orjson", version = "3.11.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "orjson", version = "3.11.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pydantic-extra-types" }, { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "python-multipart", version = "0.0.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "python-multipart", version = "0.0.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.22", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyyaml" }, { name = "ujson" }, { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, @@ -1051,7 +1048,7 @@ standard = [ { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "python-multipart", version = "0.0.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "python-multipart", version = "0.0.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.22", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, ] @@ -1064,7 +1061,7 @@ standard-no-fastapi-cloud-cli = [ { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "python-multipart", version = "0.0.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "python-multipart", version = "0.0.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.22", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, ] @@ -1076,7 +1073,7 @@ dev = [ { name = "black" }, { name = "cairosvg" }, { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, - { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "coverage", version = "7.13.3", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, { name = "dirty-equals" }, { name = "flask" }, { name = "gitpython" }, @@ -1107,7 +1104,7 @@ dev = [ { name = "ruff" }, { name = "sqlmodel" }, { name = "strawberry-graphql", version = "0.283.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "strawberry-graphql", version = "0.288.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "strawberry-graphql", version = "0.291.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "typer" }, { name = "types-orjson" }, { name = "types-ujson" }, @@ -1148,7 +1145,7 @@ tests = [ { name = "a2wsgi" }, { name = "anyio", extra = ["trio"] }, { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, - { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "coverage", version = "7.13.3", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, { name = "dirty-equals" }, { name = "flask" }, { name = "httpx" }, @@ -1163,7 +1160,7 @@ tests = [ { name = "ruff" }, { name = "sqlmodel" }, { name = "strawberry-graphql", version = "0.283.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "strawberry-graphql", version = "0.288.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "strawberry-graphql", version = "0.291.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "types-orjson" }, { name = "types-ujson" }, ] @@ -1189,7 +1186,7 @@ requires-dist = [ { name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" }, { name = "jinja2", marker = "extra == 'standard'", specifier = ">=3.1.5" }, { name = "jinja2", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=3.1.5" }, - { name = "orjson", marker = "extra == 'all'", specifier = ">=3.2.1" }, + { name = "orjson", marker = "extra == 'all'", specifier = ">=3.9.3" }, { name = "pydantic", specifier = ">=2.7.0" }, { name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" }, { name = "pydantic-extra-types", marker = "extra == 'standard'", specifier = ">=2.0.0" }, @@ -1204,7 +1201,7 @@ requires-dist = [ { name = "starlette", specifier = ">=0.40.0,<0.51.0" }, { name = "typing-extensions", specifier = ">=4.8.0" }, { name = "typing-inspection", specifier = ">=0.4.2" }, - { name = "ujson", marker = "extra == 'all'", specifier = ">=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0" }, + { name = "ujson", marker = "extra == 'all'", specifier = ">=5.8.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.12.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.12.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.12.0" }, @@ -1219,7 +1216,7 @@ dev = [ { name = "cairosvg", specifier = "==2.8.2" }, { name = "coverage", extras = ["toml"], specifier = ">=6.5.0,<8.0" }, { name = "dirty-equals", specifier = "==0.9.0" }, - { name = "flask", specifier = ">=1.1.2,<4.0.0" }, + { name = "flask", specifier = ">=3.0.0,<4.0.0" }, { name = "gitpython", specifier = "==3.1.46" }, { name = "griffe-typingdoc", specifier = "==0.3.0" }, { name = "griffe-warnings-deprecated", specifier = "==1.1.0" }, @@ -1287,7 +1284,7 @@ tests = [ { name = "anyio", extras = ["trio"], specifier = ">=3.2.1,<5.0.0" }, { name = "coverage", extras = ["toml"], specifier = ">=6.5.0,<8.0" }, { name = "dirty-equals", specifier = "==0.9.0" }, - { name = "flask", specifier = ">=1.1.2,<4.0.0" }, + { name = "flask", specifier = ">=3.0.0,<4.0.0" }, { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, { name = "inline-snapshot", specifier = ">=0.21.1" }, { name = "mypy", specifier = "==1.14.1" }, @@ -1337,7 +1334,7 @@ standard-no-fastapi-cloud-cli = [ [[package]] name = "fastapi-cloud-cli" -version = "0.8.0" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastar" }, @@ -1350,9 +1347,9 @@ dependencies = [ { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version < '3.10'" }, { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, extra = ["standard"], marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/5d/3b33438de35521fab4968b232caa9a4bd568a5078f2b2dfb7bb8a4528603/fastapi_cloud_cli-0.8.0.tar.gz", hash = "sha256:cf07c502528bfd9e6b184776659f05d9212811d76bbec9fbb6bf34bed4c7456f", size = 30257, upload-time = "2025-12-23T12:08:33.904Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/15/6c3d85d63964340fde6f36cc80f3f365d35f371e6a918d68ff3a3d588ef2/fastapi_cloud_cli-0.11.0.tar.gz", hash = "sha256:ecc83a5db106be35af528eccb01aa9bced1d29783efd48c8c1c831cf111eea99", size = 36170, upload-time = "2026-01-15T09:51:33.681Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/8e/abb95ef59e91bb5adaa2d18fbf9ea70fd524010bb03f406a2dd2a4775ef9/fastapi_cloud_cli-0.8.0-py3-none-any.whl", hash = "sha256:e9f40bee671d985fd25d7a5409b56d4f103777bf8a0c6d746ea5fbf97a8186d9", size = 22306, upload-time = "2025-12-23T12:08:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/60f79270a3320780be7e2ae8a1740cb98a692920b569ba420b97bcc6e175/fastapi_cloud_cli-0.11.0-py3-none-any.whl", hash = "sha256:76857b0f09d918acfcb50ade34682ba3b2079ca0c43fda10215de301f185a7f8", size = 26884, upload-time = "2026-01-15T09:51:34.471Z" }, ] [[package]] @@ -1569,15 +1566,15 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.2" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.10' and python_full_version < '3.14'", ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/e0/a75dbe4bca1e7d41307323dad5ea2efdd95408f74ab2de8bd7dba9b51a1a/filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64", size = 19510, upload-time = "2026-01-02T15:33:32.582Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8", size = 16697, upload-time = "2026-01-02T15:33:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] @@ -1613,15 +1610,15 @@ wheels = [ [[package]] name = "fsspec" -version = "2025.12.0" +version = "2026.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.10' and python_full_version < '3.14'", ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/27/954057b0d1f53f086f681755207dda6de6c660ce133c829158e8e8fe7895/fsspec-2025.12.0.tar.gz", hash = "sha256:c505de011584597b1060ff778bb664c1bc022e87921b0e4f10cc9c44f9635973", size = 309748, upload-time = "2025-12-03T15:23:42.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] [[package]] @@ -1663,15 +1660,16 @@ wheels = [ [[package]] name = "google-auth" -version = "2.47.0" +version = "2.48.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cryptography" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/3c/ec64b9a275ca22fa1cd3b6e77fefcf837b0732c890aa32d2bd21313d9b33/google_auth-2.47.0.tar.gz", hash = "sha256:833229070a9dfee1a353ae9877dcd2dec069a8281a4e72e72f77d4a70ff945da", size = 323719, upload-time = "2026-01-06T21:55:31.045Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl", hash = "sha256:c516d68336bfde7cf0da26aab674a36fedcf04b37ac4edd59c597178760c3498", size = 234867, upload-time = "2026-01-06T21:55:28.6Z" }, + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, ] [package.optional-dependencies] @@ -1692,7 +1690,7 @@ dependencies = [ { name = "httpx", marker = "python_full_version < '3.10'" }, { name = "pydantic", marker = "python_full_version < '3.10'" }, { name = "requests", marker = "python_full_version < '3.10'" }, - { name = "tenacity", marker = "python_full_version < '3.10'" }, + { name = "tenacity", version = "9.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, { name = "websockets", marker = "python_full_version < '3.10'" }, ] @@ -1703,7 +1701,7 @@ wheels = [ [[package]] name = "google-genai" -version = "1.57.0" +version = "1.62.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", @@ -1717,13 +1715,13 @@ dependencies = [ { name = "pydantic", marker = "python_full_version >= '3.10'" }, { name = "requests", marker = "python_full_version >= '3.10'" }, { name = "sniffio", marker = "python_full_version >= '3.10'" }, - { name = "tenacity", marker = "python_full_version >= '3.10'" }, + { name = "tenacity", version = "9.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, { name = "websockets", marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/b4/8251c2d2576224a4b51a8ab6159820f9200b8da28ff555c78ee15607096e/google_genai-1.57.0.tar.gz", hash = "sha256:0ff9c36b8d68abfbdbd13b703ece926de5f3e67955666b36315ecf669b94a826", size = 485648, upload-time = "2026-01-07T20:38:20.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/4c/71b32b5c8db420cf2fd0d5ef8a672adbde97d85e5d44a0b4fca712264ef1/google_genai-1.62.0.tar.gz", hash = "sha256:709468a14c739a080bc240a4f3191df597bf64485b1ca3728e0fb67517774c18", size = 490888, upload-time = "2026-02-04T22:48:41.989Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/02/858bdae08e2184b6afe0b18bc3113318522c9cf326a5a1698055edd31f88/google_genai-1.57.0-py3-none-any.whl", hash = "sha256:d63c7a89a1f549c4d14032f41a0cdb4b6fe3f565e2eee6b5e0907a0aeceabefd", size = 713323, upload-time = "2026-01-07T20:38:18.051Z" }, + { url = "https://files.pythonhosted.org/packages/09/5f/4645d8a28c6e431d0dd6011003a852563f3da7037d36af53154925b099fd/google_genai-1.62.0-py3-none-any.whl", hash = "sha256:4c3daeff3d05fafee4b9a1a31f9c07f01bc22051081aa58b4d61f58d16d1bcc0", size = 724166, upload-time = "2026-02-04T22:48:39.956Z" }, ] [[package]] @@ -1816,61 +1814,66 @@ wheels = [ [[package]] name = "greenlet" -version = "3.3.0" +version = "3.3.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.10' and python_full_version < '3.14'", ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/6a/33d1702184d94106d3cdd7bfb788e19723206fce152e303473ca3b946c7b/greenlet-3.3.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d", size = 273658, upload-time = "2025-12-04T14:23:37.494Z" }, - { url = "https://files.pythonhosted.org/packages/d6/b7/2b5805bbf1907c26e434f4e448cd8b696a0b71725204fa21a211ff0c04a7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb", size = 574810, upload-time = "2025-12-04T14:50:04.154Z" }, - { url = "https://files.pythonhosted.org/packages/94/38/343242ec12eddf3d8458c73f555c084359883d4ddc674240d9e61ec51fd6/greenlet-3.3.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd", size = 586248, upload-time = "2025-12-04T14:57:39.35Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/0ae86792fb212e4384041e0ef8e7bc66f59a54912ce407d26a966ed2914d/greenlet-3.3.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b299a0cb979f5d7197442dccc3aee67fce53500cd88951b7e6c35575701c980b", size = 597403, upload-time = "2025-12-04T15:07:10.831Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, - { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, - { url = "https://files.pythonhosted.org/packages/f4/6b/d4e73f5dfa888364bbf02efa85616c6714ae7c631c201349782e5b428925/greenlet-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b49e7ed51876b459bd645d83db257f0180e345d3f768a35a85437a24d5a49082", size = 300740, upload-time = "2025-12-04T14:47:52.773Z" }, - { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, - { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, - { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, - { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, - { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, - { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, - { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, - { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, - { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, - { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, - { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, - { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, - { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, - { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, - { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, - { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, - { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, - { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, - { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, - { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, - { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, - { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, + { url = "https://files.pythonhosted.org/packages/fe/65/5b235b40581ad75ab97dcd8b4218022ae8e3ab77c13c919f1a1dfe9171fd/greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13", size = 273723, upload-time = "2026-01-23T15:30:37.521Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ad/eb4729b85cba2d29499e0a04ca6fbdd8f540afd7be142fd571eea43d712f/greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4", size = 574874, upload-time = "2026-01-23T16:00:54.551Z" }, + { url = "https://files.pythonhosted.org/packages/87/32/57cad7fe4c8b82fdaa098c89498ef85ad92dfbb09d5eb713adedfc2ae1f5/greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5", size = 586309, upload-time = "2026-01-23T16:05:25.18Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/f041005cb87055e62b0d68680e88ec1a57f4688523d5e2fb305841bc8307/greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5", size = 597461, upload-time = "2026-01-23T16:15:51.943Z" }, + { url = "https://files.pythonhosted.org/packages/87/eb/8a1ec2da4d55824f160594a75a9d8354a5fe0a300fb1c48e7944265217e1/greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe", size = 586985, upload-time = "2026-01-23T15:32:47.968Z" }, + { url = "https://files.pythonhosted.org/packages/15/1c/0621dd4321dd8c351372ee8f9308136acb628600658a49be1b7504208738/greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729", size = 1547271, upload-time = "2026-01-23T16:04:18.977Z" }, + { url = "https://files.pythonhosted.org/packages/9d/53/24047f8924c83bea7a59c8678d9571209c6bfe5f4c17c94a78c06024e9f2/greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4", size = 1613427, upload-time = "2026-01-23T15:33:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ff/07/ac9bf1ec008916d1a3373cae212884c1dcff4a4ba0d41127ce81a8deb4e9/greenlet-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:7932f5f57609b6a3b82cc11877709aa7a98e3308983ed93552a1c377069b20c8", size = 226100, upload-time = "2026-01-23T15:30:56.957Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, + { url = "https://files.pythonhosted.org/packages/1f/54/dcf9f737b96606f82f8dd05becfb8d238db0633dd7397d542a296fe9cad3/greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b", size = 226462, upload-time = "2026-01-23T15:36:50.422Z" }, + { url = "https://files.pythonhosted.org/packages/91/37/61e1015cf944ddd2337447d8e97fb423ac9bc21f9963fb5f206b53d65649/greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4", size = 225715, upload-time = "2026-01-23T15:33:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, + { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, + { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, + { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" }, + { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" }, + { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, + { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, + { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" }, ] [[package]] @@ -2084,13 +2087,13 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.3.0" +version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "filelock", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "fsspec", version = "2025.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "fsspec", version = "2026.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, { name = "httpx" }, { name = "packaging" }, @@ -2100,9 +2103,9 @@ dependencies = [ { name = "typer-slim" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/c9/d42b5cfa0a50b77cf9165e13edfaf2e3bd4e0def9cb67b6b8a07224a52ab/huggingface_hub-1.3.0.tar.gz", hash = "sha256:289e2a3586fdf01e35882944eaa06fbd57436de24b6e653d1fab248584acd66b", size = 622092, upload-time = "2026-01-09T09:54:44.663Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/fc/eb9bc06130e8bbda6a616e1b80a7aa127681c448d6b49806f61db2670b61/huggingface_hub-1.4.1.tar.gz", hash = "sha256:b41131ec35e631e7383ab26d6146b8d8972abc8b6309b963b306fbcca87f5ed5", size = 642156, upload-time = "2026-02-06T09:20:03.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/5b/c5fde1f56b1f072b3028ec5413f3f5bf472c5891ebb34589cddb1689609f/huggingface_hub-1.3.0-py3-none-any.whl", hash = "sha256:763f450169bb05ea3867990e9d3ba9464eb617b874791301dc81be2c6ffb0bf5", size = 533092, upload-time = "2026-01-09T09:54:43.228Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ae/2f6d96b4e6c5478d87d606a1934b5d436c4a2bce6bb7c6fdece891c128e3/huggingface_hub-1.4.1-py3-none-any.whl", hash = "sha256:9931d075fb7a79af5abc487106414ec5fba2c0ae86104c0c62fd6cae38873d18", size = 553326, upload-time = "2026-02-06T09:20:00.728Z" }, ] [[package]] @@ -2205,120 +2208,120 @@ wheels = [ [[package]] name = "jiter" -version = "0.12.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652, upload-time = "2025-11-09T20:46:41.021Z" }, - { url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829, upload-time = "2025-11-09T20:46:43.281Z" }, - { url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568, upload-time = "2025-11-09T20:46:45.075Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052, upload-time = "2025-11-09T20:46:46.818Z" }, - { url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585, upload-time = "2025-11-09T20:46:48.319Z" }, - { url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541, upload-time = "2025-11-09T20:46:49.643Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423, upload-time = "2025-11-09T20:46:51.731Z" }, - { url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958, upload-time = "2025-11-09T20:46:53.432Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084, upload-time = "2025-11-09T20:46:54.848Z" }, - { url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054, upload-time = "2025-11-09T20:46:56.487Z" }, - { url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368, upload-time = "2025-11-09T20:46:58.638Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847, upload-time = "2025-11-09T20:47:00.295Z" }, - { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, - { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, - { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, - { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, - { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, - { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, - { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, - { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, - { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, - { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, - { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, - { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, - { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, - { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, - { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, - { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, - { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, - { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, - { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, - { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, - { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, - { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, - { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, - { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, - { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, - { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, - { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, - { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, - { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, - { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, - { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, - { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, - { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, - { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, - { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, - { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/7d/da/3e1fbd1f03f89ff0b4469d481be0b5cf2880c8e7b56fd80303b3ab5ae52d/jiter-0.12.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c9d28b218d5f9e5f69a0787a196322a5056540cb378cac8ff542b4fa7219966c", size = 319378, upload-time = "2025-11-09T20:48:51.761Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4e/e07d69285e9e19a153050a6d281d2f0968600753a8fed8a3a141d6ffc140/jiter-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0ee12028daf8cfcf880dd492349a122a64f42c059b6c62a2b0c96a83a8da820", size = 312195, upload-time = "2025-11-09T20:48:53.547Z" }, - { url = "https://files.pythonhosted.org/packages/2d/82/1f1cb5231b36af9f3d6d5b6030e70110faf14fd143419fc5fe7d852e691a/jiter-0.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b135ebe757a82d67ed2821526e72d0acf87dd61f6013e20d3c45b8048af927b", size = 352777, upload-time = "2025-11-09T20:48:55.058Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5e/728393bbbc99b31e8f7a4fdd8fa55e455a0a9648f79097d9088baf1f676f/jiter-0.12.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15d7fafb81af8a9e3039fc305529a61cd933eecee33b4251878a1c89859552a3", size = 370738, upload-time = "2025-11-09T20:48:56.632Z" }, - { url = "https://files.pythonhosted.org/packages/30/08/ac92f0df7b14ac82f2fe0a382a8000e600ab90af95798d4a7db0c1bd0736/jiter-0.12.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92d1f41211d8a8fe412faad962d424d334764c01dac6691c44691c2e4d3eedaf", size = 483744, upload-time = "2025-11-09T20:48:58.006Z" }, - { url = "https://files.pythonhosted.org/packages/7e/f4/dbfa4e759a2b82e969a14c3d0a91b176f1ed94717183a2f495cf94a651b9/jiter-0.12.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a64a48d7c917b8f32f25c176df8749ecf08cec17c466114727efe7441e17f6d", size = 382888, upload-time = "2025-11-09T20:48:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d9/b86fff7f748b0bb54222a8f132ffaf4d1be56b4591fa76d3cfdd701a33e5/jiter-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:122046f3b3710b85de99d9aa2f3f0492a8233a2f54a64902b096efc27ea747b5", size = 366465, upload-time = "2025-11-09T20:49:01.408Z" }, - { url = "https://files.pythonhosted.org/packages/93/3c/1152d8b433317a568927e13c1b125c680e6c058ff5d304833be8469bd4f2/jiter-0.12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:27ec39225e03c32c6b863ba879deb427882f243ae46f0d82d68b695fa5b48b40", size = 392603, upload-time = "2025-11-09T20:49:02.784Z" }, - { url = "https://files.pythonhosted.org/packages/6e/92/ff19d8fb87f3f9438eb7464862c8d0126455bc046b345d59b21443640c62/jiter-0.12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26b9e155ddc132225a39b1995b3b9f0fe0f79a6d5cbbeacf103271e7d309b404", size = 523780, upload-time = "2025-11-09T20:49:04.42Z" }, - { url = "https://files.pythonhosted.org/packages/87/3a/4260e2d84e4a293c36d2a8e8b8dcd69609c671f3bd310e4625359217c517/jiter-0.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ab05b7c58e29bb9e60b70c2e0094c98df79a1e42e397b9bb6eaa989b7a66dd0", size = 514874, upload-time = "2025-11-09T20:49:05.844Z" }, - { url = "https://files.pythonhosted.org/packages/2e/f7/574d2cb79e86feb035ade18c2254da71d04417555907c9df51dd6b183426/jiter-0.12.0-cp39-cp39-win32.whl", hash = "sha256:59f9f9df87ed499136db1c2b6c9efb902f964bed42a582ab7af413b6a293e7b0", size = 208329, upload-time = "2025-11-09T20:49:07.444Z" }, - { url = "https://files.pythonhosted.org/packages/05/ce/50725ec39782d8c973f19ae2d7dd3d192d01332c7cbde48c75e16a3e85a9/jiter-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3719596a1ebe7a48a498e8d5d0c4bf7553321d4c3eee1d620628d51351a3928", size = 206557, upload-time = "2025-11-09T20:49:08.888Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, - { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, - { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, - { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, - { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, - { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/41/95/8e6611379c9ce8534ff94dd800c50d6d0061b2c9ae6386fbcd86c7386f0a/jiter-0.13.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4397ee562b9f69d283e5674445551b47a5e8076fdde75e71bfac5891113dc543", size = 313635, upload-time = "2026-02-02T12:37:23.545Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/17db64dcaf84bbb187874232222030ea4d689e6008f93bda6e7c691bc4c7/jiter-0.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f90023f8f672e13ea1819507d2d21b9d2d1c18920a3b3a5f1541955a85b5504", size = 309761, upload-time = "2026-02-02T12:37:25.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/b2e2a7b12b94ecc7248acf2a8fe6288be893d1ebb9728655ceada22f00ad/jiter-0.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed0240dd1536a98c3ab55e929c60dfff7c899fecafcb7d01161b21a99fc8c363", size = 355245, upload-time = "2026-02-02T12:37:26.646Z" }, + { url = "https://files.pythonhosted.org/packages/77/3f/5b159663c5be622daec20074c997bb66bc1fac63c167c02aef3df476fb32/jiter-0.13.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6207fc61c395b26fffdcf637a0b06b4326f35bfa93c6e92fe1a166a21aeb6731", size = 365842, upload-time = "2026-02-02T12:37:28.207Z" }, + { url = "https://files.pythonhosted.org/packages/98/30/76a68fa2c9c815c6b7802a92fc354080d66ffba9acc4690fd85622f77ad4/jiter-0.13.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00203f47c214156df427b5989de74cb340c65c8180d09be1bf9de81d0abad599", size = 489223, upload-time = "2026-02-02T12:37:29.571Z" }, + { url = "https://files.pythonhosted.org/packages/a3/39/7c5cb85ccd71241513c878054c26a55828ccded6567d931a23ea4be73787/jiter-0.13.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c26ad6967c9dcedf10c995a21539c3aa57d4abad7001b7a84f621a263a6b605", size = 375762, upload-time = "2026-02-02T12:37:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6a/381cd18d050b0102e60324e8d3f51f37ef02c56e9f4e5f0b7d26ba18958d/jiter-0.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a576f5dce9ac7de5d350b8e2f552cf364f32975ed84717c35379a51c7cb198bd", size = 364996, upload-time = "2026-02-02T12:37:32.931Z" }, + { url = "https://files.pythonhosted.org/packages/37/1e/d66310f1f7085c13ea6f1119c9566ec5d2cfd1dc90df963118a6869247bb/jiter-0.13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b22945be8425d161f2e536cdae66da300b6b000f1c0ba3ddf237d1bfd45d21b8", size = 395463, upload-time = "2026-02-02T12:37:34.446Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ab/06ae77cb293f860b152c356c635c15aaa800ce48772865a41704d9fac80d/jiter-0.13.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6eeb7db8bc77dc20476bc2f7407a23dbe3d46d9cc664b166e3d474e1c1de4baa", size = 520944, upload-time = "2026-02-02T12:37:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8e/57b49b20361c42a80d455a6d83cb38626204508cab4298d6a22880205319/jiter-0.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:19cd6f85e1dc090277c3ce90a5b7d96f32127681d825e71c9dce28788e39fc0c", size = 554955, upload-time = "2026-02-02T12:37:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/79/dd/113489973c3b4256e383321aea11bd57389e401912fa48eb145a99b38767/jiter-0.13.0-cp39-cp39-win32.whl", hash = "sha256:dc3ce84cfd4fa9628fe62c4f85d0d597a4627d4242cfafac32a12cc1455d00f7", size = 206876, upload-time = "2026-02-02T12:37:39.225Z" }, + { url = "https://files.pythonhosted.org/packages/6e/73/2bdfc7133c5ee0c8f18cfe4a7582f3cfbbf3ff672cec1b5f4ca67ff9d041/jiter-0.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:9ffda299e417dc83362963966c50cb76d42da673ee140de8a8ac762d4bb2378b", size = 206404, upload-time = "2026-02-02T12:37:40.632Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, ] [[package]] name = "jmespath" -version = "1.0.1" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] [[package]] @@ -2362,11 +2365,11 @@ wheels = [ [[package]] name = "logfire-api" -version = "4.17.0" +version = "4.22.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/2a/76d8fbafa881cb03d5ad6e1d67d537e8c308ae7145812b8891f7b8751224/logfire_api-4.17.0.tar.gz", hash = "sha256:4647dad05146a68af441d59a7746a966df4c2581b316616f1210f8cf74931353", size = 58305, upload-time = "2026-01-07T10:52:17.768Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/c2/38871722347486f59f48dbe407541ff1e02398637b800778a2602f2353a7/logfire_api-4.22.0.tar.gz", hash = "sha256:b22a038e76c58cba1ab2ce7ce1a26aa5f3df8c63e84d6ad01fb22f75273ca830", size = 59723, upload-time = "2026-02-04T12:17:59.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/bc/3844e103dca998dcc195d6ef09e0f29d9000bac870117db1dd59a29bfeef/logfire_api-4.17.0-py3-none-any.whl", hash = "sha256:80a4b79cd9918934cdf2043d944cfb04182708178d846273484d47f3619a5a39", size = 96146, upload-time = "2026-01-07T10:52:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/4e8395f3e4b1951b1de8d06cb7d7ef1105efd294ea3443bf39b3594390af/logfire_api-4.22.0-py3-none-any.whl", hash = "sha256:9fd2a2dac9cc3adf71ad4dac7bc9d26af326a5aa87445be90026048d22b92a8e", size = 98543, upload-time = "2026-02-04T12:17:56.365Z" }, ] [[package]] @@ -2386,15 +2389,15 @@ wheels = [ [[package]] name = "markdown" -version = "3.10" +version = "3.10.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.10' and python_full_version < '3.14'", ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" }, ] [[package]] @@ -2403,7 +2406,7 @@ version = "0.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/47/ec9eae4a6d2f336d95681df43720e2b25b045dc3ed44ae6d30a5ce2f5dff/markdown_include_variants-0.0.8.tar.gz", hash = "sha256:46d812340c64dcd3646b1eaa356bafb31626dd7b4955d15c44ff8c48c6357227", size = 46882, upload-time = "2025-12-12T16:11:04.254Z" } wheels = [ @@ -2548,7 +2551,7 @@ dependencies = [ { name = "jsonschema", marker = "python_full_version >= '3.10'" }, { name = "pydantic", marker = "python_full_version >= '3.10'" }, { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "python-multipart", version = "0.0.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.22", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pywin32", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -2575,7 +2578,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cyclic" }, { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "rcslice" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bf/f0/f395a9cf164471d3c7bbe58cbd64d74289575a8b85a962b49a804ab7ed34/mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297", size = 15051, upload-time = "2022-07-26T05:46:14.129Z" } @@ -2622,7 +2625,7 @@ dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "markupsafe" }, { name = "mergedeep" }, { name = "mkdocs-get-deps" }, @@ -2643,7 +2646,7 @@ version = "1.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "markupsafe" }, { name = "mkdocs" }, ] @@ -2700,7 +2703,7 @@ dependencies = [ { name = "colorama" }, { name = "jinja2" }, { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "mkdocs" }, { name = "mkdocs-material-extensions" }, { name = "paginate" }, @@ -2742,7 +2745,7 @@ dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "markupsafe" }, { name = "mkdocs" }, { name = "mkdocs-autorefs" }, @@ -2851,7 +2854,7 @@ wheels = [ [[package]] name = "openai" -version = "2.14.0" +version = "2.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2863,9 +2866,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/b1/12fe1c196bea326261718eb037307c1c1fe1dedc2d2d4de777df822e6238/openai-2.14.0.tar.gz", hash = "sha256:419357bedde9402d23bf8f2ee372fca1985a73348debba94bddff06f19459952", size = 626938, upload-time = "2025-12-19T03:28:45.742Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/a2/677f22c4b487effb8a09439fb6134034b5f0a39ca27df8b95fac23a93720/openai-2.17.0.tar.gz", hash = "sha256:47224b74bd20f30c6b0a6a329505243cb2f26d5cf84d9f8d0825ff8b35e9c999", size = 631445, upload-time = "2026-02-05T16:27:40.953Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/4b/7c1a00c2c3fbd004253937f7520f692a9650767aa73894d7a34f0d65d3f4/openai-2.14.0-py3-none-any.whl", hash = "sha256:7ea40aca4ffc4c4a776e77679021b47eec1160e341f42ae086ba949c9dcc9183", size = 1067558, upload-time = "2025-12-19T03:28:43.727Z" }, + { url = "https://files.pythonhosted.org/packages/44/97/284535aa75e6e84ab388248b5a323fc296b1f70530130dee37f7f4fbe856/openai-2.17.0-py3-none-any.whl", hash = "sha256:4f393fd886ca35e113aac7ff239bcd578b81d8f104f5aedc7d3693eb2af1d338", size = 1069524, upload-time = "2026-02-05T16:27:38.941Z" }, ] [[package]] @@ -2885,6 +2888,9 @@ wheels = [ name = "orjson" version = "3.11.5" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" }, @@ -2975,6 +2981,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/e6/babf31154e047e465bc194eb72d1326d7c52ad4d7f50bf92b02b3cacda5c/orjson-3.11.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b94b947ac08586af635ef922d69dc9bc63321527a3a04647f4986a73f4bd30", size = 133189, upload-time = "2025-12-06T15:55:38.143Z" }, ] +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" }, + { url = "https://files.pythonhosted.org/packages/52/a2/fa129e749d500f9b183e8a3446a193818a25f60261e9ce143ad61e975208/orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67", size = 128670, upload-time = "2026-02-02T15:37:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/1e82011cd1e0bd051ef9d35bed1aa7fb4ea1f0a055dc2c841b46b43a9ebd/orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11", size = 123832, upload-time = "2026-02-02T15:37:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d8/a26b431ef962c7d55736674dddade876822f3e33223c1f47a36879350d04/orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc", size = 129171, upload-time = "2026-02-02T15:37:11.112Z" }, + { url = "https://files.pythonhosted.org/packages/a7/19/f47819b84a580f490da260c3ee9ade214cf4cf78ac9ce8c1c758f80fdfc9/orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16", size = 141967, upload-time = "2026-02-02T15:37:12.282Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cd/37ece39a0777ba077fdcdbe4cccae3be8ed00290c14bf8afdc548befc260/orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222", size = 130991, upload-time = "2026-02-02T15:37:13.465Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ed/f2b5d66aa9b6b5c02ff5f120efc7b38c7c4962b21e6be0f00fd99a5c348e/orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa", size = 133674, upload-time = "2026-02-02T15:37:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6e/baa83e68d1aa09fa8c3e5b2c087d01d0a0bd45256de719ed7bc22c07052d/orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e", size = 138722, upload-time = "2026-02-02T15:37:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/0c/47/7f8ef4963b772cd56999b535e553f7eb5cd27e9dd6c049baee6f18bfa05d/orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2", size = 409056, upload-time = "2026-02-02T15:37:17.895Z" }, + { url = "https://files.pythonhosted.org/packages/38/eb/2df104dd2244b3618f25325a656f85cc3277f74bbd91224752410a78f3c7/orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c", size = 144196, upload-time = "2026-02-02T15:37:19.349Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2a/ee41de0aa3a6686598661eae2b4ebdff1340c65bfb17fcff8b87138aab21/orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f", size = 134979, upload-time = "2026-02-02T15:37:20.906Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/92fc5d3d402b87a8b28277a9ed35386218a6a5287c7fe5ee9b9f02c53fb2/orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de", size = 127968, upload-time = "2026-02-02T15:37:23.178Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/a576bf36d73d60df06904d3844a9df08e25d59eba64363aaf8ec2f9bff41/orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993", size = 125128, upload-time = "2026-02-02T15:37:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +] + [[package]] name = "outcome" version = "1.3.0.post0" @@ -2989,11 +3080,11 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -3007,11 +3098,11 @@ wheels = [ [[package]] name = "pathspec" -version = "1.0.2" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/b9/6eb731b52f132181a9144bbe77ff82117f6b2d2fbfba49aaab2c014c4760/pathspec-1.0.2.tar.gz", hash = "sha256:fa32b1eb775ed9ba8d599b22c5f906dc098113989da2c00bf8b210078ca7fb92", size = 130502, upload-time = "2026-01-08T04:33:27.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl", hash = "sha256:62f8558917908d237d399b9b338ef455a814801a4688bc41074b25feefd93472", size = 54844, upload-time = "2026-01-08T04:33:26.4Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] @@ -3154,22 +3245,22 @@ wheels = [ [[package]] name = "playwright" -version = "1.57.0" +version = "1.58.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", version = "3.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "greenlet", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "greenlet", version = "3.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyee" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/b6/e17543cea8290ae4dced10be21d5a43c360096aa2cce0aa7039e60c50df3/playwright-1.57.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:9351c1ac3dfd9b3820fe7fc4340d96c0d3736bb68097b9b7a69bd45d25e9370c", size = 41985039, upload-time = "2025-12-09T08:06:18.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/04/ef95b67e1ff59c080b2effd1a9a96984d6953f667c91dfe9d77c838fc956/playwright-1.57.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4a9d65027bce48eeba842408bcc1421502dfd7e41e28d207e94260fa93ca67e", size = 40775575, upload-time = "2025-12-09T08:06:22.105Z" }, - { url = "https://files.pythonhosted.org/packages/60/bd/5563850322a663956c927eefcf1457d12917e8f118c214410e815f2147d1/playwright-1.57.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:99104771abc4eafee48f47dac2369e0015516dc1ce8c409807d2dd440828b9a4", size = 41985042, upload-time = "2025-12-09T08:06:25.357Z" }, - { url = "https://files.pythonhosted.org/packages/56/61/3a803cb5ae0321715bfd5247ea871d25b32c8f372aeb70550a90c5f586df/playwright-1.57.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:284ed5a706b7c389a06caa431b2f0ba9ac4130113c3a779767dda758c2497bb1", size = 45975252, upload-time = "2025-12-09T08:06:29.186Z" }, - { url = "https://files.pythonhosted.org/packages/83/d7/b72eb59dfbea0013a7f9731878df8c670f5f35318cedb010c8a30292c118/playwright-1.57.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a1bae6c0a07839cdeaddbc0756b3b2b85e476c07945f64ece08f1f956a86f1", size = 45706917, upload-time = "2025-12-09T08:06:32.549Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/3fc9ebd7c95ee54ba6a68d5c0bc23e449f7235f4603fc60534a364934c16/playwright-1.57.0-py3-none-win32.whl", hash = "sha256:1dd93b265688da46e91ecb0606d36f777f8eadcf7fbef12f6426b20bf0c9137c", size = 36553860, upload-time = "2025-12-09T08:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/58/d4/dcdfd2a33096aeda6ca0d15584800443dd2be64becca8f315634044b135b/playwright-1.57.0-py3-none-win_amd64.whl", hash = "sha256:6caefb08ed2c6f29d33b8088d05d09376946e49a73be19271c8cd5384b82b14c", size = 36553864, upload-time = "2025-12-09T08:06:38.915Z" }, - { url = "https://files.pythonhosted.org/packages/6a/60/fe31d7e6b8907789dcb0584f88be741ba388413e4fbce35f1eba4e3073de/playwright-1.57.0-py3-none-win_arm64.whl", hash = "sha256:5f065f5a133dbc15e6e7c71e7bc04f258195755b1c32a432b792e28338c8335e", size = 32837940, upload-time = "2025-12-09T08:06:42.268Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c9/9c6061d5703267f1baae6a4647bfd1862e386fbfdb97d889f6f6ae9e3f64/playwright-1.58.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:96e3204aac292ee639edbfdef6298b4be2ea0a55a16b7068df91adac077cc606", size = 42251098, upload-time = "2026-01-30T15:09:24.028Z" }, + { url = "https://files.pythonhosted.org/packages/e0/40/59d34a756e02f8c670f0fee987d46f7ee53d05447d43cd114ca015cb168c/playwright-1.58.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:70c763694739d28df71ed578b9c8202bb83e8fe8fb9268c04dd13afe36301f71", size = 41039625, upload-time = "2026-01-30T15:09:27.558Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ee/3ce6209c9c74a650aac9028c621f357a34ea5cd4d950700f8e2c4b7fe2c4/playwright-1.58.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:185e0132578733d02802dfddfbbc35f42be23a45ff49ccae5081f25952238117", size = 42251098, upload-time = "2026-01-30T15:09:30.461Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/009958cbf23fac551a940d34e3206e6c7eed2b8c940d0c3afd1feb0b0589/playwright-1.58.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:c95568ba1eda83812598c1dc9be60b4406dffd60b149bc1536180ad108723d6b", size = 46235268, upload-time = "2026-01-30T15:09:33.787Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a6/0e66ad04b6d3440dae73efb39540c5685c5fc95b17c8b29340b62abbd952/playwright-1.58.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9999948f1ab541d98812de25e3a8c410776aa516d948807140aff797b4bffa", size = 45964214, upload-time = "2026-01-30T15:09:36.751Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/236e60ab9f6d62ed0fd32150d61f1f494cefbf02304c0061e78ed80c1c32/playwright-1.58.0-py3-none-win32.whl", hash = "sha256:1e03be090e75a0fabbdaeab65ce17c308c425d879fa48bb1d7986f96bfad0b99", size = 36815998, upload-time = "2026-01-30T15:09:39.627Z" }, + { url = "https://files.pythonhosted.org/packages/41/f8/5ec599c5e59d2f2f336a05b4f318e733077cd5044f24adb6f86900c3e6a7/playwright-1.58.0-py3-none-win_amd64.whl", hash = "sha256:a2bf639d0ce33b3ba38de777e08697b0d8f3dc07ab6802e4ac53fb65e3907af8", size = 36816005, upload-time = "2026-01-30T15:09:42.449Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c4/cc0229fea55c87d6c9c67fe44a21e2cd28d1d558a5478ed4d617e9fb0c93/playwright-1.58.0-py3-none-win_arm64.whl", hash = "sha256:32ffe5c303901a13a0ecab91d1c3f74baf73b84f4bedbb6b935f5bc11cc98e1b", size = 33085919, upload-time = "2026-01-30T15:09:45.71Z" }, ] [[package]] @@ -3279,11 +3370,27 @@ wheels = [ name = "pycparser" version = "2.23" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -3361,7 +3468,7 @@ evals = [ ] google = [ { name = "google-genai", version = "1.47.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "google-genai", version = "1.57.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "google-genai", version = "1.62.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] groq = [ { name = "groq" }, @@ -3379,7 +3486,8 @@ openai = [ { name = "openai" }, ] retries = [ - { name = "tenacity" }, + { name = "tenacity", version = "9.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tenacity", version = "9.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] vertexai = [ { name = "google-auth" }, @@ -3652,16 +3760,16 @@ crypto = [ [[package]] name = "pymdown-extensions" -version = "10.20" +version = "10.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/35/e3814a5b7df295df69d035cfb8aab78b2967cdf11fcfae7faed726b66664/pymdown_extensions-10.20.tar.gz", hash = "sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52", size = 852774, upload-time = "2025-12-31T19:59:42.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/6c/9e370934bfa30e889d12e61d0dae009991294f40055c238980066a7fbd83/pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a", size = 852860, upload-time = "2026-01-24T05:56:56.758Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" }, + { url = "https://files.pythonhosted.org/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0", size = 268768, upload-time = "2026-01-24T05:56:54.537Z" }, ] [[package]] @@ -3782,15 +3890,15 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.21" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.10' and python_full_version < '3.14'", ] -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] @@ -3956,21 +4064,21 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] [[package]] name = "rich-toolkit" -version = "0.17.1" +version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -3978,9 +4086,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/09/3f9b8d9daaf235195c626f21e03604c05b987404ee3bcacee0c1f67f2a8e/rich_toolkit-0.17.1.tar.gz", hash = "sha256:5af54df8d1dd9c8530e462e1bdcaed625c9b49f5a55b035aa0ba1c17bdb87c9a", size = 187925, upload-time = "2025-12-17T10:49:22.583Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/f1/bcfbde3ca38db54b5dcf7ee3d0caf3ed9133a169aec5a58ad9ec50ba12e8/rich_toolkit-0.18.1.tar.gz", hash = "sha256:bf104f1945a7252debeda7d7138118eaf848fff5ea81d9eda556cbc5f911122c", size = 192514, upload-time = "2026-02-01T10:56:31.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/7b/15e55fa8a76d0d41bf34d965af78acdaf80a315907adb30de8b63c272694/rich_toolkit-0.17.1-py3-none-any.whl", hash = "sha256:96d24bb921ecd225ffce7c526a9149e74006410c05e6d405bd74ffd54d5631ed", size = 31412, upload-time = "2025-12-17T10:49:21.793Z" }, + { url = "https://files.pythonhosted.org/packages/da/43/6f9860c4bfb1f181c347941542a8955ce24b228f84550253765aa1854d53/rich_toolkit-0.18.1-py3-none-any.whl", hash = "sha256:04011a9751f4c2becdf44bd1aaff8562d4b00caf04f14e483a9873c15fbe3154", size = 32255, upload-time = "2026-02-01T10:56:33.071Z" }, ] [[package]] @@ -4304,16 +4412,16 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.49.0" +version = "2.52.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/94/23ac26616a883f492428d9ee9ad6eee391612125326b784dbfc30e1e7bab/sentry_sdk-2.49.0.tar.gz", hash = "sha256:c1878599cde410d481c04ef50ee3aedd4f600e4d0d253f4763041e468b332c30", size = 387228, upload-time = "2026-01-08T09:56:25.642Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/eb/1b497650eb564701f9a7b8a95c51b2abe9347ed2c0b290ba78f027ebe4ea/sentry_sdk-2.52.0.tar.gz", hash = "sha256:fa0bec872cfec0302970b2996825723d67390cdd5f0229fb9efed93bd5384899", size = 410273, upload-time = "2026-02-04T15:03:54.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/43/1c586f9f413765201234541857cb82fda076f4b0f7bad4a0ec248da39cf3/sentry_sdk-2.49.0-py2.py3-none-any.whl", hash = "sha256:6ea78499133874445a20fe9c826c9e960070abeb7ae0cdf930314ab16bb97aa0", size = 415693, upload-time = "2026-01-08T09:56:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/ca/63/2c6daf59d86b1c30600bff679d039f57fd1932af82c43c0bde1cbc55e8d4/sentry_sdk-2.52.0-py2.py3-none-any.whl", hash = "sha256:931c8f86169fc6f2752cb5c4e6480f0d516112e78750c312e081ababecbaf2ed", size = 435547, upload-time = "2026-02-04T15:03:51.567Z" }, ] [[package]] @@ -4376,59 +4484,66 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.45" +version = "2.0.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", version = "3.2.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.10' and platform_machine == 'AMD64') or (python_full_version < '3.10' and platform_machine == 'WIN32') or (python_full_version < '3.10' and platform_machine == 'aarch64') or (python_full_version < '3.10' and platform_machine == 'amd64') or (python_full_version < '3.10' and platform_machine == 'ppc64le') or (python_full_version < '3.10' and platform_machine == 'win32') or (python_full_version < '3.10' and platform_machine == 'x86_64')" }, - { name = "greenlet", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and platform_machine == 'AMD64') or (python_full_version >= '3.10' and platform_machine == 'WIN32') or (python_full_version >= '3.10' and platform_machine == 'aarch64') or (python_full_version >= '3.10' and platform_machine == 'amd64') or (python_full_version >= '3.10' and platform_machine == 'ppc64le') or (python_full_version >= '3.10' and platform_machine == 'win32') or (python_full_version >= '3.10' and platform_machine == 'x86_64')" }, + { name = "greenlet", version = "3.3.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and platform_machine == 'AMD64') or (python_full_version >= '3.10' and platform_machine == 'WIN32') or (python_full_version >= '3.10' and platform_machine == 'aarch64') or (python_full_version >= '3.10' and platform_machine == 'amd64') or (python_full_version >= '3.10' and platform_machine == 'ppc64le') or (python_full_version >= '3.10' and platform_machine == 'win32') or (python_full_version >= '3.10' and platform_machine == 'x86_64')" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/70/75b1387d72e2847220441166c5eb4e9846dd753895208c13e6d66523b2d9/sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85", size = 2154148, upload-time = "2025-12-10T20:03:21.023Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a4/7805e02323c49cb9d1ae5cd4913b28c97103079765f520043f914fca4cb3/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4", size = 3233051, upload-time = "2025-12-09T22:06:04.768Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ec/32ae09139f61bef3de3142e85c47abdee8db9a55af2bb438da54a4549263/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0", size = 3232781, upload-time = "2025-12-09T22:09:54.435Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bd/bf7b869b6f5585eac34222e1cf4405f4ba8c3b85dd6b1af5d4ce8bca695f/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0", size = 3182096, upload-time = "2025-12-09T22:06:06.169Z" }, - { url = "https://files.pythonhosted.org/packages/21/6a/c219720a241bb8f35c88815ccc27761f5af7fdef04b987b0e8a2c1a6dcaa/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826", size = 3205109, upload-time = "2025-12-09T22:09:55.969Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c4/6ccf31b2bc925d5d95fab403ffd50d20d7c82b858cf1a4855664ca054dce/sqlalchemy-2.0.45-cp310-cp310-win32.whl", hash = "sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a", size = 2114240, upload-time = "2025-12-09T21:29:54.007Z" }, - { url = "https://files.pythonhosted.org/packages/de/29/a27a31fca07316def418db6f7c70ab14010506616a2decef1906050a0587/sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl", hash = "sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7", size = 2137615, upload-time = "2025-12-09T21:29:55.85Z" }, - { url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, - { url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, - { url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, - { url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, - { url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, - { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, - { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, - { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, - { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, - { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, - { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, - { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, - { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, - { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, - { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, - { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, - { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, - { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, - { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, - { url = "https://files.pythonhosted.org/packages/53/01/a01b9829d146ba59972e6dfc88138142f5ffa4110e492c83326e7d765a17/sqlalchemy-2.0.45-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d29b2b99d527dbc66dd87c3c3248a5dd789d974a507f4653c969999fc7c1191b", size = 2157179, upload-time = "2025-12-10T20:05:13.998Z" }, - { url = "https://files.pythonhosted.org/packages/1f/78/ed43ed8ac27844f129adfc45a8735bab5dcad3e5211f4dc1bd7e676bc3ed/sqlalchemy-2.0.45-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59a8b8bd9c6bedf81ad07c8bd5543eedca55fe9b8780b2b628d495ba55f8db1e", size = 3233038, upload-time = "2025-12-09T22:06:55.42Z" }, - { url = "https://files.pythonhosted.org/packages/24/1c/721ec797f21431c905ad98cbce66430d72a340935e3b7e3232cf05e015cc/sqlalchemy-2.0.45-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd93c6f5d65f254ceabe97548c709e073d6da9883343adaa51bf1a913ce93f8e", size = 3233117, upload-time = "2025-12-09T22:10:03.143Z" }, - { url = "https://files.pythonhosted.org/packages/52/33/dcfb8dffb2ccd7c6803d63454dc1917ef5ec5b5e281fecbbc0ed1de1f125/sqlalchemy-2.0.45-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d0beadc2535157070c9c17ecf25ecec31e13c229a8f69196d7590bde8082bf1", size = 3182306, upload-time = "2025-12-09T22:06:56.894Z" }, - { url = "https://files.pythonhosted.org/packages/53/76/7cf8ce9e6dcac1d37125425aadec406d8a839dffc1b8763f6e7a56b0bf33/sqlalchemy-2.0.45-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e057f928ffe9c9b246a55b469c133b98a426297e1772ad24ce9f0c47d123bd5b", size = 3205587, upload-time = "2025-12-09T22:10:04.812Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ac/5cd0d14f7830981c06f468507237b0a8205691d626492b5551a67535eb30/sqlalchemy-2.0.45-cp39-cp39-win32.whl", hash = "sha256:c1c2091b1489435ff85728fafeb990f073e64f6f5e81d5cd53059773e8521eb6", size = 2115932, upload-time = "2025-12-09T22:09:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/b9/eb/76f6db8828c6e0cfac89820a07a40a2bab25e82e69827177b942a9bff42a/sqlalchemy-2.0.45-cp39-cp39-win_amd64.whl", hash = "sha256:56ead1f8dfb91a54a28cd1d072c74b3d635bcffbd25e50786533b822d4f2cde2", size = 2139570, upload-time = "2025-12-09T22:09:18.545Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, + { url = "https://files.pythonhosted.org/packages/40/26/66ba59328dc25e523bfcb0f8db48bdebe2035e0159d600e1f01c0fc93967/sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:895296687ad06dc9b11a024cf68e8d9d3943aa0b4964278d2553b86f1b267735", size = 2155051, upload-time = "2026-01-21T18:27:28.965Z" }, + { url = "https://files.pythonhosted.org/packages/21/cd/9336732941df972fbbfa394db9caa8bb0cf9fe03656ec728d12e9cbd6edc/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab65cb2885a9f80f979b85aa4e9c9165a31381ca322cbde7c638fe6eefd1ec39", size = 3234666, upload-time = "2026-01-21T18:32:28.72Z" }, + { url = "https://files.pythonhosted.org/packages/38/62/865ae8b739930ec433cd4123760bee7f8dafdc10abefd725a025604fb0de/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52fe29b3817bd191cc20bad564237c808967972c97fa683c04b28ec8979ae36f", size = 3232917, upload-time = "2026-01-21T18:44:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/38/805904b911857f2b5e00fdea44e9570df62110f834378706939825579296/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09168817d6c19954d3b7655da6ba87fcb3a62bb575fb396a81a8b6a9fadfe8b5", size = 3185790, upload-time = "2026-01-21T18:32:30.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/3260bb53aabd2d274856337456ea52f6a7eccf6cce208e558f870cec766b/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be6c0466b4c25b44c5d82b0426b5501de3c424d7a3220e86cd32f319ba56798e", size = 3207206, upload-time = "2026-01-21T18:44:55.93Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b3/67c432d7f9d88bb1a61909b67e29f6354d59186c168fb5d381cf438d3b73/sqlalchemy-2.0.46-cp310-cp310-win32.whl", hash = "sha256:1bc3f601f0a818d27bfe139f6766487d9c88502062a2cd3a7ee6c342e81d5047", size = 2115296, upload-time = "2026-01-21T18:33:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8c/25fb284f570f9d48e6c240f0269a50cec9cf009a7e08be4c0aaaf0654972/sqlalchemy-2.0.46-cp310-cp310-win_amd64.whl", hash = "sha256:e0c05aff5c6b1bb5fb46a87e0f9d2f733f83ef6cbbbcd5c642b6c01678268061", size = 2138540, upload-time = "2026-01-21T18:33:14.22Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/b42ad16800d0885105b59380ad69aad0cce5a65276e269ce2729a2343b6a/sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684", size = 2154851, upload-time = "2026-01-21T18:27:30.54Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/d8710068cb79f64d002ebed62a7263c00c8fd95f4ebd4b5be8f7ca93f2bc/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62", size = 3311241, upload-time = "2026-01-21T18:32:33.45Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/20c71487c7219ab3aa7421c7c62d93824c97c1460f2e8bb72404b0192d13/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f", size = 3310741, upload-time = "2026-01-21T18:44:57.887Z" }, + { url = "https://files.pythonhosted.org/packages/65/80/d26d00b3b249ae000eee4db206fcfc564bf6ca5030e4747adf451f4b5108/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01", size = 3263116, upload-time = "2026-01-21T18:32:35.044Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/74dda7506640923821340541e8e45bd3edd8df78664f1f2e0aae8077192b/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999", size = 3285327, upload-time = "2026-01-21T18:44:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/9f/25/6dcf8abafff1389a21c7185364de145107b7394ecdcb05233815b236330d/sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d", size = 2114564, upload-time = "2026-01-21T18:33:15.85Z" }, + { url = "https://files.pythonhosted.org/packages/93/5f/e081490f8523adc0088f777e4ebad3cac21e498ec8a3d4067074e21447a1/sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597", size = 2139233, upload-time = "2026-01-21T18:33:17.528Z" }, + { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" }, + { url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, + { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, + { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, + { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, + { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/9a/06/a29b51a577cc5746712ed8a2870794659a6bf405264b32dd5ccc380844d1/sqlalchemy-2.0.46-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:90bde6c6b1827565a95fde597da001212ab436f1b2e0c2dcc7246e14db26e2a3", size = 2158097, upload-time = "2026-01-21T18:24:45.892Z" }, + { url = "https://files.pythonhosted.org/packages/be/55/44689ed21b5a82708502243310878cfc76e0f326ed16103f4336f605055b/sqlalchemy-2.0.46-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b1e5f3a5f1ff4f42d5daab047428cd45a3380e51e191360a35cef71c9a7a2a", size = 3233722, upload-time = "2026-01-21T18:30:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/be/11/1d6024d9cdd2108d500b399bdc77a1738119789aa70c83d68e1012d32596/sqlalchemy-2.0.46-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93bb0aae40b52c57fd74ef9c6933c08c040ba98daf23ad33c3f9893494b8d3ce", size = 3233038, upload-time = "2026-01-21T18:32:26.945Z" }, + { url = "https://files.pythonhosted.org/packages/38/6d/f813e3204baea710f2d82a61821bdf7b39cebda6dbba7cdeb976b0552239/sqlalchemy-2.0.46-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4e2cc868b7b5208aec6c960950b7bb821f82c2fe66446c92ee0a571765e91a5", size = 3183163, upload-time = "2026-01-21T18:30:58.647Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5d/32b70643ef73c1bb3723a98316b89182cad2b9a6744d5335f1d69fcdb3f2/sqlalchemy-2.0.46-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:965c62be8256d10c11f8907e7a8d3e18127a4c527a5919d85fa87fd9ecc2cfdc", size = 3205174, upload-time = "2026-01-21T18:32:28.684Z" }, + { url = "https://files.pythonhosted.org/packages/99/a9/b9f7bd299b7550925e1e7d71d634e1eee23c035abed7de125fda7c74b0c8/sqlalchemy-2.0.46-cp39-cp39-win32.whl", hash = "sha256:9397b381dcee8a2d6b99447ae85ea2530dcac82ca494d1db877087a13e38926d", size = 2117095, upload-time = "2026-01-21T18:34:02.596Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/2e376b34a7c2f3d9d40811c3412fdc65cd35c6da2d660c283ad24bd9bab1/sqlalchemy-2.0.46-cp39-cp39-win_amd64.whl", hash = "sha256:4396c948d8217e83e2c202fbdcc0389cf8c93d2c1c5e60fa5c5a955eae0e64be", size = 2140517, upload-time = "2026-01-21T18:34:03.958Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, ] [[package]] @@ -4446,15 +4561,15 @@ wheels = [ [[package]] name = "sse-starlette" -version = "3.1.2" +version = "3.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "python_full_version >= '3.10'" }, { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303, upload-time = "2025-12-31T08:02:20.023Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484, upload-time = "2025-12-31T08:02:18.894Z" }, + { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, ] [[package]] @@ -4511,7 +4626,7 @@ wheels = [ [[package]] name = "strawberry-graphql" -version = "0.288.2" +version = "0.291.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", @@ -4524,9 +4639,9 @@ dependencies = [ { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/13/306dd2edb5f0c4aa51e2d071994a91ed1a0304e9feafa79ea0f04da51298/strawberry_graphql-0.288.2.tar.gz", hash = "sha256:853dbab407e3f5099f3a27dbf37786535894a0fbf150df5dde145fc290db607e", size = 215182, upload-time = "2026-01-01T20:01:19.277Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/18/5a07bc95bdf1cb73e93cd1f1d5b11750ded6f5e4d9407e7d500abc635b36/strawberry_graphql-0.291.1.tar.gz", hash = "sha256:13463d09e042857e4c12eee0eafc2bbdef963e4a30e20ed9a9c9fb0d487d4aac", size = 217765, upload-time = "2026-02-06T10:07:15.861Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/14/15abfa6d289048eeb1b1cca4a582db08a3c1f42784b485c21ef54617e2c7/strawberry_graphql-0.288.2-py3-none-any.whl", hash = "sha256:ad72d7904582db333158568751bb6186a872380a8cc6671159d011d279382542", size = 313137, upload-time = "2026-01-01T20:01:17.32Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/47ec56118decc90b9ae85a98180dc326fdad2ee031cba0e05c6c62349b88/strawberry_graphql-0.291.1-py3-none-any.whl", hash = "sha256:09798d3172b42be4d5526771ec581e1b1f93c7020b82a8b07b4c507676f75aa0", size = 316439, upload-time = "2026-02-06T10:07:18.247Z" }, ] [[package]] @@ -4545,11 +4660,27 @@ wheels = [ name = "tenacity" version = "9.1.2" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] +[[package]] +name = "tenacity" +version = "9.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/4a/c3357c8742f361785e3702bb4c9c68c4cb37a80aa657640b820669be5af1/tenacity-9.1.3.tar.gz", hash = "sha256:a6724c947aa717087e2531f883bde5c9188f603f6669a9b8d54eb998e604c12a", size = 49002, upload-time = "2026-02-05T06:33:12.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/6b/cdc85edb15e384d8e934aad89638cc8646e118c80de94c60125d0fc0a185/tenacity-9.1.3-py3-none-any.whl", hash = "sha256:51171cfc6b8a7826551e2f029426b10a6af189c5ac6986adcd7eb36d42f17954", size = 28858, upload-time = "2026-02-05T06:33:11.219Z" }, +] + [[package]] name = "termcolor" version = "3.1.0" @@ -4651,63 +4782,68 @@ wheels = [ [[package]] name = "tomli" -version = "2.3.0" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, - { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, - { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, - { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, - { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, - { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, - { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, - { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, - { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, - { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, - { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, - { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, - { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, - { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, - { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, - { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, - { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, - { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, - { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, - { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, - { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, - { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "tqdm" -version = "4.67.1" +version = "4.67.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[package]] @@ -5243,11 +5379,11 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.2.14" +version = "0.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/62/a7c072fbfefb2980a00f99ca994279cb9ecf310cb2e6b2a4d2a28fe192b3/wcwidth-0.5.3.tar.gz", hash = "sha256:53123b7af053c74e9fe2e92ac810301f6139e64379031f7124574212fb3b4091", size = 157587, upload-time = "2026-01-31T03:52:10.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/3c/c1/d73f12f8cdb1891334a2ccf7389eed244d3941e74d80dd220badb937f3fb/wcwidth-0.5.3-py3-none-any.whl", hash = "sha256:d584eff31cd4753e1e5ff6c12e1edfdb324c995713f75d26c29807bb84bf649e", size = 92981, upload-time = "2026-01-31T03:52:09.14Z" }, ] [[package]] From 8e50c55fd90c4433ddc9096ff439da9498a8314a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Feb 2026 13:45:47 +0000 Subject: [PATCH 224/243] =?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 c1a563f18..bd62fc063 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 👷 Run tests with lower bound uv sync, upgrade `fastapi[all]` minimum dependencies: `ujson >=5.8.0`, `orjson >=3.9.3`. PR [#14846](https://github.com/fastapi/fastapi/pull/14846) by [@tiangolo](https://github.com/tiangolo). + ## 0.128.2 ### Features From f9f79926047d7f5d8e0127dd585e7683ee57e9f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Feb 2026 07:18:30 -0800 Subject: [PATCH 225/243] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Re-implement=20`on?= =?UTF-8?q?=5Fevent`=20in=20FastAPI=20for=20compatibility=20with=20the=20n?= =?UTF-8?q?ext=20Starlette,=20while=20keeping=20backwards=20compatibility?= =?UTF-8?q?=20(#14851)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/routing.py | 157 +++++++++++++++++++++++++++++++++++- tests/test_router_events.py | 76 +++++++++++++++++ 2 files changed, 229 insertions(+), 4 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 9ca2f4673..c95f624bd 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1,22 +1,31 @@ +import contextlib import email.message import functools import inspect import json +import types from collections.abc import ( AsyncIterator, Awaitable, Collection, Coroutine, + Generator, Mapping, Sequence, ) -from contextlib import AsyncExitStack, asynccontextmanager +from contextlib import ( + AbstractAsyncContextManager, + AbstractContextManager, + AsyncExitStack, + asynccontextmanager, +) from enum import Enum, IntEnum from typing import ( Annotated, Any, Callable, Optional, + TypeVar, Union, ) @@ -143,6 +152,50 @@ def websocket_session( return app +_T = TypeVar("_T") + + +# Vendored from starlette.routing to avoid importing private symbols +class _AsyncLiftContextManager(AbstractAsyncContextManager[_T]): + """ + Wraps a synchronous context manager to make it async. + + This is vendored from Starlette to avoid importing private symbols. + """ + + def __init__(self, cm: AbstractContextManager[_T]) -> None: + self._cm = cm + + async def __aenter__(self) -> _T: + return self._cm.__enter__() + + async def __aexit__( + self, + exc_type: Optional[type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[types.TracebackType], + ) -> Optional[bool]: + return self._cm.__exit__(exc_type, exc_value, traceback) + + +# Vendored from starlette.routing to avoid importing private symbols +def _wrap_gen_lifespan_context( + lifespan_context: Callable[[Any], Generator[Any, Any, Any]], +) -> Callable[[Any], AbstractAsyncContextManager[Any]]: + """ + Wrap a generator-based lifespan context into an async context manager. + + This is vendored from Starlette to avoid importing private symbols. + """ + cmgr = contextlib.contextmanager(lifespan_context) + + @functools.wraps(cmgr) + def wrapper(app: Any) -> _AsyncLiftContextManager[Any]: + return _AsyncLiftContextManager(cmgr(app)) + + return wrapper + + def _merge_lifespan_context( original_context: Lifespan[Any], nested_context: Lifespan[Any] ) -> Lifespan[Any]: @@ -160,6 +213,30 @@ def _merge_lifespan_context( return merged_lifespan # type: ignore[return-value] +class _DefaultLifespan: + """ + Default lifespan context manager that runs on_startup and on_shutdown handlers. + + This is a copy of the Starlette _DefaultLifespan class that was removed + in Starlette. FastAPI keeps it to maintain backward compatibility with + on_startup and on_shutdown event handlers. + + Ref: https://github.com/Kludex/starlette/pull/3117 + """ + + def __init__(self, router: "APIRouter") -> None: + self._router = router + + async def __aenter__(self) -> None: + await self._router._startup() + + async def __aexit__(self, *exc_info: object) -> None: + await self._router._shutdown() + + def __call__(self: _T, app: object) -> _T: + return self + + # Cache for endpoint context to avoid re-extracting on every request _endpoint_context_cache: dict[int, EndpointContext] = {} @@ -903,13 +980,33 @@ class APIRouter(routing.Router): ), ] = Default(generate_unique_id), ) -> None: + # Handle on_startup/on_shutdown locally since Starlette removed support + # Ref: https://github.com/Kludex/starlette/pull/3117 + # TODO: deprecate this once the lifespan (or alternative) interface is improved + self.on_startup: list[Callable[[], Any]] = ( + [] if on_startup is None else list(on_startup) + ) + self.on_shutdown: list[Callable[[], Any]] = ( + [] if on_shutdown is None else list(on_shutdown) + ) + + # Determine the lifespan context to use + if lifespan is None: + # Use the default lifespan that runs on_startup/on_shutdown handlers + lifespan_context: Lifespan[Any] = _DefaultLifespan(self) + elif inspect.isasyncgenfunction(lifespan): + lifespan_context = asynccontextmanager(lifespan) + elif inspect.isgeneratorfunction(lifespan): + lifespan_context = _wrap_gen_lifespan_context(lifespan) + else: + lifespan_context = lifespan + self.lifespan_context = lifespan_context + super().__init__( routes=routes, redirect_slashes=redirect_slashes, default=default, - on_startup=on_startup, - on_shutdown=on_shutdown, - lifespan=lifespan, + lifespan=lifespan_context, ) if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" @@ -4473,6 +4570,58 @@ class APIRouter(routing.Router): generate_unique_id_function=generate_unique_id_function, ) + # TODO: remove this once the lifespan (or alternative) interface is improved + async def _startup(self) -> None: + """ + Run any `.on_startup` event handlers. + + This method is kept for backward compatibility after Starlette removed + support for on_startup/on_shutdown handlers. + + Ref: https://github.com/Kludex/starlette/pull/3117 + """ + for handler in self.on_startup: + if is_async_callable(handler): + await handler() + else: + handler() + + # TODO: remove this once the lifespan (or alternative) interface is improved + async def _shutdown(self) -> None: + """ + Run any `.on_shutdown` event handlers. + + This method is kept for backward compatibility after Starlette removed + support for on_startup/on_shutdown handlers. + + Ref: https://github.com/Kludex/starlette/pull/3117 + """ + for handler in self.on_shutdown: + if is_async_callable(handler): + await handler() + else: + handler() + + # TODO: remove this once the lifespan (or alternative) interface is improved + def add_event_handler( + self, + event_type: str, + func: Callable[[], Any], + ) -> None: + """ + Add an event handler function for startup or shutdown. + + This method is kept for backward compatibility after Starlette removed + support for on_startup/on_shutdown handlers. + + Ref: https://github.com/Kludex/starlette/pull/3117 + """ + assert event_type in ("startup", "shutdown") + if event_type == "startup": + self.on_startup.append(func) + else: + self.on_shutdown.append(func) + @deprecated( """ on_event is deprecated, use lifespan event handlers instead. diff --git a/tests/test_router_events.py b/tests/test_router_events.py index 9df299cda..65f2f521c 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -241,3 +241,79 @@ def test_merged_mixed_state_lifespans() -> None: with TestClient(app) as client: assert client.app_state == {"router": True} + + +@pytest.mark.filterwarnings( + r"ignore:\s*on_event is deprecated, use lifespan event handlers instead.*:DeprecationWarning" +) +def test_router_async_shutdown_handler(state: State) -> None: + """Test that async on_shutdown event handlers are called correctly, for coverage.""" + app = FastAPI() + + @app.get("/") + def main() -> dict[str, str]: + return {"message": "Hello World"} + + @app.on_event("shutdown") + async def app_shutdown() -> None: + state.app_shutdown = True + + assert state.app_shutdown is False + with TestClient(app) as client: + assert state.app_shutdown is False + response = client.get("/") + assert response.status_code == 200, response.text + assert state.app_shutdown is True + + +def test_router_sync_generator_lifespan(state: State) -> None: + """Test that a sync generator lifespan works via _wrap_gen_lifespan_context.""" + from collections.abc import Generator + + def lifespan(app: FastAPI) -> Generator[None, None, None]: + state.app_startup = True + yield + state.app_shutdown = True + + app = FastAPI(lifespan=lifespan) # type: ignore[arg-type] + + @app.get("/") + def main() -> dict[str, str]: + return {"message": "Hello World"} + + assert state.app_startup is False + assert state.app_shutdown is False + with TestClient(app) as client: + assert state.app_startup is True + assert state.app_shutdown is False + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello World"} + assert state.app_startup is True + assert state.app_shutdown is True + + +def test_router_async_generator_lifespan(state: State) -> None: + """Test that an async generator lifespan (not wrapped) works.""" + + async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + state.app_startup = True + yield + state.app_shutdown = True + + app = FastAPI(lifespan=lifespan) # type: ignore[arg-type] + + @app.get("/") + def main() -> dict[str, str]: + return {"message": "Hello World"} + + assert state.app_startup is False + assert state.app_shutdown is False + with TestClient(app) as client: + assert state.app_startup is True + assert state.app_shutdown is False + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello World"} + assert state.app_startup is True + assert state.app_shutdown is True From fe8c33ea64f797ae544e25e7ddb82c8342fd8880 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Feb 2026 15:18:55 +0000 Subject: [PATCH 226/243] =?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 bd62fc063..823465ddf 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ♻️ Re-implement `on_event` in FastAPI for compatibility with the next Starlette, while keeping backwards compatibility. PR [#14851](https://github.com/fastapi/fastapi/pull/14851) by [@tiangolo](https://github.com/tiangolo). + ### Internal * 👷 Run tests with lower bound uv sync, upgrade `fastapi[all]` minimum dependencies: `ujson >=5.8.0`, `orjson >=3.9.3`. PR [#14846](https://github.com/fastapi/fastapi/pull/14846) by [@tiangolo](https://github.com/tiangolo). From 08233d7ffca5b967489910f8bb57161c9b52090c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Feb 2026 07:28:59 -0800 Subject: [PATCH 227/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20ru=20(update-outdated)=20(#14834)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- .../ru/docs/advanced/advanced-dependencies.md | 2 +- docs/ru/docs/advanced/wsgi.md | 20 +++++++++++++++++-- docs/ru/docs/deployment/docker.md | 4 +--- docs/ru/docs/index.md | 18 ++++++----------- docs/ru/docs/tutorial/body-multiple-params.md | 8 ++++---- .../tutorial/path-operation-configuration.md | 6 +++--- 6 files changed, 33 insertions(+), 25 deletions(-) diff --git a/docs/ru/docs/advanced/advanced-dependencies.md b/docs/ru/docs/advanced/advanced-dependencies.md index cc6691b30..fb2643cd5 100644 --- a/docs/ru/docs/advanced/advanced-dependencies.md +++ b/docs/ru/docs/advanced/advanced-dependencies.md @@ -48,7 +48,7 @@ checker(q="somequery") ``` -…и передаст возвращённое значение как значение зависимости в нашу *функцию-обработчике пути* в параметр `fixed_content_included`: +…и передаст возвращённое значение как значение зависимости в параметр `fixed_content_included` нашей *функции-обработчика пути*: {* ../../docs_src/dependencies/tutorial011_an_py39.py hl[22] *} diff --git a/docs/ru/docs/advanced/wsgi.md b/docs/ru/docs/advanced/wsgi.md index 64d7c7a28..41d3a169c 100644 --- a/docs/ru/docs/advanced/wsgi.md +++ b/docs/ru/docs/advanced/wsgi.md @@ -6,13 +6,29 @@ ## Использование `WSGIMiddleware` { #using-wsgimiddleware } -Нужно импортировать `WSGIMiddleware`. +/// info | Информация + +Для этого требуется установить `a2wsgi`, например с помощью `pip install a2wsgi`. + +/// + +Нужно импортировать `WSGIMiddleware` из `a2wsgi`. Затем оберните WSGI‑приложение (например, Flask) в middleware (Промежуточный слой). После этого смонтируйте его на путь. -{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *} +{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *} + +/// note | Примечание + +Ранее рекомендовалось использовать `WSGIMiddleware` из `fastapi.middleware.wsgi`, но теперь он помечен как устаревший. + +Вместо него рекомендуется использовать пакет `a2wsgi`. Использование остаётся таким же. + +Просто убедитесь, что пакет `a2wsgi` установлен, и импортируйте `WSGIMiddleware` из `a2wsgi`. + +/// ## Проверьте { #check-it } diff --git a/docs/ru/docs/deployment/docker.md b/docs/ru/docs/deployment/docker.md index 3937b0165..9e8562be7 100644 --- a/docs/ru/docs/deployment/docker.md +++ b/docs/ru/docs/deployment/docker.md @@ -145,8 +145,6 @@ Successfully installed fastapi pydantic * Создайте файл `main.py` со следующим содержимым: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -158,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index 02b1c9a28..b1a0c9a2e 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -161,8 +161,6 @@ $ pip install "fastapi[standard]" Создайте файл `main.py` со следующим содержимым: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -174,7 +172,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None): Если ваш код использует `async` / `await`, используйте `async def`: -```Python hl_lines="9 14" -from typing import Union - +```Python hl_lines="7 12" from fastapi import FastAPI app = FastAPI() @@ -197,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -288,9 +284,7 @@ INFO: Application startup complete. Объявите тело запроса, используя стандартные типы Python, спасибо Pydantic. -```Python hl_lines="4 9-12 25-27" -from typing import Union - +```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel @@ -300,7 +294,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Union[bool, None] = None + is_offer: bool | None = None @app.get("/") @@ -309,7 +303,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} diff --git a/docs/ru/docs/tutorial/body-multiple-params.md b/docs/ru/docs/tutorial/body-multiple-params.md index 9ae57a311..9d9400494 100644 --- a/docs/ru/docs/tutorial/body-multiple-params.md +++ b/docs/ru/docs/tutorial/body-multiple-params.md @@ -101,13 +101,13 @@ Поскольку по умолчанию, отдельные значения интерпретируются как query-параметры, вам не нужно явно добавлять `Query`, вы можете просто сделать так: ```Python -q: Union[str, None] = None +q: str | None = None ``` -Или в Python 3.10 и выше: +Или в Python 3.9: ```Python -q: str | None = None +q: Union[str, None] = None ``` Например: @@ -116,7 +116,7 @@ q: str | None = None /// info | Информация -`Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`,`Path` и других, которые вы увидите позже. +`Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`, `Path` и других, которые вы увидите позже. /// diff --git a/docs/ru/docs/tutorial/path-operation-configuration.md b/docs/ru/docs/tutorial/path-operation-configuration.md index 96a54ffea..112a1efca 100644 --- a/docs/ru/docs/tutorial/path-operation-configuration.md +++ b/docs/ru/docs/tutorial/path-operation-configuration.md @@ -52,7 +52,7 @@ Вы можете добавить параметры `summary` и `description`: -{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} ## Описание из строк документации { #description-from-docstring } @@ -70,7 +70,7 @@ Вы можете указать описание ответа с помощью параметра `response_description`: -{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | Дополнительная информация @@ -78,7 +78,7 @@ /// -/// check +/// check | Проверка OpenAPI указывает, что каждой *операции пути* необходимо описание ответа. From 01e85c03bd992ef933fb1203879cd5a234796f78 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Feb 2026 15:29:27 +0000 Subject: [PATCH 228/243] =?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 823465ddf..bca40fa32 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * ♻️ Re-implement `on_event` in FastAPI for compatibility with the next Starlette, while keeping backwards compatibility. PR [#14851](https://github.com/fastapi/fastapi/pull/14851) by [@tiangolo](https://github.com/tiangolo). +### Translations + +* 🌐 Update translations for ru (update-outdated). PR [#14834](https://github.com/fastapi/fastapi/pull/14834) by [@tiangolo](https://github.com/tiangolo). + ### Internal * 👷 Run tests with lower bound uv sync, upgrade `fastapi[all]` minimum dependencies: `ujson >=5.8.0`, `orjson >=3.9.3`. PR [#14846](https://github.com/fastapi/fastapi/pull/14846) by [@tiangolo](https://github.com/tiangolo). From 19f13ead4cf61376e5bc615ebc8253e2ee4104b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Feb 2026 07:30:48 -0800 Subject: [PATCH 229/243] =?UTF-8?q?=F0=9F=91=B7=20Run=20tests=20with=20Sta?= =?UTF-8?q?rlette=20from=20git=20(#14849)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70710e340..e314b6a04 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,6 +51,9 @@ jobs: python-version: [ "3.14" ] uv-resolution: - highest + starlette-src: + - starlette-pypi + - starlette-git include: - os: ubuntu-latest python-version: "3.9" @@ -78,11 +81,13 @@ jobs: python-version: "3.14" coverage: coverage uv-resolution: highest + starlette-src: starlette-git fail-fast: false runs-on: ${{ matrix.os }} env: UV_PYTHON: ${{ matrix.python-version }} UV_RESOLUTION: ${{ matrix.uv-resolution }} + STARLETTE_SRC: ${{ matrix.starlette-src }} steps: - name: Dump GitHub context env: @@ -102,10 +107,13 @@ jobs: uv.lock - name: Install Dependencies run: uv sync --no-dev --group tests --extra all + - name: Install Starlette from source + if: matrix.starlette-src == 'starlette-git' + run: uv pip install "git+https://github.com/Kludex/starlette@main" - run: mkdir coverage - name: Test if: matrix.codspeed != 'codspeed' - run: uv run bash scripts/test.sh + run: uv run --no-sync bash scripts/test.sh env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} @@ -117,7 +125,7 @@ jobs: CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} with: mode: simulation - run: uv run coverage run -m pytest tests/ --codspeed + run: uv run --no-sync coverage run -m pytest tests/ --codspeed # Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow - name: Store coverage files if: matrix.coverage == 'coverage' From 201feedd681a26e3ed82628f963ad3a9af72d612 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Feb 2026 15:31:16 +0000 Subject: [PATCH 230/243] =?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 bca40fa32..2dc6c908d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Internal +* 👷 Run tests with Starlette from git. PR [#14849](https://github.com/fastapi/fastapi/pull/14849) by [@tiangolo](https://github.com/tiangolo). * 👷 Run tests with lower bound uv sync, upgrade `fastapi[all]` minimum dependencies: `ujson >=5.8.0`, `orjson >=3.9.3`. PR [#14846](https://github.com/fastapi/fastapi/pull/14846) by [@tiangolo](https://github.com/tiangolo). ## 0.128.2 From f6cc650a127070ff427ef4d7b4666d304b1cc27e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Feb 2026 08:37:37 -0800 Subject: [PATCH 231/243] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Starlett?= =?UTF-8?q?e=20supported=20version=20range=20to=20`starlette>=3D0.40.0,<1.?= =?UTF-8?q?0.0`=20(#14853)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ae9ae891e..519f45e31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.40.0,<0.51.0", + "starlette>=0.40.0,<1.0.0", "pydantic>=2.7.0", "typing-extensions>=4.8.0", "typing-inspection>=0.4.2", diff --git a/uv.lock b/uv.lock index 435dddec5..88b9f28df 100644 --- a/uv.lock +++ b/uv.lock @@ -1198,7 +1198,7 @@ requires-dist = [ { name = "python-multipart", marker = "extra == 'standard'", specifier = ">=0.0.18" }, { name = "python-multipart", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.0.18" }, { name = "pyyaml", marker = "extra == 'all'", specifier = ">=5.3.1" }, - { name = "starlette", specifier = ">=0.40.0,<0.51.0" }, + { name = "starlette", specifier = ">=0.40.0,<1.0.0" }, { name = "typing-extensions", specifier = ">=4.8.0" }, { name = "typing-inspection", specifier = ">=0.4.2" }, { name = "ujson", marker = "extra == 'all'", specifier = ">=5.8.0" }, From 661cdfb8a465ae8f629b2605fc4a9171f19200fc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Feb 2026 16:38:15 +0000 Subject: [PATCH 232/243] =?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 2dc6c908d..c78265512 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * ♻️ Re-implement `on_event` in FastAPI for compatibility with the next Starlette, while keeping backwards compatibility. PR [#14851](https://github.com/fastapi/fastapi/pull/14851) by [@tiangolo](https://github.com/tiangolo). +### Upgrades + +* ⬆️ Upgrade Starlette supported version range to `starlette>=0.40.0,<1.0.0`. PR [#14853](https://github.com/fastapi/fastapi/pull/14853) by [@tiangolo](https://github.com/tiangolo). + ### Translations * 🌐 Update translations for ru (update-outdated). PR [#14834](https://github.com/fastapi/fastapi/pull/14834) by [@tiangolo](https://github.com/tiangolo). From 36985f5f25a5a706e5c9bd4630a3c74f45f6bfe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Feb 2026 17:44:11 +0100 Subject: [PATCH 233/243] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.12?= =?UTF-8?q?8.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c78265512..a55a15abd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.128.3 + ### Refactors * ♻️ Re-implement `on_event` in FastAPI for compatibility with the next Starlette, while keeping backwards compatibility. PR [#14851](https://github.com/fastapi/fastapi/pull/14851) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 99b9630d2..b23e3c42f 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.128.2" +__version__ = "0.128.3" from starlette import status as status From 5a31b37cc7f12d006d7daebdc82b623e25b3ed2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Feb 2026 09:18:30 -0800 Subject: [PATCH 234/243] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20developm?= =?UTF-8?q?ent=20dependencies=20(#14854)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 88 +- uv.lock | 2941 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 2705 insertions(+), 324 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 519f45e31..4895c2d34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,62 +129,62 @@ dev = [ { include-group = "tests" }, { include-group = "docs" }, { include-group = "translations" }, - "playwright>=1.57.0", - "prek==0.2.22", + "playwright >=1.57.0", + "prek >=0.2.22", ] docs = [ { include-group = "docs-tests" }, - "black==25.1.0", - "cairosvg==2.8.2", - "griffe-typingdoc==0.3.0", - "griffe-warnings-deprecated==1.1.0", - "jieba==0.42.1", - "markdown-include-variants==0.0.8", - "mdx-include>=1.4.1,<2.0.0", - "mkdocs-macros-plugin==1.5.0", - "mkdocs-material==9.7.0", - "mkdocs-redirects>=1.2.1,<1.3.0", - "mkdocstrings[python]==0.30.1", - "pillow==11.3.0", - "python-slugify==8.0.4", - "pyyaml>=5.3.1,<7.0.0", - "typer==0.21.1", + "black >=25.1.0", + "cairosvg >=2.8.2", + "griffe-typingdoc >=0.3.0", + "griffe-warnings-deprecated >=1.1.0", + "jieba >=0.42.1", + "markdown-include-variants >=0.0.8", + "mdx-include >=1.4.1,<2.0.0", + "mkdocs-macros-plugin >=1.5.0", + "mkdocs-material >=9.7.0", + "mkdocs-redirects >=1.2.1,<1.3.0", + "mkdocstrings[python] >=0.30.1", + "pillow >=11.3.0", + "python-slugify >=8.0.4", + "pyyaml >=5.3.1,<7.0.0", + "typer >=0.21.1", ] docs-tests = [ - "httpx>=0.23.0,<1.0.0", - "ruff==0.14.14", + "httpx >=0.23.0,<1.0.0", + "ruff >=0.14.14", ] github-actions = [ - "httpx>=0.27.0,<1.0.0", - "pydantic>=2.5.3,<3.0.0", - "pydantic-settings>=2.1.0,<3.0.0", - "pygithub>=2.3.0,<3.0.0", - "pyyaml>=5.3.1,<7.0.0", - "smokeshow>=0.5.0", + "httpx >=0.27.0,<1.0.0", + "pydantic >=2.5.3,<3.0.0", + "pydantic-settings >=2.1.0,<3.0.0", + "pygithub >=2.3.0,<3.0.0", + "pyyaml >=5.3.1,<7.0.0", + "smokeshow >=0.5.0", ] tests = [ { include-group = "docs-tests" }, - "anyio[trio]>=3.2.1,<5.0.0", - "coverage[toml]>=6.5.0,<8.0", - "dirty-equals==0.9.0", - "flask>=3.0.0,<4.0.0", - "inline-snapshot>=0.21.1", - "mypy==1.14.1", - "pwdlib[argon2]>=0.2.1", - "pyjwt==2.9.0", - "pytest>=7.1.3,<9.0.0", - "pytest-codspeed==4.2.0", - "pyyaml>=5.3.1,<7.0.0", - "sqlmodel==0.0.31", - "strawberry-graphql>=0.200.0,<1.0.0", - "types-orjson==3.6.2", - "types-ujson==5.10.0.20240515", - "a2wsgi>=1.9.0,<=2.0.0", + "anyio[trio] >=3.2.1,<5.0.0", + "coverage[toml] >=6.5.0,<8.0", + "dirty-equals >=0.9.0", + "flask >=3.0.0,<4.0.0", + "inline-snapshot >=0.21.1", + "mypy >=1.14.1", + "pwdlib[argon2] >=0.2.1", + "pyjwt >=2.9.0", + "pytest >=7.1.3,<9.0.0", + "pytest-codspeed >=4.2.0", + "pyyaml >=5.3.1,<7.0.0", + "sqlmodel >=0.0.31", + "strawberry-graphql >=0.200.0,<1.0.0", + "types-orjson >=3.6.2", + "types-ujson >=5.10.0.20240515", + "a2wsgi >=1.9.0,<=2.0.0", ] translations = [ - "gitpython==3.1.46", - "pydantic-ai==0.4.10", - "pygithub==2.8.1", + "gitpython >=3.1.46", + "pydantic-ai >=0.4.10", + "pygithub >=2.8.1", ] [tool.pdm] diff --git a/uv.lock b/uv.lock index 88b9f28df..9a807f8b4 100644 --- a/uv.lock +++ b/uv.lock @@ -31,6 +31,165 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889, upload-time = "2025-11-06T15:17:15.325Z" }, ] +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/446655656861d3e7e2c32bfcf160c7aa9e9dc63776a691b124dba65cdd77/aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e", size = 741433, upload-time = "2026-01-03T17:32:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/cb/49/773c4b310b5140d2fb5e79bb0bf40b7b41dad80a288ca1a8759f5f72bda9/aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7", size = 497332, upload-time = "2026-01-03T17:32:28.37Z" }, + { url = "https://files.pythonhosted.org/packages/bc/31/1dcbc4b83a4e6f76a0ad883f07f21ffbfe29750c89db97381701508c9f45/aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02", size = 492365, upload-time = "2026-01-03T17:32:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b5/b50657496c8754482cd7964e50aaf3aa84b3db61ed45daec4c1aec5b94b4/aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43", size = 1660440, upload-time = "2026-01-03T17:32:32.586Z" }, + { url = "https://files.pythonhosted.org/packages/2a/73/9b69e5139d89d75127569298931444ad78ea86a5befd5599780b1e9a6880/aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6", size = 1632740, upload-time = "2026-01-03T17:32:34.793Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/3ea9b5af694b4e3aec0d0613a806132ca744747146fca68e96bf056f61a7/aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce", size = 1719782, upload-time = "2026-01-03T17:32:37.737Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/46b3b06e60851cbb71efb0f79a3267279cbef7b12c58e68a1e897f269cca/aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80", size = 1813527, upload-time = "2026-01-03T17:32:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/36/23/71ceb78c769ed65fe4c697692de232b63dab399210678d2b00961ccb0619/aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a", size = 1661268, upload-time = "2026-01-03T17:32:42.082Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/86e929523d955e85ebab7c0e2b9e0cb63604cfc27dc3280e10d0063cf682/aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6", size = 1552742, upload-time = "2026-01-03T17:32:44.622Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/3f5987cba1bab6bd151f0d97aa60f0ce04d3c83316692a6bb6ba2fb69f92/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558", size = 1632918, upload-time = "2026-01-03T17:32:46.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/2c/7e1e85121f2e31ee938cb83a8f32dfafd4908530c10fabd6d46761c12ac7/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7", size = 1644446, upload-time = "2026-01-03T17:32:49.063Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/ce6133d423ad0e8ca976a7c848f7146bca3520eea4ccf6b95e2d077c9d20/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877", size = 1689487, upload-time = "2026-01-03T17:32:51.113Z" }, + { url = "https://files.pythonhosted.org/packages/50/f7/ff7a27c15603d460fd1366b3c22054f7ae4fa9310aca40b43bde35867fcd/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3", size = 1540715, upload-time = "2026-01-03T17:32:53.38Z" }, + { url = "https://files.pythonhosted.org/packages/17/02/053f11346e5b962e6d8a1c4f8c70c29d5970a1b4b8e7894c68e12c27a57f/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704", size = 1711835, upload-time = "2026-01-03T17:32:56.088Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/9b9761ddf276fd6708d13720197cbac19b8d67ecfa9116777924056cfcaa/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f", size = 1649593, upload-time = "2026-01-03T17:32:58.181Z" }, + { url = "https://files.pythonhosted.org/packages/ae/72/5d817e9ea218acae12a5e3b9ad1178cf0c12fc3570c0b47eea2daf95f9ea/aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1", size = 434831, upload-time = "2026-01-03T17:33:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/39/cb/22659d9bf3149b7a2927bc2769cc9c8f8f5a80eba098398e03c199a43a85/aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538", size = 457697, upload-time = "2026-01-03T17:33:03.167Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -173,6 +332,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -182,6 +350,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "authlib" +version = "1.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/dc/ed1681bf1339dd6ea1ce56136bad4baabc6f7ad466e375810702b0237047/authlib-1.6.7.tar.gz", hash = "sha256:dbf10100011d1e1b34048c9d120e83f13b35d69a826ae762b93d2fb5aafc337b", size = 164950, upload-time = "2026-02-06T14:04:14.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/00/3ed12264094ec91f534fae429945efbaa9f8c666f3aa7061cc3b2a26a0cd/authlib-1.6.7-py2.py3-none-any.whl", hash = "sha256:c637340d9a02789d2efa1d003a7437d10d3e565237bcb5fcbc6c134c7b95bab0", size = 244115, upload-time = "2026-02-06T14:04:12.141Z" }, +] + [[package]] name = "babel" version = "2.18.0" @@ -191,6 +371,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + [[package]] name = "backrefs" version = "6.1" @@ -206,43 +395,106 @@ wheels = [ ] [[package]] -name = "black" -version = "25.1.0" +name = "beartype" +version = "0.22.9" source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "black" +version = "25.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, + { name = "mypy-extensions", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pathspec", marker = "python_full_version < '3.10'" }, { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "pytokens", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, - { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, - { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, - { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, - { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, - { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, - { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, - { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, - { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, - { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b6/ae7507470a4830dbbfe875c701e84a4a5fb9183d1497834871a715716a92/black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", size = 1628593, upload-time = "2025-01-29T05:37:23.672Z" }, - { url = "https://files.pythonhosted.org/packages/24/c1/ae36fa59a59f9363017ed397750a0cd79a470490860bc7713967d89cdd31/black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f", size = 1460000, upload-time = "2025-01-29T05:37:25.829Z" }, - { url = "https://files.pythonhosted.org/packages/ac/b6/98f832e7a6c49aa3a464760c67c7856363aa644f2f3c74cf7d624168607e/black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", size = 1765963, upload-time = "2025-01-29T04:18:38.116Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e9/2cb0a017eb7024f70e0d2e9bdb8c5a5b078c5740c7f8816065d06f04c557/black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", size = 1419419, upload-time = "2025-01-29T04:18:30.191Z" }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d2/6caccbc96f9311e8ec3378c296d4f4809429c43a6cd2394e3c390e86816d/black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e", size = 1743501, upload-time = "2025-11-10T01:59:06.202Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/b986d57828b3f3dccbf922e2864223197ba32e74c5004264b1c62bc9f04d/black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0", size = 1597308, upload-time = "2025-11-10T01:57:58.633Z" }, + { url = "https://files.pythonhosted.org/packages/39/8e/8b58ef4b37073f52b64a7b2dd8c9a96c84f45d6f47d878d0aa557e9a2d35/black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37", size = 1656194, upload-time = "2025-11-10T01:57:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/9c2267a7955ecc545306534ab88923769a979ac20a27cf618d370091e5dd/black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03", size = 1347996, upload-time = "2025-11-10T01:57:22.391Z" }, + { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, + { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, + { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" }, + { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9a/5b2c0e3215fe748fcf515c2dd34658973a1210bf610e24de5ba887e4f1c8/black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06", size = 1743063, upload-time = "2025-11-10T02:02:43.175Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/245164c6efc27333409c62ba54dcbfbe866c6d1957c9a6c0647786e950da/black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2", size = 1596867, upload-time = "2025-11-10T02:00:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/1a3859a7da205f3d50cf3a8bec6bdc551a91c33ae77a045bb24c1f46ab54/black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc", size = 1655678, upload-time = "2025-11-10T01:57:09.028Z" }, + { url = "https://files.pythonhosted.org/packages/56/1a/6dec1aeb7be90753d4fcc273e69bc18bfd34b353223ed191da33f7519410/black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc", size = 1347452, upload-time = "2025-11-10T01:57:01.871Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, +] + +[[package]] +name = "black" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pathspec", marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytokens", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/1b/523329e713f965ad0ea2b7a047eeb003007792a0353622ac7a8cb2ee6fef/black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", size = 1849661, upload-time = "2026-01-18T04:59:12.425Z" }, + { url = "https://files.pythonhosted.org/packages/14/82/94c0640f7285fa71c2f32879f23e609dd2aa39ba2641f395487f24a578e7/black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", size = 1689065, upload-time = "2026-01-18T04:59:13.993Z" }, + { url = "https://files.pythonhosted.org/packages/f0/78/474373cbd798f9291ed8f7107056e343fd39fef42de4a51c7fd0d360840c/black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", size = 1751502, upload-time = "2026-01-18T04:59:15.971Z" }, + { url = "https://files.pythonhosted.org/packages/29/89/59d0e350123f97bc32c27c4d79563432d7f3530dca2bff64d855c178af8b/black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", size = 1400102, upload-time = "2026-01-18T04:59:17.8Z" }, + { url = "https://files.pythonhosted.org/packages/e1/bc/5d866c7ae1c9d67d308f83af5462ca7046760158bbf142502bad8f22b3a1/black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", size = 1207038, upload-time = "2026-01-18T04:59:19.543Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, ] [[package]] @@ -283,6 +535,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4c/a8/95656f91b795eb47b73a00d36c51c7a5729eafa632c7348caa068ff63e50/botocore-1.42.43-py3-none-any.whl", hash = "sha256:1c0e30f62e274978ac3bcab253e3a859febea634b72b5e343589db7d17f83cd6", size = 14610179, upload-time = "2026-02-05T20:31:32.727Z" }, ] +[[package]] +name = "cachetools" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/af/df70e9b65bc77a1cbe0768c0aa4617147f30f8306ded98c1744bcdc0ae1e/cachetools-7.0.0.tar.gz", hash = "sha256:a9abf18ff3b86c7d05b27ead412e235e16ae045925e531fae38d5fada5ed5b08", size = 35796, upload-time = "2026-02-01T18:59:47.411Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/df/2dd32cce20cbcf6f2ec456b58d44368161ad28320729f64e5e1d5d7bd0ae/cachetools-7.0.0-py3-none-any.whl", hash = "sha256:d52fef60e6e964a1969cfb61ccf6242a801b432790fe520d78720d757c81cbd2", size = 13487, upload-time = "2026-02-01T18:59:45.981Z" }, +] + [[package]] name = "cairocffi" version = "1.7.1" @@ -303,7 +564,8 @@ dependencies = [ { name = "cairocffi" }, { name = "cssselect2" }, { name = "defusedxml" }, - { name = "pillow" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "tinycss2", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "tinycss2", version = "1.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] @@ -552,6 +814,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + [[package]] name = "cohere" version = "5.20.4" @@ -808,6 +1079,19 @@ toml = [ { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, ] +[[package]] +name = "croniter" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, + { name = "pytz", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/2f/44d1ae153a0e27be56be43465e5cb39b9650c781e001e7864389deb25090/croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577", size = 64481, upload-time = "2024-12-17T17:17:47.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/4b/290b4c3efd6417a8b0c284896de19b1d5855e6dbdb97d2a35e68fa42de85/croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368", size = 25468, upload-time = "2024-12-17T17:17:45.359Z" }, +] + [[package]] name = "cross-web" version = "0.4.1" @@ -903,6 +1187,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/c0/9f59d2ebd9d585e1681c51767eb138bcd9d0ea770f6fc003cd875c7f5e62/cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed", size = 2547, upload-time = "2018-09-26T16:47:05.609Z" }, ] +[[package]] +name = "cyclopts" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "docstring-parser", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, + { name = "rich-rst", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/93/6085aa89c3fff78a5180987354538d72e43b0db27e66a959302d0c07821a/cyclopts-4.5.1.tar.gz", hash = "sha256:fadc45304763fd9f5d6033727f176898d17a1778e194436964661a005078a3dd", size = 162075, upload-time = "2026-01-25T15:23:54.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/7c/996760c30f1302704af57c66ff2d723f7d656d0d0b93563b5528a51484bb/cyclopts-4.5.1-py3-none-any.whl", hash = "sha256:0642c93601e554ca6b7b9abd81093847ea4448b2616280f2a0952416574e8c7a", size = 199807, upload-time = "2026-01-25T15:23:55.219Z" }, +] + [[package]] name = "defusedxml" version = "0.7.1" @@ -914,11 +1215,20 @@ wheels = [ [[package]] name = "dirty-equals" -version = "0.9.0" +version = "0.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/99/133892f401ced5a27e641a473c547d5fbdb39af8f85dac8a9d633ea3e7a7/dirty_equals-0.9.0.tar.gz", hash = "sha256:17f515970b04ed7900b733c95fd8091f4f85e52f1fb5f268757f25c858eb1f7b", size = 50412, upload-time = "2025-01-11T23:23:40.491Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/1d/c5913ac9d6615515a00f4bdc71356d302437cb74ff2e9aaccd3c14493b78/dirty_equals-0.11.tar.gz", hash = "sha256:f4ac74ee88f2d11e2fa0f65eb30ee4f07105c5f86f4dc92b09eb1138775027c3", size = 128067, upload-time = "2025-11-17T01:51:24.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/0c/03cc99bf3b6328604b10829de3460f2b2ad3373200c45665c38508e550c6/dirty_equals-0.9.0-py3-none-any.whl", hash = "sha256:ff4d027f5cfa1b69573af00f7ba9043ea652dbdce3fe5cbe828e478c7346db9c", size = 28226, upload-time = "2025-01-11T23:23:37.489Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8d/dbff05239043271dbeace563a7686212a3dd517864a35623fe4d4a64ca19/dirty_equals-0.11-py3-none-any.whl", hash = "sha256:b1d7093273fc2f9be12f443a8ead954ef6daaf6746fd42ef3a5616433ee85286", size = 28051, upload-time = "2025-11-17T01:51:22.849Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, ] [[package]] @@ -964,6 +1274,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[package]] name = "email-validator" version = "2.3.0" @@ -1008,6 +1327,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "fakeredis" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis", marker = "python_full_version >= '3.10'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187, upload-time = "2025-12-16T19:45:52.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605, upload-time = "2025-12-16T19:45:51.08Z" }, +] + +[package.optional-dependencies] +lua = [ + { name = "lupa", marker = "python_full_version >= '3.10'" }, +] + [[package]] name = "fastapi" source = { editable = "." } @@ -1015,7 +1353,7 @@ dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "0.52.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] @@ -1070,7 +1408,8 @@ standard-no-fastapi-cloud-cli = [ dev = [ { name = "a2wsgi" }, { name = "anyio", extra = ["trio"] }, - { name = "black" }, + { name = "black", version = "25.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "black", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "cairosvg" }, { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, { name = "coverage", version = "7.13.3", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, @@ -1087,14 +1426,17 @@ dev = [ { name = "mkdocs-macros-plugin" }, { name = "mkdocs-material" }, { name = "mkdocs-redirects" }, - { name = "mkdocstrings", extra = ["python"] }, + { name = "mkdocstrings", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version < '3.10'" }, + { name = "mkdocstrings", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version >= '3.10'" }, { name = "mypy" }, - { name = "pillow" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "playwright" }, { name = "prek" }, { name = "pwdlib", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, extra = ["argon2"], marker = "python_full_version < '3.10'" }, { name = "pwdlib", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, extra = ["argon2"], marker = "python_full_version >= '3.10'" }, - { name = "pydantic-ai" }, + { name = "pydantic-ai", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic-ai", version = "1.56.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pygithub" }, { name = "pyjwt" }, { name = "pytest" }, @@ -1104,13 +1446,14 @@ dev = [ { name = "ruff" }, { name = "sqlmodel" }, { name = "strawberry-graphql", version = "0.283.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "strawberry-graphql", version = "0.291.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "strawberry-graphql", version = "0.291.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "typer" }, { name = "types-orjson" }, { name = "types-ujson" }, ] docs = [ - { name = "black" }, + { name = "black", version = "25.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "black", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "cairosvg" }, { name = "griffe-typingdoc" }, { name = "griffe-warnings-deprecated" }, @@ -1121,8 +1464,10 @@ docs = [ { name = "mkdocs-macros-plugin" }, { name = "mkdocs-material" }, { name = "mkdocs-redirects" }, - { name = "mkdocstrings", extra = ["python"] }, - { name = "pillow" }, + { name = "mkdocstrings", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version < '3.10'" }, + { name = "mkdocstrings", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version >= '3.10'" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "python-slugify" }, { name = "pyyaml" }, { name = "ruff" }, @@ -1160,13 +1505,14 @@ tests = [ { name = "ruff" }, { name = "sqlmodel" }, { name = "strawberry-graphql", version = "0.283.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "strawberry-graphql", version = "0.291.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "strawberry-graphql", version = "0.291.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "types-orjson" }, { name = "types-ujson" }, ] translations = [ { name = "gitpython" }, - { name = "pydantic-ai" }, + { name = "pydantic-ai", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic-ai", version = "1.56.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pygithub" }, ] @@ -1212,64 +1558,64 @@ provides-extras = ["standard", "standard-no-fastapi-cloud-cli", "all"] dev = [ { name = "a2wsgi", specifier = ">=1.9.0,<=2.0.0" }, { name = "anyio", extras = ["trio"], specifier = ">=3.2.1,<5.0.0" }, - { name = "black", specifier = "==25.1.0" }, - { name = "cairosvg", specifier = "==2.8.2" }, + { name = "black", specifier = ">=25.1.0" }, + { name = "cairosvg", specifier = ">=2.8.2" }, { name = "coverage", extras = ["toml"], specifier = ">=6.5.0,<8.0" }, - { name = "dirty-equals", specifier = "==0.9.0" }, + { name = "dirty-equals", specifier = ">=0.9.0" }, { name = "flask", specifier = ">=3.0.0,<4.0.0" }, - { name = "gitpython", specifier = "==3.1.46" }, - { name = "griffe-typingdoc", specifier = "==0.3.0" }, - { name = "griffe-warnings-deprecated", specifier = "==1.1.0" }, + { name = "gitpython", specifier = ">=3.1.46" }, + { name = "griffe-typingdoc", specifier = ">=0.3.0" }, + { name = "griffe-warnings-deprecated", specifier = ">=1.1.0" }, { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, { name = "inline-snapshot", specifier = ">=0.21.1" }, - { name = "jieba", specifier = "==0.42.1" }, - { name = "markdown-include-variants", specifier = "==0.0.8" }, + { name = "jieba", specifier = ">=0.42.1" }, + { name = "markdown-include-variants", specifier = ">=0.0.8" }, { name = "mdx-include", specifier = ">=1.4.1,<2.0.0" }, - { name = "mkdocs-macros-plugin", specifier = "==1.5.0" }, - { name = "mkdocs-material", specifier = "==9.7.0" }, + { name = "mkdocs-macros-plugin", specifier = ">=1.5.0" }, + { name = "mkdocs-material", specifier = ">=9.7.0" }, { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, - { name = "mkdocstrings", extras = ["python"], specifier = "==0.30.1" }, - { name = "mypy", specifier = "==1.14.1" }, - { name = "pillow", specifier = "==11.3.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" }, + { name = "mypy", specifier = ">=1.14.1" }, + { name = "pillow", specifier = ">=11.3.0" }, { name = "playwright", specifier = ">=1.57.0" }, - { name = "prek", specifier = "==0.2.22" }, + { name = "prek", specifier = ">=0.2.22" }, { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" }, - { name = "pydantic-ai", specifier = "==0.4.10" }, - { name = "pygithub", specifier = "==2.8.1" }, - { name = "pyjwt", specifier = "==2.9.0" }, + { name = "pydantic-ai", specifier = ">=0.4.10" }, + { name = "pygithub", specifier = ">=2.8.1" }, + { name = "pyjwt", specifier = ">=2.9.0" }, { name = "pytest", specifier = ">=7.1.3,<9.0.0" }, - { name = "pytest-codspeed", specifier = "==4.2.0" }, - { name = "python-slugify", specifier = "==8.0.4" }, + { name = "pytest-codspeed", specifier = ">=4.2.0" }, + { name = "python-slugify", specifier = ">=8.0.4" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, - { name = "ruff", specifier = "==0.14.14" }, - { name = "sqlmodel", specifier = "==0.0.31" }, + { name = "ruff", specifier = ">=0.14.14" }, + { name = "sqlmodel", specifier = ">=0.0.31" }, { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, - { name = "typer", specifier = "==0.21.1" }, - { name = "types-orjson", specifier = "==3.6.2" }, - { name = "types-ujson", specifier = "==5.10.0.20240515" }, + { name = "typer", specifier = ">=0.21.1" }, + { name = "types-orjson", specifier = ">=3.6.2" }, + { name = "types-ujson", specifier = ">=5.10.0.20240515" }, ] docs = [ - { name = "black", specifier = "==25.1.0" }, - { name = "cairosvg", specifier = "==2.8.2" }, - { name = "griffe-typingdoc", specifier = "==0.3.0" }, - { name = "griffe-warnings-deprecated", specifier = "==1.1.0" }, + { name = "black", specifier = ">=25.1.0" }, + { name = "cairosvg", specifier = ">=2.8.2" }, + { name = "griffe-typingdoc", specifier = ">=0.3.0" }, + { name = "griffe-warnings-deprecated", specifier = ">=1.1.0" }, { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, - { name = "jieba", specifier = "==0.42.1" }, - { name = "markdown-include-variants", specifier = "==0.0.8" }, + { name = "jieba", specifier = ">=0.42.1" }, + { name = "markdown-include-variants", specifier = ">=0.0.8" }, { name = "mdx-include", specifier = ">=1.4.1,<2.0.0" }, - { name = "mkdocs-macros-plugin", specifier = "==1.5.0" }, - { name = "mkdocs-material", specifier = "==9.7.0" }, + { name = "mkdocs-macros-plugin", specifier = ">=1.5.0" }, + { name = "mkdocs-material", specifier = ">=9.7.0" }, { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, - { name = "mkdocstrings", extras = ["python"], specifier = "==0.30.1" }, - { name = "pillow", specifier = "==11.3.0" }, - { name = "python-slugify", specifier = "==8.0.4" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" }, + { name = "pillow", specifier = ">=11.3.0" }, + { name = "python-slugify", specifier = ">=8.0.4" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, - { name = "ruff", specifier = "==0.14.14" }, - { name = "typer", specifier = "==0.21.1" }, + { name = "ruff", specifier = ">=0.14.14" }, + { name = "typer", specifier = ">=0.21.1" }, ] docs-tests = [ { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, - { name = "ruff", specifier = "==0.14.14" }, + { name = "ruff", specifier = ">=0.14.14" }, ] github-actions = [ { name = "httpx", specifier = ">=0.27.0,<1.0.0" }, @@ -1283,26 +1629,26 @@ tests = [ { name = "a2wsgi", specifier = ">=1.9.0,<=2.0.0" }, { name = "anyio", extras = ["trio"], specifier = ">=3.2.1,<5.0.0" }, { name = "coverage", extras = ["toml"], specifier = ">=6.5.0,<8.0" }, - { name = "dirty-equals", specifier = "==0.9.0" }, + { name = "dirty-equals", specifier = ">=0.9.0" }, { name = "flask", specifier = ">=3.0.0,<4.0.0" }, { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, { name = "inline-snapshot", specifier = ">=0.21.1" }, - { name = "mypy", specifier = "==1.14.1" }, + { name = "mypy", specifier = ">=1.14.1" }, { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" }, - { name = "pyjwt", specifier = "==2.9.0" }, + { name = "pyjwt", specifier = ">=2.9.0" }, { name = "pytest", specifier = ">=7.1.3,<9.0.0" }, - { name = "pytest-codspeed", specifier = "==4.2.0" }, + { name = "pytest-codspeed", specifier = ">=4.2.0" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, - { name = "ruff", specifier = "==0.14.14" }, - { name = "sqlmodel", specifier = "==0.0.31" }, + { name = "ruff", specifier = ">=0.14.14" }, + { name = "sqlmodel", specifier = ">=0.0.31" }, { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, - { name = "types-orjson", specifier = "==3.6.2" }, - { name = "types-ujson", specifier = "==5.10.0.20240515" }, + { name = "types-orjson", specifier = ">=3.6.2" }, + { name = "types-ujson", specifier = ">=5.10.0.20240515" }, ] translations = [ - { name = "gitpython", specifier = "==3.1.46" }, - { name = "pydantic-ai", specifier = "==0.4.10" }, - { name = "pygithub", specifier = "==2.8.1" }, + { name = "gitpython", specifier = ">=3.1.46" }, + { name = "pydantic-ai", specifier = ">=0.4.10" }, + { name = "pygithub", specifier = ">=2.8.1" }, ] [[package]] @@ -1552,6 +1898,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/ba/b814fb09b32c8f3059451c33bb11d55eb23188e6a499f5dde628d13bc076/fastavro-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:a38607444281619eda3a9c1be9f5397634012d1b237142eee1540e810b30ac8b", size = 510328, upload-time = "2025-10-10T15:42:32.415Z" }, ] +[[package]] +name = "fastmcp" +version = "2.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib", marker = "python_full_version >= '3.10'" }, + { name = "cyclopts", marker = "python_full_version >= '3.10'" }, + { name = "exceptiongroup", marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "jsonref", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema-path", marker = "python_full_version >= '3.10'" }, + { name = "mcp", marker = "python_full_version >= '3.10'" }, + { name = "openapi-pydantic", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"], marker = "python_full_version >= '3.10'" }, + { name = "pydantic", extra = ["email"], marker = "python_full_version >= '3.10'" }, + { name = "pydocket", marker = "python_full_version >= '3.10'" }, + { name = "pyperclip", marker = "python_full_version >= '3.10'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "websockets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/32/982678d44f13849530a74ab101ed80e060c2ee6cf87471f062dcf61705fd/fastmcp-2.14.5.tar.gz", hash = "sha256:38944dc582c541d55357082bda2241cedb42cd3a78faea8a9d6a2662c62a42d7", size = 8296329, upload-time = "2026-02-03T15:35:21.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/c1/1a35ec68ff76ea8443aa115b18bcdee748a4ada2124537ee90522899ff9f/fastmcp-2.14.5-py3-none-any.whl", hash = "sha256:d81e8ec813f5089d3624bec93944beaefa86c0c3a4ef1111cbef676a761ebccf", size = 417784, upload-time = "2026-02-03T15:35:18.489Z" }, +] + [[package]] name = "filelock" version = "3.19.1" @@ -1596,6 +1971,143 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/ae5cdac87a00962122ea37bb346d41b66aec05f9ce328fa2b9e216f8967b/frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", size = 86967, upload-time = "2025-10-06T05:37:55.607Z" }, + { url = "https://files.pythonhosted.org/packages/8a/10/17059b2db5a032fd9323c41c39e9d1f5f9d0c8f04d1e4e3e788573086e61/frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", size = 49984, upload-time = "2025-10-06T05:37:57.049Z" }, + { url = "https://files.pythonhosted.org/packages/4b/de/ad9d82ca8e5fa8f0c636e64606553c79e2b859ad253030b62a21fe9986f5/frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", size = 50240, upload-time = "2025-10-06T05:37:58.145Z" }, + { url = "https://files.pythonhosted.org/packages/4e/45/3dfb7767c2a67d123650122b62ce13c731b6c745bc14424eea67678b508c/frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", size = 219472, upload-time = "2025-10-06T05:37:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bf/5bf23d913a741b960d5c1dac7c1985d8a2a1d015772b2d18ea168b08e7ff/frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", size = 221531, upload-time = "2025-10-06T05:38:00.521Z" }, + { url = "https://files.pythonhosted.org/packages/d0/03/27ec393f3b55860859f4b74cdc8c2a4af3dbf3533305e8eacf48a4fd9a54/frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", size = 219211, upload-time = "2025-10-06T05:38:01.842Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ad/0fd00c404fa73fe9b169429e9a972d5ed807973c40ab6b3cf9365a33d360/frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", size = 231775, upload-time = "2025-10-06T05:38:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c3/86962566154cb4d2995358bc8331bfc4ea19d07db1a96f64935a1607f2b6/frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", size = 236631, upload-time = "2025-10-06T05:38:04.609Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/6ffad161dbd83782d2c66dc4d378a9103b31770cb1e67febf43aea42d202/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", size = 218632, upload-time = "2025-10-06T05:38:05.917Z" }, + { url = "https://files.pythonhosted.org/packages/58/b2/4677eee46e0a97f9b30735e6ad0bf6aba3e497986066eb68807ac85cf60f/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", size = 235967, upload-time = "2025-10-06T05:38:07.614Z" }, + { url = "https://files.pythonhosted.org/packages/05/f3/86e75f8639c5a93745ca7addbbc9de6af56aebb930d233512b17e46f6493/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", size = 228799, upload-time = "2025-10-06T05:38:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/30/00/39aad3a7f0d98f5eb1d99a3c311215674ed87061aecee7851974b335c050/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", size = 230566, upload-time = "2025-10-06T05:38:10.52Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4d/aa144cac44568d137846ddc4d5210fb5d9719eb1d7ec6fa2728a54b5b94a/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", size = 217715, upload-time = "2025-10-06T05:38:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/64/4c/8f665921667509d25a0dd72540513bc86b356c95541686f6442a3283019f/frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", size = 39933, upload-time = "2025-10-06T05:38:13.061Z" }, + { url = "https://files.pythonhosted.org/packages/79/bd/bcc926f87027fad5e59926ff12d136e1082a115025d33c032d1cd69ab377/frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", size = 44121, upload-time = "2025-10-06T05:38:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/4c/07/9c2e4eb7584af4b705237b971b89a4155a8e57599c4483a131a39256a9a0/frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", size = 40312, upload-time = "2025-10-06T05:38:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + [[package]] name = "fsspec" version = "2025.10.0" @@ -1621,6 +2133,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] +[[package]] +name = "genai-prices" +version = "0.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport", marker = "python_full_version < '3.10'" }, + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/87/bdc11c1671e3a3fe701c3c4aaae4aa2bb7a84a6bb1812dfb5693c87d3872/genai_prices-0.0.52.tar.gz", hash = "sha256:0df7420b555fa3a48d09e5c7802ba35b5dfa9fd49b0c3bb2c150c59060d83f52", size = 58364, upload-time = "2026-01-28T12:07:49.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/33/6316b4907a0bffc1bcc99074c7e2d01184fdfeee401c864146a40d55ad10/genai_prices-0.0.52-py3-none-any.whl", hash = "sha256:639e7a2ae7eddf5710febb9779b9c9e31ff5acf464b4eb1f6018798ea642e6d3", size = 60937, upload-time = "2026-01-28T12:07:47.921Z" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -1724,6 +2250,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/5f/4645d8a28c6e431d0dd6011003a852563f3da7037d36af53154925b099fd/google_genai-1.62.0-py3-none-any.whl", hash = "sha256:4c3daeff3d05fafee4b9a1a31f9c07f01bc22051081aa58b4d61f58d16d1bcc0", size = 724166, upload-time = "2026-02-04T22:48:39.956Z" }, ] +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", version = "6.33.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + [[package]] name = "graphql-core" version = "3.2.7" @@ -1951,6 +2489,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/88/3175759d2ef30406ea721f4d837bfa1ba4339fde3b81ba8c5640a96ed231/groq-1.0.0-py3-none-any.whl", hash = "sha256:6e22bf92ffad988f01d2d4df7729add66b8fd5dbfb2154b5bbf3af245b72c731", size = 138292, upload-time = "2025-12-17T23:34:21.957Z" }, ] +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, + { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, + { url = "https://files.pythonhosted.org/packages/58/6c/40a4bba2c753ea8eeb8d776a31e9c54f4e506edf36db93a3db5456725294/grpcio-1.78.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:86f85dd7c947baa707078a236288a289044836d4b640962018ceb9cd1f899af5", size = 5947902, upload-time = "2026-02-06T09:56:48.469Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4c/ed7664a37a7008be41204c77e0d88bbc4ac531bcf0c27668cd066f9ff6e2/grpcio-1.78.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:de8cb00d1483a412a06394b8303feec5dcb3b55f81d83aa216dbb6a0b86a94f5", size = 11824772, upload-time = "2026-02-06T09:56:51.264Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5b/45a5c23ba3c4a0f51352366d9b25369a2a51163ab1c93482cb8408726617/grpcio-1.78.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e888474dee2f59ff68130f8a397792d8cb8e17e6b3434339657ba4ee90845a8c", size = 6521579, upload-time = "2026-02-06T09:56:54.967Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/392e647d918004231e3d1c780ed125c48939bfc8f845adb8b5820410da3e/grpcio-1.78.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:86ce2371bfd7f212cf60d8517e5e854475c2c43ce14aa910e136ace72c6db6c1", size = 7199330, upload-time = "2026-02-06T09:56:57.611Z" }, + { url = "https://files.pythonhosted.org/packages/68/2f/42a52d78bdbdb3f1310ed690a3511cd004740281ca75d300b7bd6d9d3de3/grpcio-1.78.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0c689c02947d636bc7fab3e30cc3a3445cca99c834dfb77cd4a6cabfc1c5597", size = 6726696, upload-time = "2026-02-06T09:57:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/0f/83/b3d932a4fbb2dce3056f6df2926fc2d3ddc5d5acbafbec32c84033cf3f23/grpcio-1.78.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ce7599575eeb25c0f4dc1be59cada6219f3b56176f799627f44088b21381a28a", size = 7299076, upload-time = "2026-02-06T09:57:04.124Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d9/70ea1be55efaf91fd19f7258b1292772a8226cf1b0e237717fba671073cb/grpcio-1.78.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:684083fd383e9dc04c794adb838d4faea08b291ce81f64ecd08e4577c7398adf", size = 8284493, upload-time = "2026-02-06T09:57:06.746Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2f/3dddccf49e3e75564655b84175fca092d3efd81d2979fc89c4b1c1d879dc/grpcio-1.78.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab399ef5e3cd2a721b1038a0f3021001f19c5ab279f145e1146bb0b9f1b2b12c", size = 7724340, upload-time = "2026-02-06T09:57:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/dfdb3183141db787a9363078a98764675996a7c2448883153091fd7c8527/grpcio-1.78.0-cp39-cp39-win32.whl", hash = "sha256:f3d6379493e18ad4d39537a82371c5281e153e963cecb13f953ebac155756525", size = 4077641, upload-time = "2026-02-06T09:57:11.881Z" }, + { url = "https://files.pythonhosted.org/packages/aa/aa/694b2f505345cfdd234cffb2525aa379a81695e6c02fd40d7e9193e871c6/grpcio-1.78.0-cp39-cp39-win_amd64.whl", hash = "sha256:5361a0630a7fdb58a6a97638ab70e1dae2893c4d08d7aba64ded28bb9e7a29df", size = 4799428, upload-time = "2026-02-06T09:57:14.493Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -2087,25 +2696,28 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.4.1" +version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "fsspec", version = "2026.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, - { name = "httpx" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, { name = "packaging" }, { name = "pyyaml" }, - { name = "shellingham" }, + { name = "requests" }, { name = "tqdm" }, - { name = "typer-slim" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/fc/eb9bc06130e8bbda6a616e1b80a7aa127681c448d6b49806f61db2670b61/huggingface_hub-1.4.1.tar.gz", hash = "sha256:b41131ec35e631e7383ab26d6146b8d8972abc8b6309b963b306fbcca87f5ed5", size = 642156, upload-time = "2026-02-06T09:20:03.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/ae/2f6d96b4e6c5478d87d606a1934b5d436c4a2bce6bb7c6fdece891c128e3/huggingface_hub-1.4.1-py3-none-any.whl", hash = "sha256:9931d075fb7a79af5abc487106414ec5fba2c0ae86104c0c62fd6cae38873d18", size = 553326, upload-time = "2026-02-06T09:20:00.728Z" }, + { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, +] + +[package.optional-dependencies] +inference = [ + { name = "aiohttp" }, ] [[package]] @@ -2188,6 +2800,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version >= '3.10' and python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jieba" version = "0.42.1" @@ -2324,6 +2981,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -2339,6 +3005,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] +[[package]] +name = "jsonschema-path" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "referencing", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, +] + [[package]] name = "jsonschema-specifications" version = "2025.9.1" @@ -2351,6 +3032,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version >= '3.10' and python_full_version < '3.12'" }, + { name = "jaraco-classes", marker = "python_full_version >= '3.10'" }, + { name = "jaraco-context", marker = "python_full_version >= '3.10'" }, + { name = "jaraco-functools", marker = "python_full_version >= '3.10'" }, + { name = "jeepney", marker = "python_full_version >= '3.10' and sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "secretstorage", marker = "python_full_version >= '3.10' and sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + [[package]] name = "lia-web" version = "0.3.1" @@ -2363,6 +3062,113 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/8b/b628fc18658f94b3d094708a18b71083cf47628e85cbc6b9edba54d5b2d7/lia_web-0.3.1-py3-none-any.whl", hash = "sha256:e4e6e7a9381e228aca60a6f3d67dbae9a5f4638eced242d931f95797ddba3f8b", size = 5933, upload-time = "2025-12-25T20:41:52.289Z" }, ] +[[package]] +name = "librt" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, + { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, + { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, + { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/2668bb01f568bc89ace53736df950845f8adfcacdf6da087d5cef12110cb/librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6", size = 56680, upload-time = "2026-01-14T12:56:02.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d4/dbb3edf2d0ec4ba08dcaf1865833d32737ad208962d4463c022cea6e9d3c/librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b", size = 58612, upload-time = "2026-01-14T12:56:03.616Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/64b029de4ac9901fcd47832c650a0fd050555a452bd455ce8deddddfbb9f/librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c", size = 163654, upload-time = "2026-01-14T12:56:04.975Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/95e2abb1b48eb8f8c7fc2ae945321a6b82777947eb544cc785c3f37165b2/librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5", size = 172477, upload-time = "2026-01-14T12:56:06.103Z" }, + { url = "https://files.pythonhosted.org/packages/7e/27/9bdf12e05b0eb089dd008d9c8aabc05748aad9d40458ade5e627c9538158/librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71", size = 186220, upload-time = "2026-01-14T12:56:09.958Z" }, + { url = "https://files.pythonhosted.org/packages/53/6a/c3774f4cc95e68ed444a39f2c8bd383fd18673db7d6b98cfa709f6634b93/librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e", size = 183841, upload-time = "2026-01-14T12:56:11.109Z" }, + { url = "https://files.pythonhosted.org/packages/58/6b/48702c61cf83e9c04ad5cec8cad7e5e22a2cde23a13db8ef341598897ddd/librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63", size = 179751, upload-time = "2026-01-14T12:56:12.278Z" }, + { url = "https://files.pythonhosted.org/packages/35/87/5f607fc73a131d4753f4db948833063c6aad18e18a4e6fbf64316c37ae65/librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94", size = 199319, upload-time = "2026-01-14T12:56:13.425Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cc/b7c5ac28ae0f0645a9681248bae4ede665bba15d6f761c291853c5c5b78e/librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb", size = 43434, upload-time = "2026-01-14T12:56:14.781Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/dce0c92f786495adf2c1e6784d9c50a52fb7feb1cfb17af97a08281a6e82/librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be", size = 49801, upload-time = "2026-01-14T12:56:15.827Z" }, +] + +[[package]] +name = "logfire" +version = "4.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "executing", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-instrumentation", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-sdk", marker = "python_full_version >= '3.10'" }, + { name = "protobuf", version = "6.33.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/29/4386b331b8aecad429c043bbd1b7684cdb56ff9e11a5bd96845483dc0968/logfire-4.22.0.tar.gz", hash = "sha256:284b3b955c7515d4428dbc1e04784c3e652e62acf7597bd64a0aa9ecb6a7dedd", size = 654771, upload-time = "2026-02-04T12:17:57.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/ce/24a598c271d9631b09fb42f86f1e6d63b50647ecf0ce438d368a1e1af66b/logfire-4.22.0-py3-none-any.whl", hash = "sha256:9a0d20885613efe4bc9efcfa19a7943a6c642bc21339d926c51fcc74c0d083d6", size = 242637, upload-time = "2026-02-04T12:17:54.787Z" }, +] + +[package.optional-dependencies] +httpx = [ + { name = "opentelemetry-instrumentation-httpx", marker = "python_full_version >= '3.10'" }, +] + [[package]] name = "logfire-api" version = "4.22.0" @@ -2372,6 +3178,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/93/4e8395f3e4b1951b1de8d06cb7d7ef1105efd294ea3443bf39b3594390af/logfire_api-4.22.0-py3-none-any.whl", hash = "sha256:9fd2a2dac9cc3adf71ad4dac7bc9d26af326a5aa87445be90026048d22b92a8e", size = 98543, upload-time = "2026-02-04T12:17:56.365Z" }, ] +[[package]] +name = "lupa" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/15/713cab5d0dfa4858f83b99b3e0329072df33dc14fc3ebbaa017e0f9755c4/lupa-2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b3dabda836317e63c5ad052826e156610f356a04b3003dfa0dbe66b5d54d671", size = 954828, upload-time = "2025-10-24T07:17:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/2e/71/704740cbc6e587dd6cc8dabf2f04820ac6a671784e57cc3c29db795476db/lupa-2.6-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8726d1c123bbe9fbb974ce29825e94121824e66003038ff4532c14cc2ed0c51c", size = 1919259, upload-time = "2025-10-24T07:17:18.586Z" }, + { url = "https://files.pythonhosted.org/packages/eb/18/f248341c423c5d48837e35584c6c3eb4acab7e722b6057d7b3e28e42dae8/lupa-2.6-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f4e159e7d814171199b246f9235ca8961f6461ea8c1165ab428afa13c9289a94", size = 984998, upload-time = "2025-10-24T07:17:20.428Z" }, + { url = "https://files.pythonhosted.org/packages/44/1e/8a4bd471e018aad76bcb9455d298c2c96d82eced20f2ae8fcec8cd800948/lupa-2.6-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:202160e80dbfddfb79316692a563d843b767e0f6787bbd1c455f9d54052efa6c", size = 1174871, upload-time = "2025-10-24T07:17:22.755Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5c/3a3f23fd6a91b0986eea1ceaf82ad3f9b958fe3515a9981fb9c4eb046c8b/lupa-2.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5deede7c5b36ab64f869dae4831720428b67955b0bb186c8349cf6ea121c852b", size = 1057471, upload-time = "2025-10-24T07:17:24.908Z" }, + { url = "https://files.pythonhosted.org/packages/45/ac/01be1fed778fb0c8f46ee8cbe344e4d782f6806fac12717f08af87aa4355/lupa-2.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86f04901f920bbf7c0cac56807dc9597e42347123e6f1f3ca920f15f54188ce5", size = 2100592, upload-time = "2025-10-24T07:17:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6c/1a05bb873e30830f8574e10cd0b4cdbc72e9dbad2a09e25810b5e3b1f75d/lupa-2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6deef8f851d6afb965c84849aa5b8c38856942df54597a811ce0369ced678610", size = 1081396, upload-time = "2025-10-24T07:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c2/a19dd80d6dc98b39bbf8135b8198e38aa7ca3360b720eac68d1d7e9286b5/lupa-2.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:21f2b5549681c2a13b1170a26159d30875d367d28f0247b81ca347222c755038", size = 1192007, upload-time = "2025-10-24T07:17:31.362Z" }, + { url = "https://files.pythonhosted.org/packages/4f/43/e1b297225c827f55752e46fdbfb021c8982081b0f24490e42776ea69ae3b/lupa-2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:66eea57630eab5e6f49fdc5d7811c0a2a41f2011be4ea56a087ea76112011eb7", size = 2196661, upload-time = "2025-10-24T07:17:33.484Z" }, + { url = "https://files.pythonhosted.org/packages/2e/8f/2272d429a7fa9dc8dbd6e9c5c9073a03af6007eb22a4c78829fec6a34b80/lupa-2.6-cp310-cp310-win32.whl", hash = "sha256:60a403de8cab262a4fe813085dd77010effa6e2eb1886db2181df803140533b1", size = 1412738, upload-time = "2025-10-24T07:17:35.11Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/1708911271dd49ad87b4b373b5a4b0e0a0516d3d2af7b76355946c7ee171/lupa-2.6-cp310-cp310-win_amd64.whl", hash = "sha256:e4656a39d93dfa947cf3db56dc16c7916cb0cc8024acd3a952071263f675df64", size = 1656898, upload-time = "2025-10-24T07:17:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/ca/29/1f66907c1ebf1881735afa695e646762c674f00738ebf66d795d59fc0665/lupa-2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d988c0f9331b9f2a5a55186701a25444ab10a1432a1021ee58011499ecbbdd5", size = 962875, upload-time = "2025-10-24T07:17:39.107Z" }, + { url = "https://files.pythonhosted.org/packages/e6/67/4a748604be360eb9c1c215f6a0da921cd1a2b44b2c5951aae6fb83019d3a/lupa-2.6-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ebe1bbf48259382c72a6fe363dea61a0fd6fe19eab95e2ae881e20f3654587bf", size = 1935390, upload-time = "2025-10-24T07:17:41.427Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/8ef9ee933a350428b7bdb8335a37ef170ab0bb008bbf9ca8f4f4310116b6/lupa-2.6-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:a8fcee258487cf77cdd41560046843bb38c2e18989cd19671dd1e2596f798306", size = 992193, upload-time = "2025-10-24T07:17:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/65/46/e6c7facebdb438db8a65ed247e56908818389c1a5abbf6a36aab14f1057d/lupa-2.6-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:561a8e3be800827884e767a694727ed8482d066e0d6edfcbf423b05e63b05535", size = 1165844, upload-time = "2025-10-24T07:17:45.437Z" }, + { url = "https://files.pythonhosted.org/packages/1c/26/9f1154c6c95f175ccbf96aa96c8f569c87f64f463b32473e839137601a8b/lupa-2.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af880a62d47991cae78b8e9905c008cbfdc4a3a9723a66310c2634fc7644578c", size = 1048069, upload-time = "2025-10-24T07:17:47.181Z" }, + { url = "https://files.pythonhosted.org/packages/68/67/2cc52ab73d6af81612b2ea24c870d3fa398443af8e2875e5befe142398b1/lupa-2.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80b22923aa4023c86c0097b235615f89d469a0c4eee0489699c494d3367c4c85", size = 2079079, upload-time = "2025-10-24T07:17:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/2e/dc/f843f09bbf325f6e5ee61730cf6c3409fc78c010d968c7c78acba3019ca7/lupa-2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:153d2cc6b643f7efb9cfc0c6bb55ec784d5bac1a3660cfc5b958a7b8f38f4a75", size = 1071428, upload-time = "2025-10-24T07:17:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/2e/60/37533a8d85bf004697449acb97ecdacea851acad28f2ad3803662487dd2a/lupa-2.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3fa8777e16f3ded50b72967dc17e23f5a08e4f1e2c9456aff2ebdb57f5b2869f", size = 1181756, upload-time = "2025-10-24T07:17:53.752Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f2/cf29b20dbb4927b6a3d27c339ac5d73e74306ecc28c8e2c900b2794142ba/lupa-2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8dbdcbe818c02a2f56f5ab5ce2de374dab03e84b25266cfbaef237829bc09b3f", size = 2175687, upload-time = "2025-10-24T07:17:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/050e02f80c7131b63db1474bff511e63c545b5a8636a24cbef3fc4da20b6/lupa-2.6-cp311-cp311-win32.whl", hash = "sha256:defaf188fde8f7a1e5ce3a5e6d945e533b8b8d547c11e43b96c9b7fe527f56dc", size = 1412592, upload-time = "2025-10-24T07:17:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/6f2af98aa5d771cea661f66c8eb8f53772ec1ab1dfbce24126cfcd189436/lupa-2.6-cp311-cp311-win_amd64.whl", hash = "sha256:9505ae600b5c14f3e17e70f87f88d333717f60411faca1ddc6f3e61dce85fa9e", size = 1669194, upload-time = "2025-10-24T07:18:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" }, + { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" }, + { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" }, + { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" }, + { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" }, + { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, + { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, + { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, + { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, + { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, + { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818, upload-time = "2025-10-24T07:18:53.378Z" }, + { url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850, upload-time = "2025-10-24T07:18:55.259Z" }, + { url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344, upload-time = "2025-10-24T07:18:57.05Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543, upload-time = "2025-10-24T07:18:59.251Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974, upload-time = "2025-10-24T07:19:01.449Z" }, + { url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458, upload-time = "2025-10-24T07:19:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683, upload-time = "2025-10-24T07:19:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892, upload-time = "2025-10-24T07:19:08.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641, upload-time = "2025-10-24T07:19:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857, upload-time = "2025-10-24T07:19:37.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191, upload-time = "2025-10-24T07:19:40.281Z" }, + { url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343, upload-time = "2025-10-24T07:19:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730, upload-time = "2025-10-24T07:19:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553, upload-time = "2025-10-24T07:19:17.123Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275, upload-time = "2025-10-24T07:19:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441, upload-time = "2025-10-24T07:19:22.509Z" }, + { url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324, upload-time = "2025-10-24T07:19:24.979Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250, upload-time = "2025-10-24T07:19:27.262Z" }, + { url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126, upload-time = "2025-10-24T07:19:29.522Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693, upload-time = "2025-10-24T07:19:31.379Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104, upload-time = "2025-10-24T07:19:33.514Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818, upload-time = "2025-10-24T07:19:36.039Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c5/918ed6c3af793764bae155d68df47bab2635ab7c94dee3dbb5d9c6d5ba38/lupa-2.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8897dc6c3249786b2cdf2f83324febb436193d4581b6a71dea49f77bf8b19bb0", size = 956718, upload-time = "2025-10-24T07:20:03.086Z" }, + { url = "https://files.pythonhosted.org/packages/da/91/0ca797da854478225c0f6a8fc3c500a2f5c11826d732735beb5dffff9e85/lupa-2.6-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:4446396ca3830be0c106c70db4b4f622c37b2d447874c07952cafb9c57949a4a", size = 1923934, upload-time = "2025-10-24T07:20:05.428Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7f/98a6a2535285d43457eb665822ab08447e2196b614db3152772d457ca426/lupa-2.6-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:5826e687c89995a6eaafeae242071ba16448eec1a9ee8e17ed48551b5d1e21c2", size = 987286, upload-time = "2025-10-24T07:20:07.338Z" }, + { url = "https://files.pythonhosted.org/packages/5f/50/edad7c180ab28aa543e6c3895e56a2c7a6ff92140a283316e6086f118552/lupa-2.6-cp39-cp39-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:5871935cb36d1d22f9c04ac0db75c06751bd95edcfa0d9309f732de908e297a9", size = 1176541, upload-time = "2025-10-24T07:20:09.305Z" }, + { url = "https://files.pythonhosted.org/packages/24/b3/27a0ec4c73011e86f9bd2eada010062308a4ed32921898d066ae54e064e1/lupa-2.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43eb6e43ea8512d0d65b995d36dd9d77aa02598035e25b84c23a1b58700c9fb2", size = 1058981, upload-time = "2025-10-24T07:20:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/a4/12/d55d45a8c253e7981f59ae920bac49dbd49888954b25fd1eb3a7be1321db/lupa-2.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:559714053018d9885cc8c36a33c5b7eb9aad30fb6357719cac3ce4dc6b39157e", size = 2103336, upload-time = "2025-10-24T07:20:13.71Z" }, + { url = "https://files.pythonhosted.org/packages/ab/17/058cc212c22d6a25990226afd02c741b2813b5f325396a9180b4accb70ac/lupa-2.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:57ac88a00ce59bd9d4ddcd4fca8e02564765725f5068786b011c9d1be3de20c5", size = 1083209, upload-time = "2025-10-24T07:20:15.587Z" }, + { url = "https://files.pythonhosted.org/packages/e6/01/8ca3a56a4e127784a15f0c7ec1f67e9894c2e9d4bf402389ddda4ea8081b/lupa-2.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:b683fbd867c2e54c44a686361b75eee7e7a790da55afdbe89f1f23b106de0274", size = 1193077, upload-time = "2025-10-24T07:20:17.857Z" }, + { url = "https://files.pythonhosted.org/packages/07/1b/c7fe79bcda6d489306bb7c1a9b4d545b7f0930b9ce80080643fc39b3fdc9/lupa-2.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d2f656903a2ed2e074bf2b7d300968028dfa327a45b055be8e3b51ef0b82f9bf", size = 2198584, upload-time = "2025-10-24T07:20:20.468Z" }, + { url = "https://files.pythonhosted.org/packages/9e/df/3f7631eea3478ac3868cbcb2763c1a4e2f7b875fcb2683f956bf7aabf65f/lupa-2.6-cp39-cp39-win32.whl", hash = "sha256:bf28f68ae231b72008523ab5ac23835ba0f76e0e99ec38b59766080a84eb596a", size = 1414693, upload-time = "2025-10-24T07:20:23.477Z" }, + { url = "https://files.pythonhosted.org/packages/08/e0/3fd9617814663129fa4a9b33a980c3fe344297337cb550c738f87f730f6b/lupa-2.6-cp39-cp39-win_amd64.whl", hash = "sha256:b4b2e9b3795a9897cf6cfcc58d08210fdc0d13ab47c9a0e13858c68932d8353c", size = 1658877, upload-time = "2025-10-24T07:20:27.086Z" }, +] + [[package]] name = "markdown" version = "3.9" @@ -2542,7 +3433,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.19.0" +version = "1.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "python_full_version >= '3.10'" }, @@ -2551,15 +3442,18 @@ dependencies = [ { name = "jsonschema", marker = "python_full_version >= '3.10'" }, { name = "pydantic", marker = "python_full_version >= '3.10'" }, { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyjwt", extra = ["crypto"], marker = "python_full_version >= '3.10'" }, { name = "python-multipart", version = "0.0.22", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pywin32", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, - { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "0.52.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/2b/916852a5668f45d8787378461eaa1244876d77575ffef024483c94c0649c/mcp-1.19.0.tar.gz", hash = "sha256:213de0d3cd63f71bc08ffe9cc8d4409cc87acffd383f6195d2ce0457c021b5c1", size = 444163, upload-time = "2025-10-24T01:11:15.839Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/a3/3e71a875a08b6a830b88c40bc413bff01f1650f1efe8a054b5e90a9d4f56/mcp-1.19.0-py3-none-any.whl", hash = "sha256:f5907fe1c0167255f916718f376d05f09a830a215327a3ccdd5ec8a519f2e572", size = 170105, upload-time = "2025-10-24T01:11:14.151Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, ] [[package]] @@ -2695,7 +3589,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.7.0" +version = "9.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -2711,9 +3605,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, ] [[package]] @@ -2741,15 +3635,17 @@ wheels = [ name = "mkdocstrings" version = "0.30.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { name = "jinja2" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "markupsafe" }, - { name = "mkdocs" }, - { name = "mkdocs-autorefs" }, - { name = "pymdown-extensions" }, + { name = "markupsafe", marker = "python_full_version < '3.10'" }, + { name = "mkdocs", marker = "python_full_version < '3.10'" }, + { name = "mkdocs-autorefs", marker = "python_full_version < '3.10'" }, + { name = "pymdown-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350, upload-time = "2025-09-19T10:49:26.446Z" } wheels = [ @@ -2759,6 +3655,31 @@ wheels = [ [package.optional-dependencies] python = [ { name = "mkdocstrings-python", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "jinja2", marker = "python_full_version >= '3.10'" }, + { name = "markdown", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe", marker = "python_full_version >= '3.10'" }, + { name = "mkdocs", marker = "python_full_version >= '3.10'" }, + { name = "mkdocs-autorefs", marker = "python_full_version >= '3.10'" }, + { name = "pymdown-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/4d/1ca8a9432579184599714aaeb36591414cc3d3bfd9d494f6db540c995ae4/mkdocstrings-1.0.2.tar.gz", hash = "sha256:48edd0ccbcb9e30a3121684e165261a9d6af4d63385fc4f39a54a49ac3b32ea8", size = 101048, upload-time = "2026-01-24T15:57:25.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/32/407a9a5fdd7d8ecb4af8d830b9bcdf47ea68f916869b3f44bac31f081250/mkdocstrings-1.0.2-py3-none-any.whl", hash = "sha256:41897815a8026c3634fe5d51472c3a569f92ded0ad8c7a640550873eea3b6817", size = 35443, upload-time = "2026-01-24T15:57:23.933Z" }, +] + +[package.optional-dependencies] +python = [ { name = "mkdocstrings-python", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] @@ -2772,7 +3693,7 @@ resolution-markers = [ dependencies = [ { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "mkdocs-autorefs", marker = "python_full_version < '3.10'" }, - { name = "mkdocstrings", marker = "python_full_version < '3.10'" }, + { name = "mkdocstrings", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/ae/58ab2bfbee2792e92a98b97e872f7c003deb903071f75d8d83aa55db28fa/mkdocstrings_python-1.18.2.tar.gz", hash = "sha256:4ad536920a07b6336f50d4c6d5603316fafb1172c5c882370cbbc954770ad323", size = 207972, upload-time = "2025-08-28T16:11:19.847Z" } @@ -2791,7 +3712,7 @@ resolution-markers = [ dependencies = [ { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "mkdocs-autorefs", marker = "python_full_version >= '3.10'" }, - { name = "mkdocstrings", marker = "python_full_version >= '3.10'" }, + { name = "mkdocstrings", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" } @@ -2800,47 +3721,220 @@ wheels = [ ] [[package]] -name = "mypy" -version = "1.14.1" +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/74525ebe3eb5fddcd6735fc03cbea3feeed4122b53bc798ac32d297ac9ae/multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f", size = 77107, upload-time = "2026-01-26T02:46:12.608Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9a/ce8744e777a74b3050b1bf56be3eed1053b3457302ea055f1ea437200a23/multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358", size = 44943, upload-time = "2026-01-26T02:46:14.016Z" }, + { url = "https://files.pythonhosted.org/packages/83/9c/1d2a283d9c6f31e260cb6c2fccadc3edcf6c4c14ee0929cd2af4d2606dd7/multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5", size = 44603, upload-time = "2026-01-26T02:46:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/87/9d/3b186201671583d8e8d6d79c07481a5aafd0ba7575e3d8566baec80c1e82/multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0", size = 240573, upload-time = "2026-01-26T02:46:16.783Z" }, + { url = "https://files.pythonhosted.org/packages/42/7d/a52f5d4d0754311d1ac78478e34dff88de71259a8585e05ee14e5f877caf/multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8", size = 240106, upload-time = "2026-01-26T02:46:18.432Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/d80118e6c30ff55b7d171bdc5520aad4b9626e657520b8d7c8ca8c2fad12/multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0", size = 219418, upload-time = "2026-01-26T02:46:20.526Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/896e60b3457f194de77c7de64f9acce9f75da0518a5230ce1df534f6747b/multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f", size = 252124, upload-time = "2026-01-26T02:46:22.157Z" }, + { url = "https://files.pythonhosted.org/packages/f4/de/ba6b30447c36a37078d0ba604aa12c1a52887af0c355236ca6e0a9d5286f/multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f", size = 249402, upload-time = "2026-01-26T02:46:23.718Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b2/50a383c96230e432895a2fd3bcfe1b65785899598259d871d5de6b93180c/multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e", size = 240346, upload-time = "2026-01-26T02:46:25.393Z" }, + { url = "https://files.pythonhosted.org/packages/89/37/16d391fd8da544b1489306e38a46785fa41dd0f0ef766837ed7d4676dde0/multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2", size = 237010, upload-time = "2026-01-26T02:46:27.408Z" }, + { url = "https://files.pythonhosted.org/packages/b0/24/3152ee026eda86d5d3e3685182911e6951af7a016579da931080ce6ac9ad/multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8", size = 232018, upload-time = "2026-01-26T02:46:29.941Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1f/48d3c27a72be7fd23a55d8847193c459959bf35a5bb5844530dab00b739b/multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941", size = 241498, upload-time = "2026-01-26T02:46:32.052Z" }, + { url = "https://files.pythonhosted.org/packages/1a/45/413643ae2952d0decdf6c1250f86d08a43e143271441e81027e38d598bd7/multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a", size = 247957, upload-time = "2026-01-26T02:46:33.666Z" }, + { url = "https://files.pythonhosted.org/packages/50/f8/f1d0ac23df15e0470776388bdb261506f63af1f81d28bacb5e262d6e12b6/multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de", size = 241651, upload-time = "2026-01-26T02:46:35.7Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c9/1a2a18f383cf129add66b6c36b75c3911a7ba95cf26cb141482de085cc12/multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5", size = 236371, upload-time = "2026-01-26T02:46:37.37Z" }, + { url = "https://files.pythonhosted.org/packages/bb/aa/77d87e3fca31325b87e0eb72d5fe9a7472dcb51391a42df7ac1f3842f6c0/multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0", size = 41426, upload-time = "2026-01-26T02:46:39.026Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b3/e8863e6a2da15a9d7e98976ff402e871b7352c76566df6c18d0378e0d9cf/multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4", size = 46180, upload-time = "2026-01-26T02:46:40.422Z" }, + { url = "https://files.pythonhosted.org/packages/93/d3/dd4fa951ad5b5fa216bf30054d705683d13405eea7459833d78f31b74c9c/multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9", size = 43231, upload-time = "2026-01-26T02:46:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, + { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, - { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, - { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, - { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, - { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, - { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, - { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, - { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, - { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, - { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, - { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, - { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, - { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, - { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, - { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, - { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, - { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, - { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, - { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, - { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, - { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, - { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, - { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, - { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] @@ -2852,6 +3946,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nexus-rpc" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/66/540687556bd28cf1ec370cc6881456203dfddb9dab047b8979c6865b5984/nexus_rpc-1.1.0.tar.gz", hash = "sha256:d65ad6a2f54f14e53ebe39ee30555eaeb894102437125733fb13034a04a44553", size = 77383, upload-time = "2025-07-07T19:03:58.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2f/9e9d0dcaa4c6ffa22b7aa31069a8a264c753ff8027b36af602cce038c92f/nexus_rpc-1.1.0-py3-none-any.whl", hash = "sha256:d1b007af2aba186a27e736f8eaae39c03aed05b488084ff6c3d1785c9ba2ad38", size = 27743, upload-time = "2025-07-07T19:03:57.556Z" }, +] + +[[package]] +name = "nexus-rpc" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/50/95d7bc91f900da5e22662c82d9bf0f72a4b01f2a552708bf2f43807707a1/nexus_rpc-1.2.0.tar.gz", hash = "sha256:b4ddaffa4d3996aaeadf49b80dfcdfbca48fe4cb616defaf3b3c5c2c8fc61890", size = 74142, upload-time = "2025-11-17T19:17:06.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/04/eaac430d0e6bf21265ae989427d37e94be5e41dc216879f1fbb6c5339942/nexus_rpc-1.2.0-py3-none-any.whl", hash = "sha256:977876f3af811ad1a09b2961d3d1ac9233bda43ff0febbb0c9906483b9d9f8a3", size = 28166, upload-time = "2025-11-17T19:17:05.64Z" }, +] + [[package]] name = "openai" version = "2.17.0" @@ -2871,6 +3996,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/97/284535aa75e6e84ab388248b5a323fc296b1f70530130dee37f7f4fbe856/openai-2.17.0-py3-none-any.whl", hash = "sha256:4f393fd886ca35e113aac7ff239bcd578b81d8f104f5aedc7d3693eb2af1d338", size = 1069524, upload-time = "2026-02-05T16:27:38.941Z" }, ] +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.39.1" @@ -2884,6 +4021,115 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, ] +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-api", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-exporter-otlp-proto-common", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-proto", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-sdk", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "wrapt", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-instrumentation", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-util-http", marker = "python_full_version >= '3.10'" }, + { name = "wrapt", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/08/11208bcfcab4fc2023252c3f322aa397fd9ad948355fea60f5fc98648603/opentelemetry_instrumentation_httpx-0.60b1.tar.gz", hash = "sha256:a506ebaf28c60112cbe70ad4f0338f8603f148938cb7b6794ce1051cd2b270ae", size = 20611, upload-time = "2025-12-11T13:37:01.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/59/b98e84eebf745ffc75397eaad4763795bff8a30cbf2373a50ed4e70646c5/opentelemetry_instrumentation_httpx-0.60b1-py3-none-any.whl", hash = "sha256:f37636dd742ad2af83d896ba69601ed28da51fa4e25d1ab62fde89ce413e275b", size = 15701, upload-time = "2025-12-11T13:36:04.56Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", version = "6.33.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/fc/c47bb04a1d8a941a4061307e1eddfa331ed4d0ab13d8a9781e6db256940a/opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6", size = 11053, upload-time = "2025-12-11T13:37:25.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" }, +] + [[package]] name = "orjson" version = "3.11.5" @@ -3080,11 +4326,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] @@ -3096,6 +4342,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] +[[package]] +name = "pathable" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, +] + [[package]] name = "pathspec" version = "1.0.4" @@ -3105,10 +4360,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] +[[package]] +name = "pathvalidate" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" }, +] + [[package]] name = "pillow" version = "11.3.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, @@ -3218,6 +4485,108 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, ] +[[package]] +name = "pillow" +version = "12.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, + { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" }, + { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, + { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, + { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, + { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, + { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, + { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, + { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, + { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, + { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, + { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, + { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, + { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, +] + [[package]] name = "platformdirs" version = "4.4.0" @@ -3274,28 +4643,35 @@ wheels = [ [[package]] name = "prek" -version = "0.2.22" +version = "0.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/a7/1e07536315f77d7b233cbf3dd916dc3424239c435ee0a0110c9b2cbcf6b0/prek-0.2.22.tar.gz", hash = "sha256:5abbda8bae0a63a18d3fe573162e8504a7b100e3603169cc2d06053891a02d7c", size = 267212, upload-time = "2025-12-13T12:57:51.797Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/f5/ee52def928dd1355c20bcfcf765e1e61434635c33f3075e848e7b83a157b/prek-0.3.2.tar.gz", hash = "sha256:dce0074ff1a21290748ca567b4bda7553ee305a8c7b14d737e6c58364a499364", size = 334229, upload-time = "2026-02-06T13:49:47.539Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/fe/ba9a940adc55d78b96b58376a8752e95261402c1e5812acce6ea1a000fb8/prek-0.2.22-py3-none-linux_armv6l.whl", hash = "sha256:d026b2d75529a743466000e8dd058d3d5e7c597c34905b333f2ede3d24cb23f1", size = 4798026, upload-time = "2025-12-13T12:57:45.286Z" }, - { url = "https://files.pythonhosted.org/packages/12/40/459cf510491271b08d19b4ef34f8293440eb472e633f4ffaf34179f39a12/prek-0.2.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:60b5bec94fa9f59fe5a9e90554c7346ceef81ea33d01bb18172d2576b07ac449", size = 4894023, upload-time = "2025-12-13T12:57:40.102Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0b/59e0438b1e7d1b6fa3f14174a916d369e27c421f8876f7ec7c7a52fbfae7/prek-0.2.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a0c7c6ceee536122916d32d26b6fa4fac9e95ba28631901164ffc0b0fed28a9e", size = 4615858, upload-time = "2025-12-13T12:57:57.471Z" }, - { url = "https://files.pythonhosted.org/packages/e9/27/ea40cf715717298fdf802da2b15a2c4445b8c114aae28cab6bf794d65670/prek-0.2.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c90273bef7b638dfc36dede62c494f958456330375ffce891c68321b2a7b46ba", size = 4810206, upload-time = "2025-12-13T12:57:46.534Z" }, - { url = "https://files.pythonhosted.org/packages/b5/12/d1c3db35839492236afb8642a2818d5b413e5fce4ea909bc7ddfb3d4591a/prek-0.2.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d8ecf202073433b87df2671a98bc44d3b68bb5711f7119b50b7bd65c2a67f13", size = 4722439, upload-time = "2025-12-13T12:57:48.106Z" }, - { url = "https://files.pythonhosted.org/packages/64/a0/0f24a9cacd5d78119f47063d860e03fa42b4d7dcf6803a49b0bef51b771b/prek-0.2.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4f26d76247ce7671cf5d9786e7fc86fdb43c065fd5507e8d64b3de7fd5e4447", size = 5037705, upload-time = "2025-12-13T12:57:50.596Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6e/7616f84141755f1d9fe232f0bd06589421ae0dabd99180fdae2840d22ae8/prek-0.2.22-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac1f0ea2c82e35eb0ffc98dfbcd9ee34cfd7350b64f97198da4c311a271cdb8f", size = 5453199, upload-time = "2025-12-13T12:57:38.458Z" }, - { url = "https://files.pythonhosted.org/packages/51/80/542a583db9b27bfd34954243666e451b266513bc742e0491cd61ff1b390e/prek-0.2.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f5d5131b9e57548f64d74665fd4414a8deb603a67d52ee18b3e6540cdb77733", size = 5399635, upload-time = "2025-12-13T12:57:43.359Z" }, - { url = "https://files.pythonhosted.org/packages/49/06/ca4e6fee73e14e1aced90f5c83b9cdf9a8e1c3b1aa1e4f45a2a65de05a28/prek-0.2.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a768484e1c94a33228765f63701261316b64e11c482abe2a35c54045d3f81feb", size = 5498340, upload-time = "2025-12-13T12:57:41.827Z" }, - { url = "https://files.pythonhosted.org/packages/85/a8/9636fc782db9c22d1740a8e5dc4e1ffc3a28099d074f812da46332e7c7a7/prek-0.2.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c941c8503ea537a84ea97074dc97b0f0dfd9861864883eb8b90586ed321847e", size = 5078431, upload-time = "2025-12-13T12:57:31.664Z" }, - { url = "https://files.pythonhosted.org/packages/4f/29/e78d2f444cf1f097aaaefee8910d7b9fe34195f06b086e0d2153b6c66e07/prek-0.2.22-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:946c6cfe18b17a7b53c49a389bf65f1e8e45a1b96bfdaeeacde21f5b5ca2d149", size = 4820871, upload-time = "2025-12-13T12:57:33.074Z" }, - { url = "https://files.pythonhosted.org/packages/fb/ec/779db6c35663e949b3f9989c584297aa115d3cc44822c149dbe40d51cd14/prek-0.2.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2cffd5809cf678b4300378d612b5da12cd2183ddc7aee78178db0b1ea48f0069", size = 4834431, upload-time = "2025-12-13T12:57:34.65Z" }, - { url = "https://files.pythonhosted.org/packages/1d/18/12bb4fece680457f4d4f13d21c5784675ce8b1db5c968261348c52087232/prek-0.2.22-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:f91df793cbc28647863eb54d578f37782736726671838ca92c9d0601329cb928", size = 4709742, upload-time = "2025-12-13T12:57:52.706Z" }, - { url = "https://files.pythonhosted.org/packages/3f/27/de1d9d037f59393568713121f4bfcea11cd546dcf96f214827983b8beccf/prek-0.2.22-py3-none-musllinux_1_1_i686.whl", hash = "sha256:3c40ba36b3e89817b20efe6163fd15387b81caf1f489060265d84103ae6e5184", size = 4925048, upload-time = "2025-12-13T12:57:49.348Z" }, - { url = "https://files.pythonhosted.org/packages/49/bf/d40eef2e5ccbc520da94c2463450d0ecab598c092684002b463fd5491ff6/prek-0.2.22-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a4154a419581723d12eccaa5b1d27686283c5c78b753c1984270d7e144a15fa7", size = 5192083, upload-time = "2025-12-13T12:57:35.957Z" }, - { url = "https://files.pythonhosted.org/packages/41/ba/11ea837a876dcc7f5df85962bc560c8627a962261f046a1615b0a6016b01/prek-0.2.22-py3-none-win32.whl", hash = "sha256:9fd3d629a256ce3171bebc3183f9c608022fff0db19a511307ab0f4c7682d5e3", size = 4586129, upload-time = "2025-12-13T12:57:54.438Z" }, - { url = "https://files.pythonhosted.org/packages/2f/8c/05ab6d11ac670664c99944e4819a77a63360aab253d8daf4ae411c705bcd/prek-0.2.22-py3-none-win_amd64.whl", hash = "sha256:ad7997ae4bef4fccc0a6761c00479bdd44f2a5bb7eb97aebda3b42fe785e10a1", size = 5273787, upload-time = "2025-12-13T12:57:37.205Z" }, - { url = "https://files.pythonhosted.org/packages/38/7a/53e8a550df705b5bf78a589c4e11d21485ac38c1a65e9c98fc3169a5eb25/prek-0.2.22-py3-none-win_arm64.whl", hash = "sha256:2442c0f12bd57675124542a92f5c799e7ffe52dc7cd98301c43c361849a3aef6", size = 4941186, upload-time = "2025-12-13T12:57:56.214Z" }, + { url = "https://files.pythonhosted.org/packages/76/69/70a5fc881290a63910494df2677c0fb241d27cfaa435bbcd0de5cd2e2443/prek-0.3.2-py3-none-linux_armv6l.whl", hash = "sha256:4f352f9c3fc98aeed4c8b2ec4dbf16fc386e45eea163c44d67e5571489bd8e6f", size = 4614960, upload-time = "2026-02-06T13:50:05.818Z" }, + { url = "https://files.pythonhosted.org/packages/c0/15/a82d5d32a2207ccae5d86ea9e44f2b93531ed000faf83a253e8d1108e026/prek-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4a000cfbc3a6ec7d424f8be3c3e69ccd595448197f92daac8652382d0acc2593", size = 4622889, upload-time = "2026-02-06T13:49:53.662Z" }, + { url = "https://files.pythonhosted.org/packages/89/75/ea833b58a12741397017baef9b66a6e443bfa8286ecbd645d14111446280/prek-0.3.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5436bdc2702cbd7bcf9e355564ae66f8131211e65fefae54665a94a07c3d450a", size = 4239653, upload-time = "2026-02-06T13:50:02.88Z" }, + { url = "https://files.pythonhosted.org/packages/10/b4/d9c3885987afac6e20df4cb7db14e3b0d5a08a77ae4916488254ebac4d0b/prek-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0161b5f584f9e7f416d6cf40a17b98f17953050ff8d8350ec60f20fe966b86b6", size = 4595101, upload-time = "2026-02-06T13:49:49.813Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/1a06473ed83dbc898de22838abdb13954e2583ce229f857f61828384634c/prek-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e641e8533bca38797eebb49aa89ed0e8db0e61225943b27008c257e3af4d631", size = 4521978, upload-time = "2026-02-06T13:49:41.266Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5e/c38390d5612e6d86b32151c1d2fdab74a57913473193591f0eb00c894c21/prek-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfca1810d49d3f9ef37599c958c4e716bc19a1d78a7e88cbdcb332e0b008994f", size = 4829108, upload-time = "2026-02-06T13:49:44.598Z" }, + { url = "https://files.pythonhosted.org/packages/80/a6/cecce2ab623747ff65ed990bb0d95fa38449ee19b348234862acf9392fff/prek-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d69d754299a95a85dc20196f633232f306bee7e7c8cba61791f49ce70404ec", size = 5357520, upload-time = "2026-02-06T13:49:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/a5/18/d6bcb29501514023c76d55d5cd03bdbc037737c8de8b6bc41cdebfb1682c/prek-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:539dcb90ad9b20837968539855df6a29493b328a1ae87641560768eed4f313b0", size = 4852635, upload-time = "2026-02-06T13:49:58.347Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0a/ae46f34ba27ba87aea5c9ad4ac9cd3e07e014fd5079ae079c84198f62118/prek-0.3.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1998db3d0cbe243984736c82232be51318f9192e2433919a6b1c5790f600b5fd", size = 4599484, upload-time = "2026-02-06T13:49:43.296Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/73bfb5b3f7c3583f9b0d431924873928705cdef6abb3d0461c37254a681b/prek-0.3.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:07ab237a5415a3e8c0db54de9d63899bcd947624bdd8820d26f12e65f8d19eb7", size = 4657694, upload-time = "2026-02-06T13:50:01.074Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/0994bc176e1a80110fad3babce2c98b0ac4007630774c9e18fc200a34781/prek-0.3.2-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0ced19701d69c14a08125f14a5dd03945982edf59e793c73a95caf4697a7ac30", size = 4509337, upload-time = "2026-02-06T13:49:54.891Z" }, + { url = "https://files.pythonhosted.org/packages/f9/13/e73f85f65ba8f626468e5d1694ab3763111513da08e0074517f40238c061/prek-0.3.2-py3-none-musllinux_1_1_i686.whl", hash = "sha256:ffb28189f976fa111e770ee94e4f298add307714568fb7d610c8a7095cb1ce59", size = 4697350, upload-time = "2026-02-06T13:50:04.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/47/98c46dcd580305b9960252a4eb966f1a7b1035c55c363f378d85662ba400/prek-0.3.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f63134b3eea14421789a7335d86f99aee277cb520427196f2923b9260c60e5c5", size = 4955860, upload-time = "2026-02-06T13:49:56.581Z" }, + { url = "https://files.pythonhosted.org/packages/73/42/1bb4bba3ff47897df11e9dfd774027cdfa135482c961a54e079af0faf45a/prek-0.3.2-py3-none-win32.whl", hash = "sha256:58c806bd1344becd480ef5a5ba348846cc000af0e1fbe854fef91181a2e06461", size = 4267619, upload-time = "2026-02-06T13:49:39.503Z" }, + { url = "https://files.pythonhosted.org/packages/97/11/6665f47a7c350d83de17403c90bbf7a762ef50876ece456a86f64f46fbfb/prek-0.3.2-py3-none-win_amd64.whl", hash = "sha256:70114b48e9eb8048b2c11b4c7715ce618529c6af71acc84dd8877871a2ef71a6", size = 4624324, upload-time = "2026-02-06T13:49:45.922Z" }, + { url = "https://files.pythonhosted.org/packages/22/e7/740997ca82574d03426f897fd88afe3fc8a7306b8c7ea342a8bc1c538488/prek-0.3.2-py3-none-win_arm64.whl", hash = "sha256:9144d176d0daa2469a25c303ef6f6fa95a8df015eb275232f5cb53551ecefef0", size = 4336008, upload-time = "2026-02-06T13:49:52.27Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, ] [[package]] @@ -3310,6 +4686,175 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/9b/01/0ebaec9003f5d619a7475165961f8e3083cf8644d704b60395df3601632d/propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", size = 80277, upload-time = "2025-10-08T19:48:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/04af97ac586b4ef6b9026c3fd36ee7798b737a832f5d3440a4280dcebd3a/propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", size = 45865, upload-time = "2025-10-08T19:48:37.859Z" }, + { url = "https://files.pythonhosted.org/packages/7c/19/b65d98ae21384518b291d9939e24a8aeac4fdb5101b732576f8f7540e834/propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", size = 47636, upload-time = "2025-10-08T19:48:39.038Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/317048c6d91c356c7154dca5af019e6effeb7ee15fa6a6db327cc19e12b4/propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", size = 201126, upload-time = "2025-10-08T19:48:40.774Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/0b2a7a5a6ee83292b4b997dbd80549d8ce7d40b6397c1646c0d9495f5a85/propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", size = 209837, upload-time = "2025-10-08T19:48:42.167Z" }, + { url = "https://files.pythonhosted.org/packages/a5/92/c699ac495a6698df6e497fc2de27af4b6ace10d8e76528357ce153722e45/propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", size = 215578, upload-time = "2025-10-08T19:48:43.56Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ee/14de81c5eb02c0ee4f500b4e39c4e1bd0677c06e72379e6ab18923c773fc/propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", size = 197187, upload-time = "2025-10-08T19:48:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/48dce9aaa6d8dd5a0859bad75158ec522546d4ac23f8e2f05fac469477dd/propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", size = 193478, upload-time = "2025-10-08T19:48:47.743Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/0516b563e801e1ace212afde869a0596a0d7115eec0b12d296d75633fb29/propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", size = 190650, upload-time = "2025-10-08T19:48:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/24/89/e0f7d4a5978cd56f8cd67735f74052f257dc471ec901694e430f0d1572fe/propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", size = 200251, upload-time = "2025-10-08T19:48:51.4Z" }, + { url = "https://files.pythonhosted.org/packages/06/7d/a1fac863d473876ed4406c914f2e14aa82d2f10dd207c9e16fc383cc5a24/propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781", size = 200919, upload-time = "2025-10-08T19:48:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4e/f86a256ff24944cf5743e4e6c6994e3526f6acfcfb55e21694c2424f758c/propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", size = 193211, upload-time = "2025-10-08T19:48:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/6e/3f/3fbad5f4356b068f1b047d300a6ff2c66614d7030f078cd50be3fec04228/propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", size = 38314, upload-time = "2025-10-08T19:48:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/a4/45/d78d136c3a3d215677abb886785aae744da2c3005bcb99e58640c56529b1/propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", size = 41912, upload-time = "2025-10-08T19:48:57.995Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/b0632941f25139f4e58450b307242951f7c2717a5704977c6d5323a800af/propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", size = 38450, upload-time = "2025-10-08T19:48:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "protobuf" +version = "5.29.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723", size = 425623, upload-time = "2026-02-04T22:54:40.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1", size = 423357, upload-time = "2026-02-04T22:54:25.805Z" }, + { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda", size = 435175, upload-time = "2026-02-04T22:54:28.592Z" }, + { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269", size = 418619, upload-time = "2026-02-04T22:54:30.266Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6", size = 320284, upload-time = "2026-02-04T22:54:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9", size = 320478, upload-time = "2026-02-04T22:54:32.909Z" }, + { url = "https://files.pythonhosted.org/packages/30/a4/ff263f5687815e1a10a9243a3a6463af42ca251224bf4b8fc4c93b9f5b80/protobuf-5.29.6-cp39-cp39-win32.whl", hash = "sha256:cb4c86de9cd8a7f3a256b9744220d87b847371c6b2f10bde87768918ef33ba49", size = 423352, upload-time = "2026-02-04T22:54:37.375Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/e943206d3b5069050d570a2c53a90631240d99adcc9a91c6ff7b41876f4d/protobuf-5.29.6-cp39-cp39-win_amd64.whl", hash = "sha256:76e07e6567f8baf827137e8d5b8204b6c7b6488bbbff1bf0a72b383f77999c18", size = 435222, upload-time = "2026-02-04T22:54:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/84d5f6dcda9165e4d6a56ac8433c9f40a8906bf2966150b8a0cfde097d78/protobuf-6.33.5-cp39-cp39-win32.whl", hash = "sha256:a3157e62729aafb8df6da2c03aa5c0937c7266c626ce11a278b6eb7963c4e37c", size = 425892, upload-time = "2026-01-29T21:51:30.382Z" }, + { url = "https://files.pythonhosted.org/packages/68/19/33d7dc2dc84439587fa1e21e1c0026c01ad2af0a62f58fd54002a7546307/protobuf-6.33.5-cp39-cp39-win_amd64.whl", hash = "sha256:8f04fa32763dcdb4973d537d6b54e615cc61108c7cb38fe59310c3192d29510a", size = 437137, upload-time = "2026-01-29T21:51:31.456Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + [[package]] name = "pwdlib" version = "0.2.1" @@ -3345,6 +4890,47 @@ argon2 = [ { name = "argon2-cffi", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] +[[package]] +name = "py-key-value-aio" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype", marker = "python_full_version >= '3.10'" }, + { name = "py-key-value-shared", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" }, +] + +[package.optional-dependencies] +disk = [ + { name = "diskcache", marker = "python_full_version >= '3.10'" }, + { name = "pathvalidate", marker = "python_full_version >= '3.10'" }, +] +keyring = [ + { name = "keyring", marker = "python_full_version >= '3.10'" }, +] +memory = [ + { name = "cachetools", marker = "python_full_version >= '3.10'" }, +] +redis = [ + { name = "redis", marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "py-key-value-shared" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" }, +] + [[package]] name = "pyasn1" version = "0.6.2" @@ -3413,85 +4999,193 @@ email = [ [[package]] name = "pydantic-ai" -version = "0.4.10" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "huggingface", "mcp", "mistral", "openai", "retries", "vertexai"] }, +resolution-markers = [ + "python_full_version < '3.10'", ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/67/eef2d1d64579c20ff732244c8f46f5b10401a47122b2404fd1a786860e0e/pydantic_ai-0.4.10.tar.gz", hash = "sha256:21cda3b62706dfdd5b9d662bb9a2ead50d51951f6ee0bbee45320b8752a95055", size = 43555371, upload-time = "2025-07-30T23:03:50.744Z" } +dependencies = [ + { name = "pydantic-ai-slim", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "huggingface", "mistral", "openai", "retries", "temporal", "vertexai"], marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/d7/fcc18ce80008e888404a3615f973aa3f39b98384d61b03621144c9f4c2d4/pydantic_ai-0.8.1.tar.gz", hash = "sha256:05974382082ee4f3706909d06bdfcc5e95f39e29230cc4d00e47429080099844", size = 43772581, upload-time = "2025-08-29T14:46:23.201Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/9c/3ea9ce322448e963fd84abb61985fa2d2ea0cb3f09bf0634cf43b2f49de6/pydantic_ai-0.4.10-py3-none-any.whl", hash = "sha256:4822325c92a57c9b45bdfd993106558e2397362b98ec211670d106c4b073ddd8", size = 10194, upload-time = "2025-07-30T23:03:42.435Z" }, + { url = "https://files.pythonhosted.org/packages/f9/04/802b8cf834dffcda8baabb3b76c549243694a83346c3f54e47a3a4d519fb/pydantic_ai-0.8.1-py3-none-any.whl", hash = "sha256:5fa923097132aa69b4d6a310b462dc091009c7b87705edf4443d37b887d5ef9a", size = 10188, upload-time = "2025-08-29T14:46:11.137Z" }, +] + +[[package]] +name = "pydantic-ai" +version = "1.56.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "pydantic-ai-slim", version = "1.56.0", source = { registry = "https://pypi.org/simple" }, extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai", "xai"], marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/1a/800a1e02b259152a49d4c11d9103784a7482c7e9b067eeea23e949d3d80f/pydantic_ai-1.56.0.tar.gz", hash = "sha256:643ff71612df52315b3b4c4b41543657f603f567223eb33245dc8098f005bdc4", size = 11795, upload-time = "2026-02-06T01:13:21.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/35/f4a7fd2b9962ddb9b021f76f293e74fda71da190bb74b57ed5b343c93022/pydantic_ai-1.56.0-py3-none-any.whl", hash = "sha256:b6b3ac74bdc004693834750da4420ea2cde0d3cbc3f134c0b7544f98f1c00859", size = 7222, upload-time = "2026-02-06T01:13:11.755Z" }, ] [[package]] name = "pydantic-ai-slim" -version = "0.4.10" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "eval-type-backport" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "httpx" }, - { name = "opentelemetry-api" }, - { name = "pydantic" }, - { name = "pydantic-graph" }, - { name = "typing-inspection" }, +resolution-markers = [ + "python_full_version < '3.10'", ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/fc/4123faf9372807e487c83acc858482f6d5a2f39ee6c1e25a895f23f700b6/pydantic_ai_slim-0.4.10.tar.gz", hash = "sha256:c9f6904aaa91c0309f91cc3a5617c570e153afbbb9888ee52190f58f029640f0", size = 189595, upload-time = "2025-07-30T23:03:55.44Z" } +dependencies = [ + { name = "eval-type-backport", marker = "python_full_version < '3.10'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "genai-prices", marker = "python_full_version < '3.10'" }, + { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "httpx", marker = "python_full_version < '3.10'" }, + { name = "opentelemetry-api", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "pydantic-graph", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-inspection", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/91/08137459b3745900501b3bd11852ced6c81b7ce6e628696d75b09bb786c5/pydantic_ai_slim-0.8.1.tar.gz", hash = "sha256:12ef3dcbe5e1dad195d5e256746ef960f6e59aeddda1a55bdd553ee375ff53ae", size = 218906, upload-time = "2025-08-29T14:46:27.517Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/d7/b1f6cf2eb9d24086de8a7f9e08956421fde514bcb7a4a3594b108e12636e/pydantic_ai_slim-0.4.10-py3-none-any.whl", hash = "sha256:49a386d6a900b4c1a81bca3469522a4c0eaab5a46a3953d1ffda8f0f2865ed97", size = 254994, upload-time = "2025-07-30T23:03:45.03Z" }, + { url = "https://files.pythonhosted.org/packages/11/ce/8dbadd04f578d02a9825a46e931005743fe223736296f30b55846c084fab/pydantic_ai_slim-0.8.1-py3-none-any.whl", hash = "sha256:fc7edc141b21fe42bc54a2d92c1127f8a75160c5e57a168dba154d3f4adb963f", size = 297821, upload-time = "2025-08-29T14:46:14.647Z" }, ] [package.optional-dependencies] ag-ui = [ - { name = "ag-ui-protocol" }, + { name = "ag-ui-protocol", marker = "python_full_version < '3.10'" }, { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] anthropic = [ - { name = "anthropic" }, + { name = "anthropic", marker = "python_full_version < '3.10'" }, ] bedrock = [ - { name = "boto3" }, + { name = "boto3", marker = "python_full_version < '3.10'" }, ] cli = [ - { name = "argcomplete" }, - { name = "prompt-toolkit" }, - { name = "rich" }, + { name = "argcomplete", marker = "python_full_version < '3.10'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.10'" }, + { name = "pyperclip", marker = "python_full_version < '3.10'" }, + { name = "rich", marker = "python_full_version < '3.10'" }, ] cohere = [ - { name = "cohere", marker = "sys_platform != 'emscripten'" }, + { name = "cohere", marker = "python_full_version < '3.10' and sys_platform != 'emscripten'" }, ] evals = [ - { name = "pydantic-evals" }, + { name = "pydantic-evals", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, ] google = [ { name = "google-genai", version = "1.47.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +groq = [ + { name = "groq", marker = "python_full_version < '3.10'" }, +] +huggingface = [ + { name = "huggingface-hub", extra = ["inference"], marker = "python_full_version < '3.10'" }, +] +mistral = [ + { name = "mistralai", marker = "python_full_version < '3.10'" }, +] +openai = [ + { name = "openai", marker = "python_full_version < '3.10'" }, +] +retries = [ + { name = "tenacity", version = "9.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +temporal = [ + { name = "temporalio", version = "1.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +vertexai = [ + { name = "google-auth", marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "1.56.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "genai-prices", marker = "python_full_version >= '3.10'" }, + { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-api", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "pydantic-graph", version = "1.56.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/5c/3a577825b9c1da8f287be7f2ee6fe9aab48bc8a80e65c8518052c589f51c/pydantic_ai_slim-1.56.0.tar.gz", hash = "sha256:9f9f9c56b1c735837880a515ae5661b465b40207b25f3a3434178098b2137f05", size = 415265, upload-time = "2026-02-06T01:13:23.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/4b/34682036528eeb9aaf093c2073540ddf399ab37b99d282a69ca41356f1aa/pydantic_ai_slim-1.56.0-py3-none-any.whl", hash = "sha256:d657e4113485020500b23b7390b0066e2a0277edc7577eaad2290735ca5dd7d5", size = 542270, upload-time = "2026-02-06T01:13:14.918Z" }, +] + +[package.optional-dependencies] +ag-ui = [ + { name = "ag-ui-protocol", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "0.52.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +anthropic = [ + { name = "anthropic", marker = "python_full_version >= '3.10'" }, +] +bedrock = [ + { name = "boto3", marker = "python_full_version >= '3.10'" }, +] +cli = [ + { name = "argcomplete", marker = "python_full_version >= '3.10'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.10'" }, + { name = "pyperclip", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, +] +cohere = [ + { name = "cohere", marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, +] +evals = [ + { name = "pydantic-evals", version = "1.56.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +fastmcp = [ + { name = "fastmcp", marker = "python_full_version >= '3.10'" }, +] +google = [ { name = "google-genai", version = "1.62.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] groq = [ - { name = "groq" }, + { name = "groq", marker = "python_full_version >= '3.10'" }, ] huggingface = [ - { name = "huggingface-hub" }, + { name = "huggingface-hub", extra = ["inference"], marker = "python_full_version >= '3.10'" }, +] +logfire = [ + { name = "logfire", extra = ["httpx"], marker = "python_full_version >= '3.10'" }, ] mcp = [ { name = "mcp", marker = "python_full_version >= '3.10'" }, ] mistral = [ - { name = "mistralai" }, + { name = "mistralai", marker = "python_full_version >= '3.10'" }, ] openai = [ - { name = "openai" }, + { name = "openai", marker = "python_full_version >= '3.10'" }, + { name = "tiktoken", marker = "python_full_version >= '3.10'" }, ] retries = [ - { name = "tenacity", version = "9.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "tenacity", version = "9.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] +temporal = [ + { name = "temporalio", version = "1.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +ui = [ + { name = "starlette", version = "0.52.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] vertexai = [ - { name = "google-auth" }, - { name = "requests" }, + { name = "google-auth", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, +] +xai = [ + { name = "xai-sdk", marker = "python_full_version >= '3.10'" }, ] [[package]] @@ -3627,20 +5321,44 @@ wheels = [ [[package]] name = "pydantic-evals" -version = "0.4.10" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "eval-type-backport", marker = "python_full_version < '3.11'" }, - { name = "logfire-api" }, - { name = "pydantic" }, - { name = "pydantic-ai-slim" }, - { name = "pyyaml" }, - { name = "rich" }, +resolution-markers = [ + "python_full_version < '3.10'", ] -sdist = { url = "https://files.pythonhosted.org/packages/98/b7/3f925d6ec9a2627e3310e87c2131514c3b44427c8975fafe83453825cfca/pydantic_evals-0.4.10.tar.gz", hash = "sha256:310acc7559d601743a101c606e50c0c5a9592bfc53cb733c7e54143b39e0fc97", size = 43729, upload-time = "2025-07-30T23:03:56.943Z" } +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.10'" }, + { name = "eval-type-backport", marker = "python_full_version < '3.10'" }, + { name = "logfire-api", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "pydantic-ai-slim", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "rich", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/9d/460a1f2c9f5f263e9d8e9661acbd654ccc81ad3373ea43048d914091a817/pydantic_evals-0.8.1.tar.gz", hash = "sha256:c398a623c31c19ce70e346ad75654fcb1517c3f6a821461f64fe5cbbe0813023", size = 43933, upload-time = "2025-08-29T14:46:28.903Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/9f/7972c450d74c2b0f6d9f9de644ac33c4c63f0f2156f8acd37a274b1f43a3/pydantic_evals-0.4.10-py3-none-any.whl", hash = "sha256:5fda10c5ced2c99f03b407bd56645574598e6daab0e5be2ed7056e815e6037f6", size = 52511, upload-time = "2025-07-30T23:03:46.717Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f9/1d21c4687167c4fa76fd3b1ed47f9bc2d38fd94cbacd9aa3f19e82e59830/pydantic_evals-0.8.1-py3-none-any.whl", hash = "sha256:6c76333b1d79632f619eb58a24ac656e9f402c47c75ad750ba0230d7f5514344", size = 52602, upload-time = "2025-08-29T14:46:16.602Z" }, +] + +[[package]] +name = "pydantic-evals" +version = "1.56.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "logfire-api", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "pydantic-ai-slim", version = "1.56.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f2/8c59284a2978af3fbda45ae3217218eaf8b071207a9290b54b7613983e5d/pydantic_evals-1.56.0.tar.gz", hash = "sha256:206635107127af6a3ee4b1fc8f77af6afb14683615a2d6b3609f79467c1c0d28", size = 47210, upload-time = "2026-02-06T01:13:25.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/51/9875d19ff6d584aaeb574aba76b49d931b822546fc60b29c4fc0da98170d/pydantic_evals-1.56.0-py3-none-any.whl", hash = "sha256:d1efb410c97135aabd2a22453b10c981b2b9851985e9354713af67ae0973b7a9", size = 56407, upload-time = "2026-02-06T01:13:17.098Z" }, ] [[package]] @@ -3658,17 +5376,39 @@ wheels = [ [[package]] name = "pydantic-graph" -version = "0.4.10" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "logfire-api" }, - { name = "pydantic" }, - { name = "typing-inspection" }, +resolution-markers = [ + "python_full_version < '3.10'", ] -sdist = { url = "https://files.pythonhosted.org/packages/04/46/62b83bd471a8743c41aba6693f55b746b8ee1294f64b7e9f42db7c3fd4b5/pydantic_graph-0.4.10.tar.gz", hash = "sha256:034063ac0ce2143a877a4fac563520492e70dde42d262b22c928f081d7759c5b", size = 21978, upload-time = "2025-07-30T23:03:57.986Z" } +dependencies = [ + { name = "httpx", marker = "python_full_version < '3.10'" }, + { name = "logfire-api", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "typing-inspection", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/97/b35b7cb82d9f1bb6d5c6d21bba54f6196a3a5f593373f3a9c163a3821fd7/pydantic_graph-0.8.1.tar.gz", hash = "sha256:c61675a05c74f661d4ff38d04b74bd652c1e0959467801986f2f85dc7585410d", size = 21675, upload-time = "2025-08-29T14:46:29.839Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/55/0026897b1cd6ab6b179b25c06b1dff496a3ebb83e30c6acb1a5a40e80cd1/pydantic_graph-0.4.10-py3-none-any.whl", hash = "sha256:e5128d5e370a6391aa6441eaefff6b3a139e583e0ac6755d83b88c9df9c6c6a7", size = 27578, upload-time = "2025-07-30T23:03:47.925Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e3/5908643b049bb2384d143885725cbeb0f53707d418357d4d1ac8d2c82629/pydantic_graph-0.8.1-py3-none-any.whl", hash = "sha256:f1dd5db0fe22f4e3323c04c65e2f0013846decc312b3efc3196666764556b765", size = 27239, upload-time = "2025-08-29T14:46:18.317Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "1.56.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "logfire-api", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/03/f92881cdb12d6f43e60e9bfd602e41c95408f06e2324d3729f7a194e2bcd/pydantic_graph-1.56.0.tar.gz", hash = "sha256:5e22972dbb43dbc379ab9944252ff864019abf3c7d465dcdf572fc8aec9a44a1", size = 58460, upload-time = "2026-02-06T01:13:26.708Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/07/8c823eb4d196137c123d4d67434e185901d3cbaea3b0c2b7667da84e72c1/pydantic_graph-1.56.0-py3-none-any.whl", hash = "sha256:ec3f0a1d6fcedd4eb9c59fef45079c2ee4d4185878d70dae26440a9c974c6bb3", size = 72346, upload-time = "2026-02-06T01:13:18.792Z" }, ] [[package]] @@ -3706,6 +5446,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, ] +[[package]] +name = "pydocket" +version = "0.17.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle", marker = "python_full_version >= '3.10'" }, + { name = "croniter", marker = "python_full_version >= '3.10'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "fakeredis", extra = ["lua"], marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-api", marker = "python_full_version >= '3.10'" }, + { name = "prometheus-client", marker = "python_full_version >= '3.10'" }, + { name = "py-key-value-aio", extra = ["memory", "redis"], marker = "python_full_version >= '3.10'" }, + { name = "python-json-logger", marker = "python_full_version >= '3.10'" }, + { name = "redis", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, + { name = "taskgroup", marker = "python_full_version == '3.10.*'" }, + { name = "typer", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/26/ac23ead3725475468b50b486939bf5feda27180050a614a7407344a0af0e/pydocket-0.17.5.tar.gz", hash = "sha256:19a6976d8fd11c1acf62feb0291a339e06beaefa100f73dd38c6499760ad3e62", size = 334829, upload-time = "2026-01-30T18:44:39.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/98/73427d065c067a99de6afbe24df3d90cf20d63152ceb42edff2b6e829d4c/pydocket-0.17.5-py3-none-any.whl", hash = "sha256:544d7c2625a33e52528ac24db25794841427dfc2cf30b9c558ac387c77746241", size = 93355, upload-time = "2026-01-30T18:44:37.972Z" }, +] + [[package]] name = "pyee" version = "13.0.0" @@ -3746,11 +5510,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.9.0" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c", size = 78825, upload-time = "2024-08-01T15:01:08.445Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", size = 22344, upload-time = "2024-08-01T15:01:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, ] [package.optional-dependencies] @@ -3807,6 +5571,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, ] +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -3876,6 +5649,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + [[package]] name = "python-multipart" version = "0.0.20" @@ -3913,6 +5695,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, ] +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/51/2a/f125667ce48105bf1f4e50e03cfa7b24b8c4f47684d7f1cf4dcb6f6b1c15/pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3", size = 161464, upload-time = "2026-01-30T01:03:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/40/df/065a30790a7ca6bb48ad9018dd44668ed9135610ebf56a2a4cb8e513fd5c/pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1", size = 246159, upload-time = "2026-01-30T01:03:40.131Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1c/fd09976a7e04960dabc07ab0e0072c7813d566ec67d5490a4c600683c158/pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db", size = 259120, upload-time = "2026-01-30T01:03:41.233Z" }, + { url = "https://files.pythonhosted.org/packages/52/49/59fdc6fc5a390ae9f308eadeb97dfc70fc2d804ffc49dd39fc97604622ec/pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1", size = 262196, upload-time = "2026-01-30T01:03:42.696Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/d6734dccf0080e3dc00a55b0827ab5af30c886f8bc127bbc04bc3445daec/pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a", size = 103510, upload-time = "2026-01-30T01:03:43.915Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -3938,6 +5773,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -4032,18 +5876,168 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/96/7935186fba032312eb8a75e6503440b0e6de76c901421f791408e4debd93/rcslice-1.1.0-py3-none-any.whl", hash = "sha256:1b12fc0c0ca452e8a9fd2b56ac008162f19e250906a4290a7e7a98be3200c2a6", size = 5180, upload-time = "2018-09-27T12:44:05.197Z" }, ] +[[package]] +name = "redis" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version >= '3.10' and python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, +] + [[package]] name = "referencing" -version = "0.37.0" +version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs", marker = "python_full_version >= '3.10'" }, { name = "rpds-py", marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, + { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, + { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, + { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, + { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, + { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" }, + { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" }, + { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, + { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, + { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, + { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, + { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" }, + { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" }, + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, + { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e7/0e1913dc52eee9c5cf8417c9813c4c55972a3f37d27cfa2e623b79b63dbc/regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5", size = 488185, upload-time = "2026-01-14T23:17:25.2Z" }, + { url = "https://files.pythonhosted.org/packages/78/df/c52c1ff4221529faad0953e197982fe9508c6dbb42327e31bf98ea07472a/regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815", size = 290628, upload-time = "2026-01-14T23:17:27.125Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/a2fef3717deaff647d7de2bccf899a576c7eaf042b6b271fc4474515fe97/regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b", size = 288509, upload-time = "2026-01-14T23:17:29.017Z" }, + { url = "https://files.pythonhosted.org/packages/70/89/faf5ee5c69168753c845a3d58b4683f61c899d162bfe1264fca88d5b3924/regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6", size = 781088, upload-time = "2026-01-14T23:17:30.961Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2c/707e5c380ad547c93686e21144e7e24dc2064dd84ec5b751b6dbdfc9be2b/regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b", size = 850516, upload-time = "2026-01-14T23:17:32.946Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3b/baa816cdcad1c0f8195f9f40ab2b2a2246c8a2989dcd90641c0c6559e3fd/regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df", size = 898124, upload-time = "2026-01-14T23:17:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/e7/74/1eb46bde30899825ed9fdf645eba16b7b97c49d12d300f5177989b9a09a4/regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e", size = 791290, upload-time = "2026-01-14T23:17:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5d/b72e176fb21e2ec248baed01151a342d1f44dd43c2b6bb6a41ad183b274e/regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c", size = 781996, upload-time = "2026-01-14T23:17:40.109Z" }, + { url = "https://files.pythonhosted.org/packages/61/0e/d3b3710eaafd994a4a71205d114abc38cda8691692a2ce2313abe68e7eb7/regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839", size = 767578, upload-time = "2026-01-14T23:17:42.134Z" }, + { url = "https://files.pythonhosted.org/packages/09/51/c6a6311833e040f95d229a34d82ac1cec2af8a5c00d58b244f2fceecef87/regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c", size = 774354, upload-time = "2026-01-14T23:17:44.392Z" }, + { url = "https://files.pythonhosted.org/packages/cc/97/c522d1f19fb2c549aaf680b115c110cd124c02062bc8c95f33db8583b4bb/regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077", size = 845297, upload-time = "2026-01-14T23:17:47.145Z" }, + { url = "https://files.pythonhosted.org/packages/99/a0/99468c386ab68a5e24c946c5c353c29c33a95523e275c17839f2446db15d/regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4", size = 755132, upload-time = "2026-01-14T23:17:49.796Z" }, + { url = "https://files.pythonhosted.org/packages/70/33/d5748c7b6c9d3621f12570583561ba529e2d1b12e4f70b8f17979b133e65/regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a", size = 835662, upload-time = "2026-01-14T23:17:52.559Z" }, + { url = "https://files.pythonhosted.org/packages/ad/15/1986972c276672505437f1ba3c9706c2d91f321cfb9b2f4d06e8bff1b999/regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3", size = 779513, upload-time = "2026-01-14T23:17:54.711Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f9/124f6a5cb3969d8e30471ed4f46cfc17c47aef1a9863ee8b4ba1d98b1bc4/regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a", size = 265923, upload-time = "2026-01-14T23:17:56.69Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c2/bb8fad7d27f1d71fc9772befd544bccd22eddc62a6735f57b003b4aff005/regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc", size = 277900, upload-time = "2026-01-14T23:17:58.72Z" }, + { url = "https://files.pythonhosted.org/packages/f7/fa/4e033327c1d8350bc812cac906d873984d3d4b39529252f392a47ccc356d/regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5", size = 270413, upload-time = "2026-01-14T23:18:00.764Z" }, ] [[package]] @@ -4076,6 +6070,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + [[package]] name = "rich-toolkit" version = "0.18.1" @@ -4374,28 +6381,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.14" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, - { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, - { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, - { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, - { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, - { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, - { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, - { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, - { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, + { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, ] [[package]] @@ -4410,6 +6416,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.10'" }, + { name = "jeepney", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + [[package]] name = "sentry-sdk" version = "2.52.0" @@ -4548,15 +6567,15 @@ wheels = [ [[package]] name = "sqlmodel" -version = "0.0.31" +version = "0.0.32" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/b8/e7cd6def4a773f25d6e29ffce63ccbfd6cf9488b804ab6fb9b80d334b39d/sqlmodel-0.0.31.tar.gz", hash = "sha256:2d41a8a9ee05e40736e2f9db8ea28cbfe9b5d4e5a18dd139e80605025e0c516c", size = 94952, upload-time = "2025-12-28T12:35:01.436Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/89/67f8964f3b2ed073fa4e95201e708291935d00e3600f36f09c1be3e279fe/sqlmodel-0.0.32.tar.gz", hash = "sha256:48e8fe4c8c3d7d8bf8468db17fa92ca680421e86cfec8b352217ef40736767be", size = 94140, upload-time = "2026-02-01T18:19:14.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/72/5aa5be921800f6418a949a73c9bb7054890881143e6bc604a93d228a95a3/sqlmodel-0.0.31-py3-none-any.whl", hash = "sha256:6d946d56cac4c2db296ba1541357cee2e795d68174e2043cd138b916794b1513", size = 27093, upload-time = "2025-12-28T12:35:00.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/de/d9b40ed2c570fd612c2abd57e4d9084a9d8eb1797447e2ce897b77b1c4b2/sqlmodel-0.0.32-py3-none-any.whl", hash = "sha256:d62f0702599592046c1a136d3512feab3d5a80e2988642ef0ed2c89b9b8b297b", size = 27416, upload-time = "2026-02-01T18:19:15.992Z" }, ] [[package]] @@ -4565,7 +6584,7 @@ version = "3.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "python_full_version >= '3.10'" }, - { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "0.52.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } wheels = [ @@ -4590,7 +6609,7 @@ wheels = [ [[package]] name = "starlette" -version = "0.50.0" +version = "0.52.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", @@ -4600,9 +6619,9 @@ dependencies = [ { name = "anyio", marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] [[package]] @@ -4626,7 +6645,7 @@ wheels = [ [[package]] name = "strawberry-graphql" -version = "0.291.1" +version = "0.291.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", @@ -4639,9 +6658,9 @@ dependencies = [ { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/18/5a07bc95bdf1cb73e93cd1f1d5b11750ded6f5e4d9407e7d500abc635b36/strawberry_graphql-0.291.1.tar.gz", hash = "sha256:13463d09e042857e4c12eee0eafc2bbdef963e4a30e20ed9a9c9fb0d487d4aac", size = 217765, upload-time = "2026-02-06T10:07:15.861Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/dd/e0e68f4b17da6ff5773fcd4bebf86fc4ff8620c854be816d047e9af8c4aa/strawberry_graphql-0.291.2.tar.gz", hash = "sha256:e6076604a786e8437bc64a27348584c082113442f072daf757b56e4863543a97", size = 217730, upload-time = "2026-02-06T14:40:51.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/7b/47ec56118decc90b9ae85a98180dc326fdad2ee031cba0e05c6c62349b88/strawberry_graphql-0.291.1-py3-none-any.whl", hash = "sha256:09798d3172b42be4d5526771ec581e1b1f93c7020b82a8b07b4c507676f75aa0", size = 316439, upload-time = "2026-02-06T10:07:18.247Z" }, + { url = "https://files.pythonhosted.org/packages/6f/14/93908a029605955e3411cdb0f2e8cfe170e5331da23357ed71d5c51b15bc/strawberry_graphql-0.291.2-py3-none-any.whl", hash = "sha256:f71d3669086c6747fd4760e6fafe3605d9a33f7d168886e5edd2b61a04972e56", size = 316389, upload-time = "2026-02-06T14:40:53.482Z" }, ] [[package]] @@ -4656,6 +6675,65 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/43/47c7cf84b3bd74a8631b02d47db356656bb8dff6f2e61a4c749963814d0d/super_collections-0.6.2-py3-none-any.whl", hash = "sha256:291b74d26299e9051d69ad9d89e61b07b6646f86a57a2f5ab3063d206eee9c56", size = 16173, upload-time = "2025-09-30T00:37:07.104Z" }, ] +[[package]] +name = "taskgroup" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version >= '3.10' and python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/e218e0160cc1b692e6e0e5ba34e8865dbb171efeb5fc9a704544b3020605/taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d", size = 11504, upload-time = "2025-01-03T09:24:13.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/b1/74babcc824a57904e919f3af16d86c08b524c0691504baf038ef2d7f655c/taskgroup-0.2.2-py2.py3-none-any.whl", hash = "sha256:e2c53121609f4ae97303e9ea1524304b4de6faf9eb2c9280c7f87976479a52fb", size = 14237, upload-time = "2025-01-03T09:24:11.41Z" }, +] + +[[package]] +name = "temporalio" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "nexus-rpc", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, + { name = "types-protobuf", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/32/375ab75d0ebb468cf9c8abbc450a03d3a8c66401fc320b338bd8c00d36b4/temporalio-1.16.0.tar.gz", hash = "sha256:dd926f3e30626fd4edf5e0ce596b75ecb5bbe0e4a0281e545ac91b5577967c91", size = 1733873, upload-time = "2025-08-21T22:12:50.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/36/12bb7234c83ddca4b8b032c8f1a9e07a03067c6ed6d2ddb39c770a4c87c6/temporalio-1.16.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:547c0853310350d3e5b5b9c806246cbf2feb523f685b05bf14ec1b0ece8a7bb6", size = 12540769, upload-time = "2025-08-21T22:11:24.551Z" }, + { url = "https://files.pythonhosted.org/packages/3c/16/a7d402435b8f994979abfeffd3f5ffcaaeada467ac16438e61c51c9f7abe/temporalio-1.16.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b05bb0d06025645aed6f936615311a6774eb8dc66280f32a810aac2283e1258", size = 12968631, upload-time = "2025-08-21T22:11:48.375Z" }, + { url = "https://files.pythonhosted.org/packages/11/6f/16663eef877b61faa5fd917b3a63497416ec4319195af75f6169a1594479/temporalio-1.16.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a08aed4e0f6c2b6bfc779b714e91dfe8c8491a0ddb4c4370627bb07f9bddcfd", size = 13164612, upload-time = "2025-08-21T22:12:16.366Z" }, + { url = "https://files.pythonhosted.org/packages/af/0e/8c6704ca7033aa09dc084f285d70481d758972cc341adc3c84d5f82f7b01/temporalio-1.16.0-cp39-abi3-win_amd64.whl", hash = "sha256:7c190362b0d7254f1f93fb71456063e7b299ac85a89f6227758af82c6a5aa65b", size = 13177058, upload-time = "2025-08-21T22:12:44.239Z" }, +] + +[[package]] +name = "temporalio" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.10' and python_full_version < '3.14'", +] +dependencies = [ + { name = "nexus-rpc", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "protobuf", version = "6.33.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version == '3.10.*'" }, + { name = "types-protobuf", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/db/7d5118d28b0918888e1ec98f56f659fdb006351e06d95f30f4274962a76f/temporalio-1.20.0.tar.gz", hash = "sha256:5a6a85b7d298b7359bffa30025f7deac83c74ac095a4c6952fbf06c249a2a67c", size = 1850498, upload-time = "2025-11-25T21:25:20.225Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/1b/e69052aa6003eafe595529485d9c62d1382dd5e671108f1bddf544fb6032/temporalio-1.20.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:fba70314b4068f8b1994bddfa0e2ad742483f0ae714d2ef52e63013ccfd7042e", size = 12061638, upload-time = "2025-11-25T21:24:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/3e8c67ed7f23bedfa231c6ac29a7a9c12b89881da7694732270f3ecd6b0c/temporalio-1.20.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffc5bb6cabc6ae67f0bfba44de6a9c121603134ae18784a2ff3a7f230ad99080", size = 11562603, upload-time = "2025-11-25T21:25:01.721Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/ed0cc11702210522a79e09703267ebeca06eb45832b873a58de3ca76b9d0/temporalio-1.20.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1e80c1e4cdf88fa8277177f563edc91466fe4dc13c0322f26e55c76b6a219e6", size = 11824016, upload-time = "2025-11-25T21:25:06.771Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/09c5cafabc80139d97338a2bdd8ec22e08817dfd2949ab3e5b73565006eb/temporalio-1.20.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba92d909188930860c9d89ca6d7a753bc5a67e4e9eac6cea351477c967355eed", size = 12189521, upload-time = "2025-11-25T21:25:12.091Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/5689c014a76aff3b744b3ee0d80815f63b1362637814f5fbb105244df09b/temporalio-1.20.0-cp310-abi3-win_amd64.whl", hash = "sha256:eacfd571b653e0a0f4aa6593f4d06fc628797898f0900d400e833a1f40cad03a", size = 12745027, upload-time = "2025-11-25T21:25:16.827Z" }, +] + [[package]] name = "tenacity" version = "9.1.2" @@ -4715,6 +6793,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, ] +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d1/7507bfb9c2ceef52ae3ae813013215c185648e21127538aae66dedd3af9c/tiktoken-0.12.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d51d75a5bffbf26f86554d28e78bfb921eae998edc2675650fd04c7e1f0cdc1e", size = 1053407, upload-time = "2025-10-06T20:22:35.492Z" }, + { url = "https://files.pythonhosted.org/packages/ee/4a/8ea1da602ac39dee4356b4cd6040a2325507482c36043044b6f581597b4f/tiktoken-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:09eb4eae62ae7e4c62364d9ec3a57c62eea707ac9a2b2c5d6bd05de6724ea179", size = 997150, upload-time = "2025-10-06T20:22:37.286Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1a/62d1d36b167eccd441aff2f0091551ca834295541b949d161021aa658167/tiktoken-0.12.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:df37684ace87d10895acb44b7f447d4700349b12197a526da0d4a4149fde074c", size = 1131575, upload-time = "2025-10-06T20:22:39.023Z" }, + { url = "https://files.pythonhosted.org/packages/f7/16/544207d63c8c50edd2321228f21d236e4e49d235128bb7e3e0f69eed0807/tiktoken-0.12.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:4c9614597ac94bb294544345ad8cf30dac2129c05e2db8dc53e082f355857af7", size = 1154920, upload-time = "2025-10-06T20:22:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/4c/0a3504157c81364fc0c64cada54efef0567961357e786706ea63bc8946e1/tiktoken-0.12.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:20cf97135c9a50de0b157879c3c4accbb29116bcf001283d26e073ff3b345946", size = 1196766, upload-time = "2025-10-06T20:22:41.365Z" }, + { url = "https://files.pythonhosted.org/packages/d4/46/8e6a258ae65447c75770fe5ea8968acab369e8c9f537f727c91f83772325/tiktoken-0.12.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:15d875454bbaa3728be39880ddd11a5a2a9e548c29418b41e8fd8a767172b5ec", size = 1258278, upload-time = "2025-10-06T20:22:42.846Z" }, + { url = "https://files.pythonhosted.org/packages/35/43/3b95de4f5e76f3cafc70dac9b1b9cfe759ff3bfd494ac91a280e93772e90/tiktoken-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cff3688ba3c639ebe816f8d58ffbbb0aa7433e23e08ab1cade5d175fc973fb3", size = 881888, upload-time = "2025-10-06T20:22:44.059Z" }, +] + [[package]] name = "tinycss2" version = "1.4.0" @@ -4905,20 +7051,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, ] -[[package]] -name = "typer-slim" -version = "0.21.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl", hash = "sha256:6e6c31047f171ac93cc5a973c9e617dbc5ab2bddc4d0a3135dc161b4e2020e0d", size = 47444, upload-time = "2026-01-06T11:21:12.441Z" }, -] - [[package]] name = "types-orjson" version = "3.6.2" @@ -4928,6 +7060,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/84/b34abd2d08381c5113e475908a1d79d27dc9a15f669213cee4ca03d1a891/types_orjson-3.6.2-py3-none-any.whl", hash = "sha256:22ee9a79236b6b0bfb35a0684eded62ad930a88a56797fa3c449b026cf7dbfe4", size = 2224, upload-time = "2022-01-07T11:31:09.271Z" }, ] +[[package]] +name = "types-protobuf" +version = "6.32.1.20251210" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/59/c743a842911887cd96d56aa8936522b0cd5f7a7f228c96e81b59fced45be/types_protobuf-6.32.1.20251210.tar.gz", hash = "sha256:c698bb3f020274b1a2798ae09dc773728ce3f75209a35187bd11916ebfde6763", size = 63900, upload-time = "2025-12-10T03:14:25.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/43/58e75bac4219cbafee83179505ff44cae3153ec279be0e30583a73b8f108/types_protobuf-6.32.1.20251210-py3-none-any.whl", hash = "sha256:2641f78f3696822a048cfb8d0ff42ccd85c25f12f871fbebe86da63793692140", size = 77921, upload-time = "2025-12-10T03:14:24.477Z" }, +] + [[package]] name = "types-requests" version = "2.31.0.6" @@ -4961,11 +7102,11 @@ wheels = [ [[package]] name = "types-ujson" -version = "5.10.0.20240515" +version = "5.10.0.20250822" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/49/abb4bcb9f2258f785edbf236b517c3e7ba8a503a8cbce6b5895930586cc0/types-ujson-5.10.0.20240515.tar.gz", hash = "sha256:ceae7127f0dafe4af5dd0ecf98ee13e9d75951ef963b5c5a9b7ea92e0d71f0d7", size = 3571, upload-time = "2024-05-15T02:24:43.704Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/bd/d372d44534f84864a96c19a7059d9b4d29db8541828b8b9dc3040f7a46d0/types_ujson-5.10.0.20250822.tar.gz", hash = "sha256:0a795558e1f78532373cf3f03f35b1f08bc60d52d924187b97995ee3597ba006", size = 8437, upload-time = "2025-08-22T03:02:19.433Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/1f/9d018cee3d09ab44a5211f0b5ed9b0422ad9a8c226bf3967f5884498d8f0/types_ujson-5.10.0.20240515-py3-none-any.whl", hash = "sha256:02bafc36b3a93d2511757a64ff88bd505e0a57fba08183a9150fbcfcb2015310", size = 2757, upload-time = "2024-05-15T02:24:42.315Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f2/d812543c350674d8b3f6e17c8922248ee3bb752c2a76f64beb8c538b40cf/types_ujson-5.10.0.20250822-py3-none-any.whl", hash = "sha256:3e9e73a6dc62ccc03449d9ac2c580cd1b7a8e4873220db498f7dd056754be080", size = 7657, upload-time = "2025-08-22T03:02:18.699Z" }, ] [[package]] @@ -5483,6 +7624,246 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, ] +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/41/be/be9b3b0a461ee3e30278706f3f3759b9b69afeedef7fe686036286c04ac6/wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc", size = 53485, upload-time = "2025-08-12T05:51:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a8/8f61d6b8f526efc8c10e12bf80b4206099fea78ade70427846a37bc9cbea/wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9", size = 38675, upload-time = "2025-08-12T05:51:42.885Z" }, + { url = "https://files.pythonhosted.org/packages/48/f1/23950c29a25637b74b322f9e425a17cc01a478f6afb35138ecb697f9558d/wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d", size = 38956, upload-time = "2025-08-12T05:52:03.149Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/dd0791943613885f62619f18ee6107e6133237a6b6ed8a9ecfac339d0b4f/wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a", size = 81745, upload-time = "2025-08-12T05:52:49.62Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/bb2d19bd1a614cc4f438abac13ae26c57186197920432d2a915183b15a8b/wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139", size = 82833, upload-time = "2025-08-12T05:52:27.738Z" }, + { url = "https://files.pythonhosted.org/packages/8d/eb/66579aea6ad36f07617fedca8e282e49c7c9bab64c63b446cfe4f7f47a49/wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df", size = 81889, upload-time = "2025-08-12T05:52:29.023Z" }, + { url = "https://files.pythonhosted.org/packages/04/9c/a56b5ac0e2473bdc3fb11b22dd69ff423154d63861cf77911cdde5e38fd2/wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b", size = 81344, upload-time = "2025-08-12T05:52:50.869Z" }, + { url = "https://files.pythonhosted.org/packages/93/4c/9bd735c42641d81cb58d7bfb142c58f95c833962d15113026705add41a07/wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81", size = 36462, upload-time = "2025-08-12T05:53:19.623Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ea/0b72f29cb5ebc16eb55c57dc0c98e5de76fc97f435fd407f7d409459c0a6/wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f", size = 38740, upload-time = "2025-08-12T05:53:18.271Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8b/9eae65fb92321e38dbfec7719b87d840a4b92fde83fd1bbf238c5488d055/wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f", size = 36806, upload-time = "2025-08-12T05:52:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "xai-sdk" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "python_full_version >= '3.10'" }, + { name = "googleapis-common-protos", marker = "python_full_version >= '3.10'" }, + { name = "grpcio", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-sdk", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "protobuf", version = "6.33.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/66/1e0163eac090733d0ed0836a0cd3c14f5b59abeaa6fdba71c7b56b1916e4/xai_sdk-1.6.1.tar.gz", hash = "sha256:b55528df188f8c8448484021d735f75b0e7d71719ddeb432c5f187ac67e3c983", size = 388223, upload-time = "2026-01-29T03:13:07.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/98/8b4019b35f2200295c5eec8176da4b779ec3a0fd60eba7196b618f437e1f/xai_sdk-1.6.1-py3-none-any.whl", hash = "sha256:f478dee9bd8839b8d341bd075277d0432aff5cd7120a4284547d25c6c9e7ab3b", size = 240917, upload-time = "2026-01-29T03:13:05.626Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/94/fd/6480106702a79bcceda5fd9c63cb19a04a6506bd5ce7fd8d9b63742f0021/yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", size = 141301, upload-time = "2025-10-06T14:12:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/e1/6d95d21b17a93e793e4ec420a925fe1f6a9342338ca7a563ed21129c0990/yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", size = 93864, upload-time = "2025-10-06T14:12:21.05Z" }, + { url = "https://files.pythonhosted.org/packages/32/58/b8055273c203968e89808413ea4c984988b6649baabf10f4522e67c22d2f/yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", size = 94706, upload-time = "2025-10-06T14:12:23.287Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/d7bfbc28a88c2895ecd0da6a874def0c147de78afc52c773c28e1aa233a3/yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", size = 347100, upload-time = "2025-10-06T14:12:28.527Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e8/37a1e7b99721c0564b1fc7b0a4d1f595ef6fb8060d82ca61775b644185f7/yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", size = 318902, upload-time = "2025-10-06T14:12:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ef/34724449d7ef2db4f22df644f2dac0b8a275d20f585e526937b3ae47b02d/yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", size = 363302, upload-time = "2025-10-06T14:12:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/88a39a5dad39889f192cce8d66cc4c58dbeca983e83f9b6bf23822a7ed91/yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", size = 370816, upload-time = "2025-10-06T14:12:34.01Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1f/5e895e547129413f56c76be2c3ce4b96c797d2d0ff3e16a817d9269b12e6/yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", size = 346465, upload-time = "2025-10-06T14:12:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/11/13/a750e9fd6f9cc9ed3a52a70fe58ffe505322f0efe0d48e1fd9ffe53281f5/yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", size = 341506, upload-time = "2025-10-06T14:12:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/67/bb6024de76e7186611ebe626aec5b71a2d2ecf9453e795f2dbd80614784c/yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", size = 335030, upload-time = "2025-10-06T14:12:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/50b38447fd94a7992996a62b8b463d0579323fcfc08c61bdba949eef8a5d/yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", size = 358560, upload-time = "2025-10-06T14:12:41.547Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/c020b6f547578c4e3dbb6335bf918f26e2f34ad0d1e515d72fd33ac0c635/yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", size = 357290, upload-time = "2025-10-06T14:12:43.861Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/c49a619ee35a402fa3a7019a4fa8d26878fec0d1243f6968bbf516789578/yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", size = 350700, upload-time = "2025-10-06T14:12:46.868Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c9/f5042d87777bf6968435f04a2bbb15466b2f142e6e47fa4f34d1a3f32f0c/yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", size = 82323, upload-time = "2025-10-06T14:12:48.633Z" }, + { url = "https://files.pythonhosted.org/packages/fd/58/d00f7cad9eba20c4eefac2682f34661d1d1b3a942fc0092eb60e78cfb733/yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", size = 87145, upload-time = "2025-10-06T14:12:50.241Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/70904f365080780d38b919edd42d224b8c4ce224a86950d2eaa2a24366ad/yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", size = 82173, upload-time = "2025-10-06T14:12:51.869Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + [[package]] name = "zipp" version = "3.23.0" From 266a3138b5332de819606d339db2392f1595fe68 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Feb 2026 17:18:53 +0000 Subject: [PATCH 235/243] =?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 a55a15abd..689610049 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* ⬆️ Upgrade development dependencies. PR [#14854](https://github.com/fastapi/fastapi/pull/14854) by [@tiangolo](https://github.com/tiangolo). + ## 0.128.3 ### Refactors From 0f5987b560fbf8fac9ed30ed3477f2acc97d9ae9 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 6 Feb 2026 20:23:48 +0300 Subject: [PATCH 236/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20tr=20(outdated=20and=20missing)=20(#14838)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tr/docs/advanced/advanced-dependencies.md | 8 +- docs/tr/docs/alternatives.md | 610 +++++++++--------- docs/tr/docs/deployment/docker.md | 8 +- docs/tr/docs/history-design-future.md | 14 +- docs/tr/docs/translation-banner.md | 11 + docs/tr/docs/tutorial/body-multiple-params.md | 6 +- .../tutorial/path-operation-configuration.md | 8 +- 7 files changed, 337 insertions(+), 328 deletions(-) create mode 100644 docs/tr/docs/translation-banner.md diff --git a/docs/tr/docs/advanced/advanced-dependencies.md b/docs/tr/docs/advanced/advanced-dependencies.md index 48c0b6211..8afb544bd 100644 --- a/docs/tr/docs/advanced/advanced-dependencies.md +++ b/docs/tr/docs/advanced/advanced-dependencies.md @@ -78,13 +78,13 @@ Bu detaylar, özellikle 0.121.0'dan eski bir FastAPI uygulamanız varsa ve `yiel ### `yield` ve `scope` ile dependency'ler { #dependencies-with-yield-and-scope } -0.121.0 sürümünde FastAPI, `yield` kullanan dependency'ler için `Depends(scope="function")` desteğini ekledi. +0.121.0 sürümünde FastAPI, `Depends(scope="function")` desteğini ekledi. `Depends(scope="function")` kullanıldığında, `yield` sonrasındaki çıkış kodu, *path operation function* biter bitmez, response client'a geri gönderilmeden önce çalıştırılır. `Depends(scope="request")` (varsayılan) kullanıldığında ise `yield` sonrasındaki çıkış kodu, response gönderildikten sonra çalıştırılır. -Daha fazlasını [Dependencies with `yield` - Early exit and `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope) dokümantasyonunda okuyabilirsiniz. +Daha fazlasını [`yield` ile Dependency'ler - Erken çıkış ve `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope) dokümantasyonunda okuyabilirsiniz. ### `yield` ve `StreamingResponse` ile dependency'ler, Teknik Detaylar { #dependencies-with-yield-and-streamingresponse-technical-details } @@ -132,7 +132,7 @@ SQLModel (veya SQLAlchemy) kullanarak bu spesifik senaryoya sahipseniz, session' Böylece session veritabanı bağlantısını serbest bırakır ve diğer request'ler bunu kullanabilir. -`yield` kullanan bir dependency'den erken çıkış gerektiren farklı bir kullanım senaryonuz varsa, lütfen kullanım senaryonuzla birlikte ve `yield` kullanan dependency'ler için erken kapatmadan neden fayda göreceğinizi açıklayarak bir GitHub Discussion Question oluşturun. +`yield` kullanan bir dependency'den erken çıkış gerektiren farklı bir kullanım senaryonuz varsa, lütfen kullanım senaryonuzla birlikte ve `yield` kullanan dependency'ler için erken kapatmadan neden fayda göreceğinizi açıklayarak bir GitHub Discussion Sorusu oluşturun. `yield` kullanan dependency'lerde erken kapatma için ikna edici kullanım senaryoları varsa, erken kapatmayı seçmeli (opt-in) hale getiren yeni bir yöntem eklemeyi düşünebilirim. @@ -144,7 +144,7 @@ Bu davranış 0.110.0 sürümünde değiştirildi. Amaç, handler olmayan (inter ### Background Tasks ve `yield` ile dependency'ler, Teknik Detaylar { #background-tasks-and-dependencies-with-yield-technical-details } -FastAPI 0.106.0 öncesinde, `yield` sonrasında exception raise etmek mümkün değildi; çünkü `yield` kullanan dependency'lerdeki çıkış kodu response gönderildikten *sonra* çalıştırılıyordu. Bu nedenle [Exception Handlers](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} zaten çalışmış olurdu. +FastAPI 0.106.0 öncesinde, `yield` sonrasında exception raise etmek mümkün değildi; çünkü `yield` kullanan dependency'lerdeki çıkış kodu response gönderildikten *sonra* çalıştırılıyordu. Bu nedenle [Exception Handler'ları](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} zaten çalışmış olurdu. Bu tasarımın ana sebeplerinden biri, background task'lerin içinde dependency'lerin "yield ettiği" aynı objeleri kullanmaya izin vermekti; çünkü çıkış kodu, background task'ler bittikten sonra çalıştırılıyordu. diff --git a/docs/tr/docs/alternatives.md b/docs/tr/docs/alternatives.md index 9b603ea81..afc1a45ef 100644 --- a/docs/tr/docs/alternatives.md +++ b/docs/tr/docs/alternatives.md @@ -1,483 +1,483 @@ -# Alternatifler, İlham Kaynakları ve Karşılaştırmalar +# Alternatifler, İlham Kaynakları ve Karşılaştırmalar { #alternatives-inspiration-and-comparisons } -**FastAPI**'ya neler ilham verdi? Diğer alternatiflerle karşılaştırıldığında farkları neler? **FastAPI** diğer alternatiflerinden neler öğrendi? +**FastAPI**'a nelerin ilham verdiği, alternatiflerle nasıl karşılaştırıldığı ve onlardan neler öğrendiği. -## Giriş +## Giriş { #intro } Başkalarının daha önceki çalışmaları olmasaydı, **FastAPI** var olmazdı. -Geçmişte oluşturulan pek çok araç **FastAPI**'a ilham kaynağı olmuştur. +Önceden oluşturulan birçok araç, ortaya çıkışına ilham verdi. -Yıllardır yeni bir framework oluşturmaktan kaçınıyordum. Başlangıçta **FastAPI**'ın çözdüğü sorunları çözebilmek için pek çok farklı framework, eklenti ve araç kullanmayı denedim. +Yıllarca yeni bir framework oluşturmaktan kaçındım. Önce **FastAPI**’ın bugün kapsadığı özelliklerin tamamını, birçok farklı framework, eklenti ve araçla çözmeyi denedim. -Ancak bir noktada, geçmişteki diğer araçlardan en iyi fikirleri alarak bütün bu çözümleri kapsayan, ayrıca bütün bunları Python'ın daha önce mevcut olmayan özelliklerini (Python 3.6+ ile gelen tip belirteçleri) kullanarak yapan bir şey üretmekten başka seçenek kalmamıştı. +Ancak bir noktada, geçmişteki araçlardan en iyi fikirleri alıp, mümkün olan en iyi şekilde birleştiren ve daha önce mevcut olmayan dil özelliklerini (Python 3.6+ tip belirteçleri) kullanarak tüm bu özellikleri sağlayan bir şey geliştirmekten başka seçenek kalmadı. -## Daha Önce Geliştirilen Araçlar +## Daha Önce Geliştirilen Araçlar { #previous-tools } -### Django +### Django { #django } -Django geniş çapta güvenilen, Python ekosistemindeki en popüler web framework'üdür. Instagram gibi sistemleri geliştirmede kullanılmıştır. +Python ekosistemindeki en popüler ve yaygın olarak güvenilen web framework’üdür. Instagram gibi sistemleri geliştirmede kullanılmıştır. -MySQL ve PostgreSQL gibi ilişkisel veritabanlarıyla nispeten sıkı bir şekilde bağlantılıdır. Bu nedenle Couchbase, MongoDB ve Cassandra gibi NoSQL veritabanlarını ana veritabanı motoru olarak kullanmak pek de kolay değil. +MySQL veya PostgreSQL gibi ilişkisel veritabanlarıyla nispeten sıkı bağlıdır, bu nedenle Couchbase, MongoDB, Cassandra vb. gibi bir NoSQL veritabanını ana depolama motoru olarak kullanmak pek kolay değildir. -Modern ön uçlarda (React, Vue.js ve Angular gibi) veya diğer sistemler (örneğin nesnelerin interneti cihazları) tarafından kullanılan API'lar yerine arka uçta HTML üretmek için oluşturuldu. +Modern bir ön uç (React, Vue.js, Angular gibi) veya onunla haberleşen diğer sistemler (ör. IoT cihazları) tarafından tüketilen API’lar üretmekten ziyade, arka uçta HTML üretmek için oluşturulmuştur. -### Django REST Framework +### Django REST Framework { #django-rest-framework } -Django REST framework'ü, Django'nun API kabiliyetlerini arttırmak için arka planda Django kullanan esnek bir araç grubu olarak oluşturuldu. +Django REST Framework, Django üzerine kurulu esnek bir araç takımı olarak, Web API’lar geliştirmeyi ve Django’nun API kabiliyetlerini artırmayı hedefler. -Üstelik Mozilla, Red Hat ve Eventbrite gibi pek çok şirket tarafından kullanılıyor. +Mozilla, Red Hat ve Eventbrite gibi birçok şirket tarafından kullanılmaktadır. -**Otomatik API dökümantasyonu**nun ilk örneklerinden biri olduğu için, **FastAPI** arayışına ilham veren ilk fikirlerden biri oldu. +**Otomatik API dökümantasyonu**nun ilk örneklerinden biriydi ve bu, “**FastAPI** arayışına” ilham veren ilk fikirlerden biriydi. /// note | Not -Django REST Framework'ü, aynı zamanda **FastAPI**'ın dayandığı Starlette ve Uvicorn'un da yaratıcısı olan Tom Christie tarafından geliştirildi. +Django REST Framework, **FastAPI**'ın üzerine inşa edildiği Starlette ve Uvicorn'un da yaratıcısı olan Tom Christie tarafından geliştirildi. /// -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -Kullanıcılar için otomatik API dökümantasyonu sunan bir web arayüzüne sahip olmalı. +Otomatik API dökümantasyonu sağlayan bir web arayüzü sunmak. /// -### Flask +### Flask { #flask } -Flask bir mikro framework olduğundan Django gibi framework'lerin aksine veritabanı entegrasyonu gibi Django ile gelen pek çok özelliği direkt barındırmaz. +Flask bir “mikroframework”tür, Django’da varsayılan gelen pek çok özelliği (veritabanı entegrasyonları vb.) içermez. -Sağladığı basitlik ve esneklik NoSQL veritabanlarını ana veritabanı sistemi olarak kullanmak gibi şeyler yapmaya olanak sağlar. +Bu basitlik ve esneklik, NoSQL veritabanlarını ana veri depolama sistemi olarak kullanmak gibi şeyleri mümkün kılar. -Yapısı oldukça basit olduğundan öğrenmesi de nispeten basittir, tabii dökümantasyonu bazı noktalarda biraz teknik hale geliyor. +Çok basit olduğu için öğrenmesi nispeten sezgiseldir, ancak dökümantasyon bazı noktalarda biraz teknikleşebilir. -Ayrıca Django ile birlikte gelen veritabanı, kullanıcı yönetimi ve diğer pek çok özelliğe ihtiyaç duymayan uygulamalarda da yaygın olarak kullanılıyor. Ancak bu tür özelliklerin pek çoğu eklentiler ile eklenebiliyor. +Ayrıca veritabanı, kullanıcı yönetimi veya Django’da önceden gelen pek çok özelliğe ihtiyaç duymayan uygulamalar için de yaygın olarak kullanılır. Yine de bu özelliklerin çoğu eklentilerle eklenebilir. -Uygulama parçalarının böyle ayrılıyor oluşu ve istenilen özelliklerle genişletilebilecek bir mikro framework olmak tam da benim istediğim bir özellikti. +Bileşenlerin ayrık olması ve gerekeni tam olarak kapsayacak şekilde genişletilebilen bir “mikroframework” olması, özellikle korumak istediğim bir özelliktir. -Flask'ın basitliği göz önünde bulundurulduğu zaman, API geliştirmek için iyi bir seçim gibi görünüyordu. Sıradaki şey ise Flask için bir "Django REST Framework"! +Flask’ın sadeliği göz önüne alındığında, API geliştirmek için iyi bir aday gibi görünüyordu. Sırada, Flask için bir “Django REST Framework” bulmak vardı. -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -Gereken araçları ve parçaları birleştirip eşleştirmeyi kolaylaştıracak bir mikro framework olmalı. +Gereken araç ve parçaları kolayca eşleştirip birleştirmeyi sağlayan bir mikroframework olmak. -Basit ve kullanması kolay bir yönlendirme sistemine sahip olmalı. +Basit ve kullanımı kolay bir yönlendirme (routing) sistemine sahip olmak. /// -### Requests +### Requests { #requests } -**FastAPI** aslında **Requests**'in bir alternatifi değil. İkisininde kapsamı oldukça farklı. +**FastAPI** aslında **Requests**’in bir alternatifi değildir. Kapsamları çok farklıdır. -Aslında Requests'i bir FastAPI uygulamasının *içinde* kullanmak daha olağan olurdu. +Hatta bir FastAPI uygulamasının içinde Requests kullanmak yaygındır. -Ama yine de, FastAPI, Requests'ten oldukça ilham aldı. +Yine de FastAPI, Requests’ten epey ilham almıştır. -**Requests**, API'lar ile bir istemci olarak *etkileşime geçmeyi* sağlayan bir kütüphaneyken **FastAPI** bir sunucu olarak API'lar oluşturmaya yarar. +**Requests** bir kütüphane olarak API’larla (istemci olarak) etkileşime geçmeye yararken, **FastAPI** API’lar (sunucu olarak) geliştirmeye yarar. -Öyle ya da böyle zıt uçlarda olmalarına rağmen birbirlerini tamamlıyorlar. +Yani daha çok zıt uçlardadırlar ama birbirlerini tamamlarlar. -Requests oldukça basit ve sezgisel bir tasarıma sahip, kullanması da mantıklı varsayılan değerlerle oldukça kolay. Ama aynı zamanda çok güçlü ve gayet özelleştirilebilir. +Requests çok basit ve sezgisel bir tasarıma sahiptir, mantıklı varsayılanlarla kullanımı çok kolaydır. Aynı zamanda çok güçlü ve özelleştirilebilirdir. -Bu yüzden resmi web sitede de söylendiği gibi: +Bu yüzden resmi web sitesinde de söylendiği gibi: -> Requests, tüm zamanların en çok indirilen Python paketlerinden biridir. +> Requests, tüm zamanların en çok indirilen Python paketlerinden biridir -Kullanım şekli oldukça basit. Örneğin bir `GET` isteği yapmak için aşağıdaki yeterli: +Kullanımı çok basittir. Örneğin bir `GET` isteği yapmak için: ```Python response = requests.get("http://example.com/some/url") ``` -Bunun FastAPI'deki API *yol işlemi* şöyle görünür: +Buna karşılık bir FastAPI API *path operation*’ı şöyle olabilir: ```Python hl_lines="1" @app.get("/some/url") def read_url(): - return {"message": "Hello World!"} + return {"message": "Hello World"} ``` `requests.get(...)` ile `@app.get(...)` arasındaki benzerliklere bakın. -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -* Basit ve sezgisel bir API'ya sahip olmalı. -* HTTP metot isimlerini (işlemlerini) anlaşılır olacak bir şekilde, direkt kullanmalı. -* Mantıklı varsayılan değerlere ve buna rağmen güçlü bir özelleştirme desteğine sahip olmalı. +* Basit ve sezgisel bir API’ya sahip olmak. +* HTTP metot isimlerini (işlemlerini) doğrudan, anlaşılır ve sezgisel bir şekilde kullanmak. +* Mantıklı varsayılanlara sahip olmak ama güçlü özelleştirmeler de sunmak. /// -### Swagger / OpenAPI +### Swagger / OpenAPI { #swagger-openapi } -Benim Django REST Framework'ünden istediğim ana özellik otomatik API dökümantasyonuydu. +Django REST Framework’ünden istediğim ana özellik otomatik API dökümantasyonuydu. -Daha sonra API'ları dökümanlamak için Swagger adında JSON (veya JSON'un bir uzantısı olan YAML'ı) kullanan bir standart olduğunu buldum. +Sonra API’ları JSON (veya JSON’un bir uzantısı olan YAML) kullanarak dökümante etmek için Swagger adlı bir standart olduğunu gördüm. -Üstelik Swagger API'ları için zaten halihazırda oluşturulmuş bir web arayüzü vardı. Yani, bir API için Swagger dökümantasyonu oluşturmak bu arayüzü otomatik olarak kullanabilmek demekti. +Ve Swagger API’ları için zaten oluşturulmuş bir web arayüzü vardı. Yani bir API için Swagger dökümantasyonu üretebilmek, bu web arayüzünü otomatik kullanabilmek demekti. -Swagger bir noktada Linux Foundation'a verildi ve adı OpenAPI olarak değiştirildi. +Bir noktada Swagger, Linux Foundation’a devredildi ve OpenAPI olarak yeniden adlandırıldı. -İşte bu yüzden versiyon 2.0 hakkında konuşurken "Swagger", versiyon 3 ve üzeri için ise "OpenAPI" adını kullanmak daha yaygın. +Bu yüzden, 2.0 sürümü söz konusu olduğunda “Swagger”, 3+ sürümler için ise “OpenAPI” denmesi yaygındır. -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -API spesifikasyonları için özel bir şema yerine bir açık standart benimseyip kullanmalı. +API spesifikasyonları için özel bir şema yerine açık bir standart benimsemek ve kullanmak. -Ayrıca standarda bağlı kullanıcı arayüzü araçlarını entegre etmeli: +Ve standartlara dayalı kullanıcı arayüzü araçlarını entegre etmek: * Swagger UI * ReDoc -Yukarıdaki ikisi oldukça popüler ve istikrarlı olduğu için seçildi, ancak hızlı bir araştırma yaparak **FastAPI** ile kullanabileceğiniz pek çok OpenAPI alternatifi arayüz bulabilirsiniz. +Bu ikisi oldukça popüler ve istikrarlı oldukları için seçildi; hızlı bir aramayla OpenAPI için onlarca alternatif kullanıcı arayüzü bulabilirsiniz (**FastAPI** ile de kullanabilirsiniz). /// -### Flask REST framework'leri +### Flask REST framework’leri { #flask-rest-frameworks } -Pek çok Flask REST framework'ü var, fakat bunları biraz araştırdıktan sonra pek çoğunun artık geliştirilmediğini ve göze batan bazı sorunlarının olduğunu gördüm. +Birçok Flask REST framework’ü var; ancak zaman ayırıp inceledikten sonra çoğunun artık sürdürülmediğini veya bazı kritik sorunlar nedeniyle uygun olmadıklarını gördüm. -### Marshmallow +### Marshmallow { #marshmallow } -API sistemlerine gereken ana özelliklerden biri de koddan veriyi alıp ağ üzerinde gönderilebilecek bir şeye çevirmek, yani veri dönüşümü. Bu işleme veritabanındaki veriyi içeren bir objeyi JSON objesine çevirmek, `datetime` objelerini metinlere çevirmek gibi örnekler verilebilir. +API sistemlerinin ihtiyaç duyduğu temel özelliklerden biri, koddan (Python) veriyi alıp ağ üzerinden gönderilebilecek bir şeye dönüştürmek, yani veri “dönüşüm”üdür. Örneğin, bir veritabanından gelen verileri içeren bir objeyi JSON objesine dönüştürmek, `datetime` objelerini string’e çevirmek vb. -API'lara gereken bir diğer büyük özellik ise veri doğrulamadır, yani verinin çeşitli parametrelere bağlı olarak doğru ve tutarlı olduğundan emin olmaktır. Örneğin bir alanın `int` olmasına karar verdiniz, daha sonra rastgele bir metin değeri almasını istemezsiniz. Bu özellikle sisteme dışarıdan gelen veri için kullanışlı bir özellik oluyor. +API’ların ihtiyaç duyduğu bir diğer önemli özellik, veri doğrulamadır; belirli parametreler göz önüne alındığında verinin geçerli olduğundan emin olmak. Örneğin, bir alanın `int` olması ve rastgele bir metin olmaması. Bu özellikle dışarıdan gelen veriler için kullanışlıdır. -Bir veri doğrulama sistemi olmazsa bütün bu kontrolleri koda dökerek kendiniz yapmak zorunda kalırdınız. +Bir veri doğrulama sistemi olmadan, tüm bu kontrolleri kod içinde el ile yapmanız gerekir. -Marshmallow bu özellikleri sağlamak için geliştirilmişti. Benim de geçmişte oldukça sık kullandığım harika bir kütüphanedir. +Marshmallow, bu özellikleri sağlamak için inşa edildi. Harika bir kütüphanedir ve geçmişte çok kullandım. -Ama... Python'un tip belirteçleri gelmeden önce oluşturulmuştu. Yani her şemayı tanımlamak için Marshmallow'un sunduğu spesifik araçları ve sınıfları kullanmanız gerekiyordu. +Ancak Python tip belirteçlerinden önce yazılmıştır. Dolayısıyla her şemayı tanımlamak için Marshmallow’un sağladığı belirli yardımcılar ve sınıflar kullanılır. -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -Kod kullanarak otomatik olarak veri tipini ve veri doğrulamayı belirten "şemalar" tanımlamalı. +Kodla, veri tiplerini ve doğrulamayı otomatik sağlayan “şemalar” tanımlamak. /// -### Webargs +### Webargs { #webargs } -API'ların ihtiyacı olan bir diğer önemli özellik ise gelen isteklerdeki verileri Python objelerine ayrıştırabilmektir. +API’ların ihtiyaç duyduğu bir diğer büyük özellik, gelen isteklerden veriyi ayrıştırmadır. -Webargs, Flask gibi bir kaç framework'ün üzerinde bunu sağlamak için geliştirilen bir araçtır. +Webargs, Flask dahil birkaç framework’ün üzerinde bunu sağlamak için geliştirilmiş bir araçtır. -Veri doğrulama için arka planda Marshmallow kullanıyor, hatta aynı geliştiriciler tarafından oluşturuldu. +Veri doğrulama için arka planda Marshmallow’u kullanır. Aynı geliştiriciler tarafından yazılmıştır. -Webargs da harika bir araç ve onu da geçmişte henüz **FastAPI** yokken çok kullandım. +**FastAPI**’dan önce benim de çok kullandığım harika bir araçtır. /// info | Bilgi -Webargs aynı Marshmallow geliştirileri tarafından oluşturuldu. +Webargs, Marshmallow geliştiricileri tarafından oluşturuldu. /// -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -Gelen istek verisi için otomatik veri doğrulamaya sahip olmalı. +Gelen istek verisini otomatik doğrulamak. /// -### APISpec +### APISpec { #apispec } -Marshmallow ve Webargs eklentiler olarak; veri doğrulama, ayrıştırma ve dönüştürmeyi sağlıyor. +Marshmallow ve Webargs; doğrulama, ayrıştırma ve dönüşümü eklenti olarak sağlar. -Ancak dökümantasyondan hala ses seda yok. Daha sonrasında ise APISpec geldi. +Ama dökümantasyon eksikti. Sonra APISpec geliştirildi. -APISpec pek çok framework için bir eklenti olarak kullanılıyor (Starlette için de bir eklentisi var). +Birçok framework için bir eklentidir (Starlette için de bir eklenti vardır). -Şemanın tanımını yol'a istek geldiğinde çalışan her bir fonksiyonun döküman dizesinin içine YAML formatında olacak şekilde yazıyorsunuz o da OpenAPI şemaları üretiyor. +Çalışma şekli: Her bir route’u işleyen fonksiyonun docstring’i içine YAML formatında şema tanımı yazarsınız. -Flask, Starlette, Responder ve benzerlerinde bu şekilde çalışıyor. +Ve OpenAPI şemaları üretir. -Fakat sonrasında yine mikro sözdizimi problemiyle karşılaşıyoruz. Python metinlerinin içinde koskoca bir YAML oluyor. +Flask, Starlette, Responder vb. için çalışma şekli böyledir. -Editör bu konuda pek yardımcı olamaz. Üstelik eğer parametreleri ya da Marshmallow şemalarını değiştirip YAML kodunu güncellemeyi unutursak artık döküman geçerliliğini yitiriyor. +Ancak yine, Python metni içinde (kocaman bir YAML) mikro bir söz dizimi sorunu ortaya çıkar. + +Editör bu konuda pek yardımcı olamaz. Parametreleri veya Marshmallow şemalarını değiştirip docstring’teki YAML’ı güncellemeyi unutursak, üretilen şema geçerliliğini yitirir. /// info | Bilgi -APISpec de aynı Marshmallow geliştiricileri tarafından oluşturuldu. +APISpec, Marshmallow geliştiricileri tarafından oluşturuldu. /// -/// check | **FastAPI**'a nasıl ilham verdi? +/// check | **FastAPI**'a ilham olan -API'lar için açık standart desteği olmalı (OpenAPI gibi). +API’lar için açık standart olan OpenAPI’ı desteklemek. /// -### Flask-apispec +### Flask-apispec { #flask-apispec } -Flask-apispec ise Webargs, Marshmallow ve APISpec'i birbirine bağlayan bir Flask eklentisi. +Webargs, Marshmallow ve APISpec’i bir araya getiren bir Flask eklentisidir. -Webargs ve Marshmallow'daki bilgiyi APISpec ile otomatik OpenAPI şemaları üretmek için kullanıyor. +Webargs ve Marshmallow’dan aldığı bilgiyi kullanarak, APISpec ile otomatik OpenAPI şemaları üretir. -Hak ettiği değeri görmeyen, harika bir araç. Piyasadaki çoğu Flask eklentisinden çok daha popüler olmalı. Hak ettiği değeri görmüyor oluşunun sebebi ise dökümantasyonun çok kısa ve soyut olması olabilir. +Harika ama yeterince değer görmeyen bir araçtır. Mevcut birçok Flask eklentisinden çok daha popüler olmalıydı. Muhtemelen dökümantasyonunun fazla kısa ve soyut olmasından kaynaklanıyor olabilir. -Böylece Flask-apispec, Python döküman dizilerine YAML gibi farklı bir syntax yazma sorununu çözmüş oldu. +Python docstring’leri içine YAML (farklı bir söz dizimi) yazma ihtiyacını ortadan kaldırdı. -**FastAPI**'ı geliştirene dek benim favori arka uç kombinasyonum Flask'in yanında Marshmallow ve Webargs ile birlikte Flask-apispec idi. +**FastAPI**’yı inşa edene kadar, Flask + Flask-apispec + Marshmallow + Webargs kombinasyonu benim favori arka uç stack’imdi. -Bunu kullanmak, bir kaç full-stack Flask projesi oluşturucusunun yaratılmasına yol açtı. Bunlar benim (ve bir kaç harici ekibin de) şimdiye kadar kullandığı asıl stackler: +Bunu kullanmak, birkaç Flask full‑stack üreticisinin ortaya çıkmasına yol açtı. Şu ana kadar benim (ve birkaç harici ekibin) kullandığı ana stack’ler: * https://github.com/tiangolo/full-stack * https://github.com/tiangolo/full-stack-flask-couchbase * https://github.com/tiangolo/full-stack-flask-couchdb -Aynı full-stack üreticiler [**FastAPI** Proje Üreticileri](project-generation.md){.internal-link target=_blank}'nin de temelini oluşturdu. +Aynı full‑stack üreticiler, [**FastAPI** Proje Üreticileri](project-generation.md){.internal-link target=_blank}’nin de temelini oluşturdu. /// info | Bilgi -Flask-apispec de aynı Marshmallow geliştiricileri tarafından üretildi. +Flask-apispec, Marshmallow geliştiricileri tarafından oluşturuldu. /// -/// check | **FastAPI**'a nasıl ilham oldu? +/// check | **FastAPI**'a ilham olan -Veri dönüşümü ve veri doğrulamayı tanımlayan kodu kullanarak otomatik olarak OpenAPI şeması oluşturmalı. +Veri dönüşümü ve doğrulamayı tanımlayan aynı koddan, OpenAPI şemasını otomatik üretmek. /// -### NestJS (and Angular) +### NestJS (ve Angular) { #nestjs-and-angular } -Bu Python teknolojisi bile değil. NestJS, Angulardan ilham almış olan bir JavaScript (TypeScript) NodeJS framework'ü. +Bu Python bile değil; NestJS, Angular’dan ilham alan bir JavaScript (TypeScript) NodeJS framework’üdür. -Flask-apispec ile yapılabileceklere nispeten benzeyen bir şey elde ediyor. +Flask-apispec ile yapılabilene kısmen benzer bir şey başarır. -Angular 2'den ilham alan, içine gömülü bir bağımlılık enjeksiyonu sistemi var. Bildiğim diğer tüm bağımlılık enjeksiyonu sistemlerinde olduğu gibi"bağımlılık"ları önceden kaydetmenizi gerektiriyor. Böylece projeyi daha detaylı hale getiriyor ve kod tekrarını da arttırıyor. +Angular 2’den esinlenen, entegre bir bağımlılık enjeksiyonu sistemi vardır. “Injectable”ları önceden kaydetmeyi gerektirir (bildiğim diğer bağımlılık enjeksiyonu sistemlerinde olduğu gibi), bu da ayrıntıyı ve kod tekrarını artırır. -Parametreler TypeScript tipleri (Python tip belirteçlerine benzer) ile açıklandığından editör desteği oldukça iyi. +Parametreler TypeScript tipleriyle (Python tip belirteçlerine benzer) açıklandığından, editör desteği oldukça iyidir. -Ama TypeScript verileri kod JavaScript'e derlendikten sonra korunmadığından, bunlara dayanarak aynı anda veri doğrulaması, veri dönüşümü ve dökümantasyon tanımlanamıyor. Bundan ve bazı tasarım tercihlerinden dolayı veri doğrulaması, dönüşümü ve otomatik şema üretimi için pek çok yere dekorator eklemek gerekiyor. Bu da projeyi oldukça detaylandırıyor. +Ancak TypeScript tip bilgisi JavaScript’e derlemeden sonra korunmadığından, aynı anda tiplere dayanarak doğrulama, dönüşüm ve dökümantasyon tanımlanamaz. Bu ve bazı tasarım kararları nedeniyle doğrulama, dönüşüm ve otomatik şema üretimi için birçok yere dekoratör eklemek gerekir; proje oldukça ayrıntılı hâle gelir. -İç içe geçen derin modelleri pek iyi işleyemiyor. Yani eğer istekteki JSON gövdesi derin bir JSON objesiyse düzgün bir şekilde dökümante edilip doğrulanamıyor. +İçiçe modelleri çok iyi işleyemez. Yani istek gövdesindeki JSON, içinde başka alanları ve onlar da içiçe JSON objelerini içeriyorsa, doğru şekilde dökümante edilip doğrulanamaz. -/// check | **FastAPI**'a nasıl ilham oldu? +/// check | **FastAPI**'a ilham olan -Güzel bir editör desteği için Python tiplerini kullanmalı. +Harika editör desteği için Python tiplerini kullanmak. -Güçlü bir bağımlılık enjeksiyon sistemine sahip olmalı. Kod tekrarını minimuma indirecek bir yol bulmalı. +Güçlü bir bağımlılık enjeksiyonu sistemine sahip olmak. Kod tekrarını en aza indirmenin bir yolunu bulmak. /// -### Sanic +### Sanic { #sanic } -Sanic, `asyncio`'ya dayanan son derece hızlı Python kütüphanelerinden biriydi. Flask'a epey benzeyecek şekilde geliştirilmişti. - -/// note | Teknik detaylar - -İçerisinde standart Python `asyncio` döngüsü yerine `uvloop` kullanıldı. Hızının asıl kaynağı buydu. - -Uvicorn ve Starlette'e ilham kaynağı olduğu oldukça açık, şu anda ikisi de açık karşılaştırmalarda Sanicten daha hızlı gözüküyor. - -/// - -/// check | **FastAPI**'a nasıl ilham oldu? - -Uçuk performans sağlayacak bir yol bulmalı. - -Tam da bu yüzden **FastAPI** Starlette'e dayanıyor, çünkü Starlette şu anda kullanılabilir en hızlı framework. (üçüncü parti karşılaştırmalı testlerine göre) - -/// - -### Falcon - -Falcon ise bir diğer yüksek performanslı Python framework'ü. Minimal olacak şekilde Hug gibi diğer framework'lerin temeli olabilmek için tasarlandı. - -İki parametre kabul eden fonksiyonlar şeklinde tasarlandı, biri "istek" ve diğeri ise "cevap". Sonra isteğin çeşitli kısımlarını **okuyor**, cevaba ise bir şeyler **yazıyorsunuz**. Bu tasarımdan dolayı istek parametrelerini ve gövdelerini standart Python tip belirteçlerini kullanarak fonksiyon parametreleriyle belirtmek mümkün değil. - -Yani veri doğrulama, veri dönüştürme ve dökümantasyonun hepsi kodda yer almalı, otomatik halledemiyoruz. Ya da Falcon üzerine bir framework olarak uygulanmaları gerekiyor, aynı Hug'da olduğu gibi. Bu ayrım Falcon'un tasarımından esinlenen, istek ve cevap objelerini parametre olarak işleyen diğer kütüphanelerde de yer alıyor. - -/// check | **FastAPI**'a nasıl ilham oldu? - -Harika bir performans'a sahip olmanın yollarını bulmalı. - -Hug ile birlikte (Hug zaten Falcon'a dayandığından) **FastAPI**'ın fonksiyonlarda `cevap` parametresi belirtmesinde ilham kaynağı oldu. - -FastAPI'da opsiyonel olmasına rağmen, daha çok header'lar, çerezler ve alternatif durum kodları belirlemede kullanılıyor. - -/// - -### Molten - -**FastAPI**'ı geliştirmenin ilk aşamalarında Molten'ı keşfettim. Pek çok ortak fikrimiz vardı: - -* Python'daki tip belirteçlerini baz alıyordu. -* Bunlara bağlı olarak veri doğrulaması ve dökümantasyon sağlıyordu. -* Bir bağımlılık enjeksiyonu sistemi vardı. - -Veri doğrulama, veri dönüştürme ve dökümantasyon için Pydantic gibi bir üçüncü parti kütüphane kullanmıyor, kendi içerisinde bunlara sahip. Yani bu veri tipi tanımlarını tekrar kullanmak pek de kolay değil. - -Biraz daha detaylı ayarlamalara gerek duyuyor. Ayrıca ASGI yerine WSGI'a dayanıyor. Yani Uvicorn, Starlette ve Sanic gibi araçların yüksek performansından faydalanacak şekilde tasarlanmamış. - -Bağımlılık enjeksiyonu sistemi bağımlılıkların önceden kaydedilmesini ve sonrasında belirlenen veri tiplerine göre çözülmesini gerektiriyor. Yani spesifik bir tip, birden fazla bileşen ile belirlenemiyor. - -Yol'lar fonksiyonun üstünde endpoint'i işleyen dekoratörler yerine farklı yerlerde tanımlanan fonksiyonlarla belirlenir. Bu Flask (ve Starlette) yerine daha çok Django'nun yaklaşımına daha yakın bir metot. Bu, kodda nispeten birbiriyle sıkı ilişkili olan şeyleri ayırmaya sebep oluyor. - -/// check | **FastAPI**'a nasıl ilham oldu? - -Model özelliklerinin "standart" değerlerini kullanarak veri tipleri için ekstra veri doğrulama koşulları tanımlamalı. Bu editör desteğini geliştiriyor ve daha önceden Pydantic'te yoktu. - -Bu aslında Pydantic'in de aynı doğrulama stiline geçmesinde ilham kaynağı oldu. Şu anda bütün bu özellikler Pydantic'in yapısında yer alıyor. - -/// - -### Hug - -Hug, Python tip belirteçlerini kullanarak API parametrelerinin tipini belirlemeyi uygulayan ilk framework'lerdendi. Bu, diğer araçlara da ilham kaynağı olan harika bir fikirdi. - -Tip belirlerken standart Python veri tipleri yerine kendi özel tiplerini kullandı, yine de bu ileriye dönük devasa bir adımdı. - -Hug ayrıca tüm API'ı JSON ile ifade eden özel bir şema oluşturan ilk framework'lerdendir. - -OpenAPI veya JSON Şeması gibi bir standarda bağlı değildi. Yani Swagger UI gibi diğer araçlarla entegre etmek kolay olmazdı. Ama yine de, bu oldukça yenilikçi bir fikirdi. - -Ayrıca ilginç ve çok rastlanmayan bir özelliği vardı: aynı framework'ü kullanarak hem API'lar hem de CLI'lar oluşturmak mümkündü. - -Senkron çalışan Python web framework'lerinin standardına (WSGI) dayandığından dolayı Websocket'leri ve diğer şeyleri işleyemiyor, ancak yine de yüksek performansa sahip. - -/// info | Bilgi - -Hug, Python dosyalarında bulunan dahil etme satırlarını otomatik olarak sıralayan harika bir araç olan `isort`'un geliştiricisi Timothy Crosley tarafından geliştirildi. - -/// - -/// check | **FastAPI**'a nasıl ilham oldu? - -Hug, APIStar'ın çeşitli kısımlarında esin kaynağı oldu ve APIStar'la birlikte en umut verici bulduğum araçlardan biriydi. - -**FastAPI**, Python tip belirteçlerini kullanarak parametre belirlemede ve API'ı otomatık tanımlayan bir şema üretmede de Hug'a esinlendi. - -**FastAPI**'ın header ve çerez tanımlamak için fonksiyonlarda `response` parametresini belirtmesinde de Hug'dan ilham alındı. - -/// - -### APIStar (<= 0.5) - -**FastAPI**'ı geliştirmeye başlamadan hemen önce **APIStar** sunucusunu buldum. Benim aradığım şeylerin neredeyse hepsine sahipti ve harika bir tasarımı vardı. - -Benim şimdiye kadar gördüğüm Python tip belirteçlerini kullanarak parametre ve istekler belirlemeyi uygulayan ilk framework'lerdendi (Molten ve NestJS'den önce). APIStar'ı da aşağı yukarı Hug ile aynı zamanlarda buldum. Fakat APIStar OpenAPI standardını kullanıyordu. - -Farklı yerlerdeki tip belirteçlerine bağlı olarak otomatik veri doğrulama, veri dönüştürme ve OpenAPI şeması oluşturma desteği sunuyordu. - -Gövde şema tanımları Pydantic ile aynı Python tip belirteçlerini kullanmıyordu, biraz daha Marsmallow'a benziyordu. Dolayısıyla editör desteği de o kadar iyi olmazdı ama APIStar eldeki en iyi seçenekti. - -O dönemlerde karşılaştırmalarda en iyi performansa sahipti (yalnızca Starlette'e kaybediyordu). - -Başlangıçta otomatik API dökümantasyonu sunan bir web arayüzü yoktu, ama ben ona Swagger UI ekleyebileceğimi biliyordum. - -Bağımlılık enjeksiyon sistemi vardı. Yukarıda bahsettiğim diğer araçlar gibi bundaki sistem de bileşenlerin önceden kaydedilmesini gerektiriyordu. Yine de harika bir özellikti. - -Güvenlik entegrasyonu olmadığından dolayı APIStar'ı hiç bir zaman tam bir projede kullanamadım. Bu yüzden Flask-apispec'e bağlı full-stack proje üreticilerde sahip olduğum özellikleri tamamen değiştiremedim. Bu güvenlik entegrasyonunu ekleyen bir PR oluşturmak da projelerim arasında yer alıyordu. - -Sonrasında ise projenin odağı değişti. - -Geliştiricinin Starlette'e odaklanması gerekince proje de artık bir API web framework'ü olmayı bıraktı. - -Artık APIStar, OpenAPI özelliklerini doğrulamak için bir dizi araç sunan bir proje haline geldi. - -/// info | Bilgi - -APIStar, aşağıdaki projeleri de üreten Tom Christie tarafından geliştirildi: - -* Django REST Framework -* **FastAPI**'ın da dayandığı Starlette -* Starlette ve **FastAPI** tarafından da kullanılan Uvicorn - -/// - -/// check | **FastAPI**'a nasıl ilham oldu? - -Var oldu. - -Aynı Python veri tipleriyle birden fazla şeyi belirleme (veri doğrulama, veri dönüştürme ve dökümantasyon), bir yandan da harika bir editör desteği sunma, benim muhteşem bulduğum bir fikirdi. - -Uzunca bir süre boyunca benzer bir framework arayıp pek çok farklı alternatifi denedikten sonra, APIStar en iyi seçenekti. - -Sonra APIStar bir sunucu olmayı bıraktı ve Starlette oluşturuldu. Starlette, böyle bir sunucu sistemi için daha iyi bir temel sunuyordu. Bu da **FastAPI**'ın son esin kaynağıydı. - -Ben bu önceki araçlardan öğrendiklerime dayanarak **FastAPI**'ın özelliklerini arttırıp geliştiriyor, tip desteği sistemi ve diğer kısımları iyileştiriyorum ancak yine de **FastAPI**'ı APIStar'ın "ruhani varisi" olarak görüyorum. - -/// - -## **FastAPI** Tarafından Kullanılanlar - -### Pydantic - -Pydantic Python tip belirteçlerine dayanan; veri doğrulama, veri dönüştürme ve dökümantasyon tanımlamak (JSON Şema kullanarak) için bir kütüphanedir. - -Tip belirteçleri kullanıyor olması onu aşırı sezgisel yapıyor. - -Marshmallow ile karşılaştırılabilir. Ancak karşılaştırmalarda Marshmallowdan daha hızlı görünüyor. Aynı Python tip belirteçlerine dayanıyor ve editör desteği de harika. - -/// check | **FastAPI** nerede kullanıyor? - -Bütün veri doğrulama, veri dönüştürme ve JSON Şemasına bağlı otomatik model dökümantasyonunu halletmek için! - -**FastAPI** yaptığı her şeyin yanı sıra bu JSON Şema verisini alıp daha sonra OpenAPI'ya yerleştiriyor. - -/// - -### Starlette - -Starlette hafif bir ASGI framework'ü ve yüksek performanslı asyncio servisleri oluşturmak için ideal. - -Kullanımı çok kolay ve sezgisel, kolaylıkla genişletilebilecek ve modüler bileşenlere sahip olacak şekilde tasarlandı. - -Sahip olduğu bir kaç özellik: - -* Cidden etkileyici bir performans. -* WebSocket desteği. -* İşlem-içi arka plan görevleri. -* Başlatma ve kapatma olayları. -* HTTPX ile geliştirilmiş bir test istemcisi. -* CORS, GZip, Static Files ve Streaming cevapları desteği. -* Session ve çerez desteği. -* Kodun %100'ü test kapsamında. -* Kodun %100'ü tip belirteçleriyle desteklenmiştir. -* Yalnızca bir kaç zorunlu bağımlılığa sahip. - -Starlette şu anda test edilen en hızlı Python framework'ü. Yalnızca bir sunucu olan Uvicorn'a yeniliyor, o da zaten bir framework değil. - -Starlette bütün temel web mikro framework işlevselliğini sağlıyor. - -Ancak otomatik veri doğrulama, veri dönüştürme ve dökümantasyon sağlamyor. - -Bu, **FastAPI**'ın onun üzerine tamamen Python tip belirteçlerine bağlı olarak eklediği (Pydantic ile) ana şeylerden biri. **FastAPI** bunun yanında artı olarak bağımlılık enjeksiyonu sistemi, güvenlik araçları, OpenAPI şema üretimi ve benzeri özellikler de ekliyor. +`asyncio` tabanlı, son derece hızlı ilk Python framework’lerinden biriydi. Flask’a oldukça benzer olacak şekilde geliştirilmişti. /// note | Teknik Detaylar -ASGI, Django'nun ana ekibi tarafından geliştirilen yeni bir "standart". Bir "Python standardı" (PEP) olma sürecinde fakat henüz bir standart değil. +Varsayılan Python `asyncio` döngüsü yerine `uvloop` kullanır; hızını esasen bu sağlar. -Bununla birlikte, halihazırda birçok araç tarafından bir "standart" olarak kullanılmakta. Bu, Uvicorn'u farklı ASGI sunucularıyla (Daphne veya Hypercorn gibi) değiştirebileceğiniz veya `python-socketio` gibi ASGI ile uyumlu araçları ekleyebileciğiniz için birlikte çalışılabilirliği büyük ölçüde arttırıyor. +Açık kıyaslamalarda, bugün Uvicorn ve Starlette’in Sanic’ten daha hızlı olduğu görülür; Sanic bu ikisine ilham vermiştir. /// -/// check | **FastAPI** nerede kullanıyor? +/// check | **FastAPI**'a ilham olan -Tüm temel web kısımlarında üzerine özellikler eklenerek kullanılmakta. +Çok yüksek performans elde etmenin bir yolunu bulmak. -`FastAPI` sınıfının kendisi direkt olarak `Starlette` sınıfını miras alıyor! - -Yani, Starlette ile yapabileceğiniz her şeyi, Starlette'in bir nevi güçlendirilmiş hali olan **FastAPI** ile doğrudan yapabilirsiniz. +Bu yüzden **FastAPI**, en hızlı framework olduğu için (üçüncü parti kıyaslamalara göre) Starlette üzerine kuruludur. /// -### Uvicorn +### Falcon { #falcon } -Uvicorn, uvlook ile httptools üzerine kurulu ışık hzında bir ASGI sunucusudur. +Falcon, başka bir yüksek performanslı Python framework’üdür; minimal olacak şekilde tasarlanmış ve Hug gibi diğer framework’lere temel olmuştur. -Bir web framework'ünden ziyade bir sunucudur, yani yollara bağlı yönlendirme yapmanızı sağlayan araçları yoktur. Bu daha çok Starlette (ya da **FastAPI**) gibi bir framework'ün sunucuya ek olarak sağladığı bir şeydir. +İki parametre alan fonksiyonlar etrafında tasarlanmıştır: “request” ve “response”. İstekten parçalar “okur”, cevaba parçalar “yazarsınız”. Bu tasarım nedeniyle, fonksiyon parametreleriyle standart Python tip belirteçlerini kullanarak istek parametrelerini ve gövdelerini ilan etmek mümkün değildir. -Starlette ve **FastAPI** için tavsiye edilen sunucu Uvicorndur. +Dolayısıyla veri doğrulama, dönüşüm ve dökümantasyon kodda yapılmalı; otomatik olmaz. Ya da Hug’da olduğu gibi Falcon’un üzerine bir framework olarak uygulanmalıdır. Falcon’un tasarımından etkilenen ve tek bir request objesi ile response objesini parametre olarak alan diğer framework’lerde de aynı ayrım vardır. -/// check | **FastAPI** neden tavsiye ediyor? +/// check | **FastAPI**'a ilham olan -**FastAPI** uygulamalarını çalıştırmak için ana web sunucusu Uvicorn! +Harika performans elde etmenin yollarını bulmak. -Gunicorn ile birleştirdiğinizde asenkron ve çoklu işlem destekleyen bir sunucu elde ediyorsunuz! - -Daha fazla detay için [Deployment](deployment/index.md){.internal-link target=_blank} bölümünü inceleyebilirsiniz. +Hug ile birlikte (Hug, Falcon’a dayanır) **FastAPI**’da fonksiyonlarda opsiyonel bir `response` parametresi ilan edilmesi fikrine ilham vermek. FastAPI’da bu parametre çoğunlukla header, cookie ve alternatif durum kodlarını ayarlamak için kullanılır. /// -## Karşılaştırma ve Hız +### Molten { #molten } -Uvicorn, Starlette ve FastAPI arasındakı farkı daha iyi anlamak ve karşılaştırma yapmak için [Kıyaslamalar](benchmarks.md){.internal-link target=_blank} bölümüne bakın! +**FastAPI**’ı geliştirmenin ilk aşamalarında Molten’ı keşfettim. Oldukça benzer fikirleri vardı: + +* Python tip belirteçlerine dayanır. +* Bu tiplere bağlı doğrulama ve dökümantasyon sağlar. +* Bağımlılık enjeksiyonu sistemi vardır. + +Pydantic gibi doğrulama, dönüşüm ve dökümantasyon için üçüncü parti bir kütüphane kullanmaz; kendi içinde sağlar. Bu yüzden bu veri tipi tanımlarını tekrar kullanmak o kadar kolay olmaz. + +Biraz daha ayrıntılı yapılandırma ister. Ve ASGI yerine WSGI tabanlı olduğundan, Uvicorn, Starlette ve Sanic gibi araçların yüksek performansından faydalanmaya yönelik tasarlanmamıştır. + +Bağımlılık enjeksiyonu sistemi, bağımlılıkların önceden kaydedilmesini ve tiplerine göre çözülmesini gerektirir. Yani belirli bir tipi sağlayan birden fazla “bileşen” tanımlanamaz. + +Route’lar, endpoint’i işleyen fonksiyonun üstüne konan dekoratörlerle değil, tek bir yerde, farklı yerlerde tanımlanmış fonksiyonlar kullanılarak ilan edilir. Bu yaklaşım, Flask (ve Starlette) yerine Django’ya daha yakındır; kodda aslında birbirine sıkı bağlı olan şeyleri ayırır. + +/// check | **FastAPI**'a ilham olan + +Model özelliklerinin “varsayılan” değerlerini kullanarak veri tiplerine ekstra doğrulamalar tanımlamak. Bu, editör desteğini iyileştirir ve Pydantic’te daha önce yoktu. + +Bu yaklaşım, Pydantic’te de aynı doğrulama beyan stilinin desteklenmesine ilham verdi (bu işlevselliklerin tamamı artık Pydantic’te mevcut). + +/// + +### Hug { #hug } + +Hug, Python tip belirteçlerini kullanarak API parametre tiplerini ilan etmeyi uygulayan ilk framework’lerden biriydi. Diğer araçlara da ilham veren harika bir fikirdi. + +Standart Python tipleri yerine kendi özel tiplerini kullansa da büyük bir adımdı. + +JSON ile tüm API’ı beyan eden özel bir şema üreten ilk framework’lerden biriydi. + +OpenAPI veya JSON Schema gibi bir standarda dayanmadığı için Swagger UI gibi diğer araçlarla doğrudan entegre edilemezdi. Yine de oldukça yenilikçiydi. + +Nadir bir özelliği daha vardı: aynı framework ile hem API’lar hem de CLI’lar oluşturmak mümkündü. + +Senkron Python web framework’leri için önceki standart olan WSGI’ye dayandığından, WebSocket vb. şeyleri işleyemez, ancak yine de yüksek performansa sahiptir. + +/// info | Bilgi + +Hug, Python dosyalarındaki import’ları otomatik sıralayan harika bir araç olan `isort`’un geliştiricisi Timothy Crosley tarafından geliştirildi. + +/// + +/// check | **FastAPI**'a ilham olan fikirler + +Hug, APIStar’ın bazı kısımlarına ilham verdi ve APIStar ile birlikte en umut verici bulduğum araçlardandı. + +**FastAPI**, parametreleri ilan etmek ve API’ı otomatik tanımlayan bir şema üretmek için Python tip belirteçlerini kullanma fikrini Hug’dan ilhamla benimsedi. + +Ayrıca header ve cookie ayarlamak için fonksiyonlarda `response` parametresi ilan etme fikrine de Hug ilham verdi. + +/// + +### APIStar (<= 0.5) { #apistar-0-5 } + +**FastAPI**’yi inşa etmeye karar vermeden hemen önce **APIStar** sunucusunu buldum. Aradığım şeylerin neredeyse hepsine sahipti ve harika bir tasarımı vardı. + +Python tip belirteçleriyle parametreleri ve istekleri ilan eden bir framework’ün gördüğüm ilk örneklerindendi (NestJS ve Molten’dan önce). Aşağı yukarı Hug ile aynı zamanlarda buldum; ancak APIStar, OpenAPI standardını kullanıyordu. + +Farklı yerlerdeki aynı tip belirteçlerine dayanarak otomatik veri doğrulama, veri dönüşümü ve OpenAPI şeması üretimi vardı. + +Gövde şema tanımları Pydantic’tekiyle aynı Python tip belirteçlerini kullanmıyordu; biraz daha Marshmallow’a benziyordu. Bu yüzden editör desteği o kadar iyi olmazdı; yine de APIStar mevcut en iyi seçenekti. + +O dönem kıyaslamalarda en iyi performansa sahipti (sadece Starlette tarafından geçiliyordu). + +Başta otomatik API dökümantasyonu sunan bir web arayüzü yoktu ama Swagger UI ekleyebileceğimi biliyordum. + +Bağımlılık enjeksiyonu sistemi vardı. Diğer araçlarda olduğu gibi bileşenlerin önceden kaydedilmesini gerektiriyordu. Yine de harika bir özellikti. + +Güvenlik entegrasyonu olmadığından tam bir projede kullanamadım; bu yüzden Flask-apispec tabanlı full‑stack üreticilerle sahip olduğum özelliklerin tamamını ikame edemedim. Bu işlevi ekleyen bir pull request’i yapılacaklar listeme almıştım. + +Sonra projenin odağı değişti. + +Artık bir API web framework’ü değildi; geliştirici Starlette’e odaklanmak zorundaydı. + +Şimdi APIStar, bir web framework’ü değil, OpenAPI spesifikasyonlarını doğrulamak için araçlar takımından ibaret. + +/// info | Bilgi + +APIStar, aşağıdakilerin de yaratıcısı olan Tom Christie tarafından geliştirildi: + +* Django REST Framework +* **FastAPI**’ın üzerine kurulu Starlette +* Starlette ve **FastAPI** tarafından kullanılan Uvicorn + +/// + +/// check | **FastAPI**'a ilham olan + +Var olmak. + +Aynı Python tipleriyle (hem veri doğrulama, dönüşüm ve dökümantasyon) birden çok şeyi ilan etmek ve aynı anda harika editör desteği sağlamak, bence dahiyane bir fikirdi. + +Uzun süre benzer bir framework arayıp birçok alternatifi denedikten sonra, APIStar mevcut en iyi seçenekti. + +Sonra APIStar bir sunucu olarak var olmaktan çıktı ve Starlette oluşturuldu; böyle bir sistem için daha iyi bir temel oldu. Bu, **FastAPI**’yi inşa etmek için son ilhamdı. + +Önceki bu araçlardan edinilen deneyimler üzerine özellikleri, tip sistemi ve diğer kısımları geliştirip artırırken, **FastAPI**’yi APIStar’ın “ruhani varisi” olarak görüyorum. + +/// + +## **FastAPI** Tarafından Kullanılanlar { #used-by-fastapi } + +### Pydantic { #pydantic } + +Pydantic, Python tip belirteçlerine dayalı olarak veri doğrulama, dönüşüm ve dökümantasyon (JSON Schema kullanarak) tanımlamak için bir kütüphanedir. + +Bu onu aşırı sezgisel kılar. + +Marshmallow ile karşılaştırılabilir. Kıyaslamalarda Marshmallow’dan daha hızlıdır. Aynı Python tip belirteçlerine dayandığı için editör desteği harikadır. + +/// check | **FastAPI** bunu şurada kullanır + +Tüm veri doğrulama, veri dönüşümü ve JSON Schema tabanlı otomatik model dökümantasyonunu halletmekte. + +**FastAPI** daha sonra bu JSON Schema verisini alır ve (yaptığı diğer şeylerin yanı sıra) OpenAPI içine yerleştirir. + +/// + +### Starlette { #starlette } + +Starlette, yüksek performanslı asyncio servisleri oluşturmak için ideal, hafif bir ASGI framework’ü/araç takımıdır. + +Çok basit ve sezgiseldir. Kolayca genişletilebilir ve modüler bileşenlere sahip olacak şekilde tasarlanmıştır. + +Şunlara sahiptir: + +* Cidden etkileyici performans. +* WebSocket desteği. +* Süreç içi arka plan görevleri. +* Başlatma ve kapatma olayları. +* HTTPX üzerinde geliştirilmiş test istemcisi. +* CORS, GZip, Statik Dosyalar, Streaming cevaplar. +* Oturum (Session) ve Cookie desteği. +* %100 test kapsamı. +* %100 tip anotasyonlu kod tabanı. +* Az sayıda zorunlu bağımlılık. + +Starlette, şu anda test edilen en hızlı Python framework’üdür. Yalnızca bir framework değil, bir sunucu olan Uvicorn tarafından geçilir. + +Starlette, temel web mikroframework işlevselliğinin tamamını sağlar. + +Ancak otomatik veri doğrulama, dönüşüm veya dökümantasyon sağlamaz. + +**FastAPI**’nin bunun üzerine eklediği ana şeylerden biri, Pydantic kullanarak, bütünüyle Python tip belirteçlerine dayalı bu özelliklerdir. Buna ek olarak bağımlılık enjeksiyonu sistemi, güvenlik yardımcıları, OpenAPI şema üretimi vb. gelir. + +/// note | Teknik Detaylar + +ASGI, Django çekirdek ekip üyeleri tarafından geliştirilen yeni bir “standart”tır. Hâlâ resmi bir “Python standardı” (PEP) değildir, ancak bu süreç üzerindedirler. + +Buna rağmen, şimdiden birçok araç tarafından bir “standart” olarak kullanılmaktadır. Bu, birlikte çalışabilirliği büyük ölçüde artırır; örneğin Uvicorn’u başka bir ASGI sunucusuyla (Daphne veya Hypercorn gibi) değiştirebilir ya da `python-socketio` gibi ASGI uyumlu araçlar ekleyebilirsiniz. + +/// + +/// check | **FastAPI** bunu şurada kullanır + +Tüm temel web kısımlarını ele almak; üzerine özellikler eklemek. + +`FastAPI` sınıfı, doğrudan `Starlette` sınıfından miras alır. + +Dolayısıyla Starlette ile yapabildiğiniz her şeyi, adeta “turbo şarjlı Starlette” olan **FastAPI** ile de doğrudan yapabilirsiniz. + +/// + +### Uvicorn { #uvicorn } + +Uvicorn, uvloop ve httptools üzerinde inşa edilmiş, ışık hızında bir ASGI sunucusudur. + +Bir web framework’ü değil, bir sunucudur. Örneğin path’lere göre yönlendirme araçları sağlamaz; bunu Starlette (veya **FastAPI**) gibi bir framework üstte sağlar. + +Starlette ve **FastAPI** için önerilen sunucudur. + +/// check | **FastAPI** bunu şöyle önerir + +**FastAPI** uygulamalarını çalıştırmak için ana web sunucusu. + +Komut satırında `--workers` seçeneğini kullanarak asenkron çok süreçli (multi‑process) bir sunucu da elde edebilirsiniz. + +Daha fazla detay için [Dağıtım](deployment/index.md){.internal-link target=_blank} bölümüne bakın. + +/// + +## Kıyaslamalar ve Hız { #benchmarks-and-speed } + +Uvicorn, Starlette ve FastAPI arasındaki farkı anlamak ve karşılaştırmak için [Kıyaslamalar](benchmarks.md){.internal-link target=_blank} bölümüne göz atın. diff --git a/docs/tr/docs/deployment/docker.md b/docs/tr/docs/deployment/docker.md index b48d2ddf5..6c8f74c77 100644 --- a/docs/tr/docs/deployment/docker.md +++ b/docs/tr/docs/deployment/docker.md @@ -145,8 +145,6 @@ Paket bağımlılıklarını tanımlamak ve yüklemek için başka formatlar ve * Aşağıdakilerle bir `main.py` dosyası oluşturun: ```Python -from typing import Union - from fastapi import FastAPI app = FastAPI() @@ -158,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): +def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} ``` @@ -260,7 +258,7 @@ FastAPI'nin düzgün şekilde kapanabilmesi ve [lifespan event](../advanced/even Detaylar için shell ve exec form için Docker dokümanlarına bakabilirsiniz. -Bu durum `docker compose` kullanırken oldukça belirgin olabilir. Daha teknik detaylar için şu Docker Compose FAQ bölümüne bakın: Why do my services take 10 seconds to recreate or stop?. +Bu durum `docker compose` kullanırken oldukça belirgin olabilir. Daha teknik detaylar için şu Docker Compose FAQ bölümüne bakın: Hizmetlerimin yeniden oluşturulması veya durması neden 10 saniye sürüyor?. #### Dizin Yapısı { #directory-structure } @@ -456,7 +454,7 @@ Container kullanmadan, uygulamaları startup'ta çalıştırmak ve restart mekan ## Replication - Process Sayısı { #replication-number-of-processes } -Kubernetes, Docker Swarm Mode, Nomad veya benzeri, birden fazla makinede dağıtık container'ları yöneten karmaşık bir sistemle kurulmuş bir cluster'ınız varsa, replication'ı her container içinde bir **process manager** (ör. worker'lı Uvicorn) kullanarak yönetmek yerine, muhtemelen **cluster seviyesinde** ele almak istersiniz. +Kubernetes, Docker Swarm Mode, Nomad veya benzeri, birden fazla makinede dağıtık container'ları yöneten karmaşık bir sistemle kurulmuş bir cluster'ınız varsa, replication'ı her container içinde bir **process manager** (ör. worker'lı Uvicorn) kullanarak yönetmek yerine, muhtemelen **cluster seviyesinde** ele almak istersiniz. Kubernetes gibi dağıtık container yönetim sistemleri, gelen request'ler için **load balancing** desteği sunarken aynı zamanda **container replication**'ını yönetmek için entegre mekanizmalara sahiptir. Hepsi **cluster seviyesinde**. diff --git a/docs/tr/docs/history-design-future.md b/docs/tr/docs/history-design-future.md index cad290828..764a51957 100644 --- a/docs/tr/docs/history-design-future.md +++ b/docs/tr/docs/history-design-future.md @@ -1,4 +1,4 @@ -# Geçmişi, Tasarımı ve Geleceği +# Geçmişi, Tasarımı ve Geleceği { #history-design-and-future } Bir süre önce, bir **FastAPI** kullanıcısı sordu: @@ -6,7 +6,7 @@ Bir süre önce, **Pydantic**'i kullanmaya karar verdim. @@ -60,11 +60,11 @@ Sonra, JSON Schema ile tamamen uyumlu olmasını sağlamak, kısıtlama bildirim Geliştirme sırasında, diğer ana gereksinim olan **Starlette**'e de katkıda bulundum. -## Geliştirme +## Geliştirme { #development } **FastAPI**'ı oluşturmaya başladığımda, parçaların çoğu zaten yerindeydi, tasarım tanımlanmıştı, gereksinimler ve araçlar hazırdı, standartlar ve tanımlamalar hakkındaki bilgi net ve tazeydi. -## Gelecek +## Gelecek { #future } Şimdiye kadar, **FastAPI**'ın fikirleriyle birçok kişiye faydalı olduğu apaçık ortada. diff --git a/docs/tr/docs/translation-banner.md b/docs/tr/docs/translation-banner.md new file mode 100644 index 000000000..b52578f71 --- /dev/null +++ b/docs/tr/docs/translation-banner.md @@ -0,0 +1,11 @@ +/// details | 🌐 Yapay Zekâ ve İnsanlar Tarafından Çeviri + +Bu çeviri, insanlar tarafından yönlendirilen bir yapay zekâ ile oluşturuldu. 🤝 + +Orijinal anlamın yanlış anlaşılması ya da kulağa doğal gelmeme gibi hatalar içerebilir. 🤖 + +[Yapay zekâyı daha iyi yönlendirmemize yardımcı olarak](https://fastapi.tiangolo.com/tr/contributing/#translations) bu çeviriyi iyileştirebilirsiniz. + +[İngilizce sürüm](ENGLISH_VERSION_URL) + +/// diff --git a/docs/tr/docs/tutorial/body-multiple-params.md b/docs/tr/docs/tutorial/body-multiple-params.md index 3bac80d6a..29970ca40 100644 --- a/docs/tr/docs/tutorial/body-multiple-params.md +++ b/docs/tr/docs/tutorial/body-multiple-params.md @@ -103,13 +103,13 @@ Elbette ihtiyaç duyduğunuzda, body parametrelerine ek olarak query parametrele Varsayılan olarak tekil değerler query parametresi olarak yorumlandığı için, ayrıca `Query` eklemeniz gerekmez; şöyle yazmanız yeterlidir: ```Python -q: Union[str, None] = None +q: str | None = None ``` -Ya da Python 3.10 ve üzeri için: +Ya da Python 3.9'da: ```Python -q: str | None = None +q: Union[str, None] = None ``` Örneğin: diff --git a/docs/tr/docs/tutorial/path-operation-configuration.md b/docs/tr/docs/tutorial/path-operation-configuration.md index 610b863a4..3fe24dc0a 100644 --- a/docs/tr/docs/tutorial/path-operation-configuration.md +++ b/docs/tr/docs/tutorial/path-operation-configuration.md @@ -52,11 +52,11 @@ Bu durumlarda tag’leri bir `Enum` içinde tutmak mantıklı olabilir. Bir `summary` ve `description` ekleyebilirsiniz: -{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *} +{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} ## Docstring’den Description { #description-from-docstring } -Açıklamalar genelde uzun olur ve birden fazla satıra yayılır; bu yüzden *path operation* açıklamasını, fonksiyonun içinde docstring olarak tanımlayabilirsiniz; **FastAPI** de onu buradan okur. +Açıklamalar genelde uzun olur ve birden fazla satıra yayılır; bu yüzden *path operation* açıklamasını, fonksiyonun içinde docstring olarak tanımlayabilirsiniz; **FastAPI** de onu buradan okur. Docstring içinde Markdown yazabilirsiniz; doğru şekilde yorumlanır ve gösterilir (docstring girintisi dikkate alınarak). @@ -70,7 +70,7 @@ Interactive docs’ta şöyle kullanılacaktır: `response_description` parametresi ile response açıklamasını belirtebilirsiniz: -{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *} +{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} /// info | Bilgi @@ -90,7 +90,7 @@ Bu yüzden siz sağlamazsanız, **FastAPI** otomatik olarak "Successful response ## Bir *path operation*’ı Deprecate Etmek { #deprecate-a-path-operation } -Bir *path operation*’ı kaldırmadan, deprecated olarak işaretlemeniz gerekiyorsa `deprecated` parametresini verin: +Bir *path operation*’ı kaldırmadan, deprecated olarak işaretlemeniz gerekiyorsa `deprecated` parametresini verin: {* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *} From 2eb454ab04e81325ed430a036103e4aede299d61 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Feb 2026 17:24:15 +0000 Subject: [PATCH 237/243] =?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 689610049..4ba0491a5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🌐 Update translations for tr (outdated and missing). PR [#14838](https://github.com/fastapi/fastapi/pull/14838) by [@YuriiMotov](https://github.com/YuriiMotov). + ### Internal * ⬆️ Upgrade development dependencies. PR [#14854](https://github.com/fastapi/fastapi/pull/14854) by [@tiangolo](https://github.com/tiangolo). From cba537ab717ff205857a0eb877c69e79cfd06a51 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 6 Feb 2026 20:24:25 +0300 Subject: [PATCH 238/243] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20?= =?UTF-8?q?for=20fr=20(outdated=20pages)=20(#14839)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/fr/docs/tutorial/body-multiple-params.md | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/fr/docs/tutorial/body-multiple-params.md b/docs/fr/docs/tutorial/body-multiple-params.md index 9aed24e98..92ca2afc3 100644 --- a/docs/fr/docs/tutorial/body-multiple-params.md +++ b/docs/fr/docs/tutorial/body-multiple-params.md @@ -2,23 +2,23 @@ Maintenant que nous avons vu comment utiliser `Path` et `Query`, voyons des usages plus avancés des déclarations de paramètres du corps de la requête. -## Mélanger les paramètres `Path`, `Query` et body { #mix-path-query-and-body-parameters } +## Mélanger les paramètres `Path`, `Query` et du corps de la requête { #mix-path-query-and-body-parameters } -Tout d'abord, sachez que vous pouvez mélanger librement les déclarations des paramètres `Path`, `Query` et du body, **FastAPI** saura quoi faire. +Tout d'abord, sachez que vous pouvez mélanger librement les déclarations des paramètres `Path`, `Query` et du corps de la requête, **FastAPI** saura quoi faire. -Et vous pouvez également déclarer des paramètres du body comme étant optionnels, en leur assignant une valeur par défaut à `None` : +Et vous pouvez également déclarer des paramètres du corps de la requête comme étant optionnels, en leur assignant une valeur par défaut à `None` : {* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} /// note | Remarque -Notez que, dans ce cas, l'élément `item` récupéré depuis le body est optionnel. Comme sa valeur par défaut est `None`. +Notez que, dans ce cas, l'élément `item` récupéré depuis le corps de la requête est optionnel. Comme sa valeur par défaut est `None`. /// -## Paramètres multiples du body { #multiple-body-parameters } +## Paramètres multiples du corps de la requête { #multiple-body-parameters } -Dans l'exemple précédent, les chemins d'accès attendraient un body JSON avec les attributs d'un `Item`, par exemple : +Dans l'exemple précédent, les chemins d'accès attendraient un corps de la requête JSON avec les attributs d'un `Item`, par exemple : ```JSON { @@ -29,13 +29,13 @@ Dans l'exemple précédent, les chemins d'accès attendraient un body JSON avec } ``` -Mais vous pouvez également déclarer plusieurs paramètres provenant du body, par exemple `item` et `user` : +Mais vous pouvez également déclarer plusieurs paramètres provenant du corps de la requête, par exemple `item` et `user` : {* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *} -Dans ce cas, **FastAPI** détectera qu'il y a plus d'un paramètre du body dans la fonction (il y a deux paramètres qui sont des modèles Pydantic). +Dans ce cas, **FastAPI** détectera qu'il y a plus d'un paramètre du corps de la requête dans la fonction (il y a deux paramètres qui sont des modèles Pydantic). -Il utilisera alors les noms des paramètres comme clés (noms de champs) dans le body, et s'attendra à recevoir un body semblable à : +Il utilisera alors les noms des paramètres comme clés (noms de champs) dans le corps de la requête, et s'attendra à recevoir un corps de la requête semblable à : ```JSON { @@ -54,7 +54,7 @@ Il utilisera alors les noms des paramètres comme clés (noms de champs) dans le /// note | Remarque -Notez que, bien que `item` ait été déclaré de la même manière qu'auparavant, il est désormais attendu à l'intérieur du body sous la clé `item`. +Notez que, bien que `item` ait été déclaré de la même manière qu'auparavant, il est désormais attendu à l'intérieur du corps de la requête sous la clé `item`. /// @@ -62,19 +62,19 @@ Notez que, bien que `item` ait été déclaré de la même manière qu'auparavan Il effectuera la validation des données composées, et les documentera ainsi pour le schéma OpenAPI et la documentation automatique. -## Valeurs singulières dans le body { #singular-values-in-body } +## Valeurs singulières dans le corps de la requête { #singular-values-in-body } De la même façon qu'il existe `Query` et `Path` pour définir des données supplémentaires pour les paramètres de requête et de chemin, **FastAPI** fournit un équivalent `Body`. -Par exemple, en étendant le modèle précédent, vous pourriez décider d'avoir une autre clé `importance` dans le même body, en plus de `item` et `user`. +Par exemple, en étendant le modèle précédent, vous pourriez décider d'avoir une autre clé `importance` dans le même corps de la requête, en plus de `item` et `user`. Si vous le déclarez tel quel, comme c'est une valeur singulière, **FastAPI** supposera qu'il s'agit d'un paramètre de requête. -Mais vous pouvez indiquer à **FastAPI** de la traiter comme une autre clé du body en utilisant `Body` : +Mais vous pouvez indiquer à **FastAPI** de la traiter comme une autre clé du corps de la requête en utilisant `Body` : {* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} -Dans ce cas, **FastAPI** s'attendra à un body semblable à : +Dans ce cas, **FastAPI** s'attendra à un corps de la requête semblable à : ```JSON { @@ -94,9 +94,9 @@ Dans ce cas, **FastAPI** s'attendra à un body semblable à : Encore une fois, il convertira les types de données, validera, documentera, etc. -## Paramètres multiples du body et paramètres de requête { #multiple-body-params-and-query } +## Paramètres multiples du corps de la requête et paramètres de requête { #multiple-body-params-and-query } -Bien entendu, vous pouvez également déclarer des paramètres de requête supplémentaires quand vous en avez besoin, en plus de tout paramètre du body. +Bien entendu, vous pouvez également déclarer des paramètres de requête supplémentaires quand vous en avez besoin, en plus de tout paramètre du corps de la requête. Comme, par défaut, les valeurs singulières sont interprétées comme des paramètres de requête, vous n'avez pas besoin d'ajouter explicitement `Query`, vous pouvez simplement écrire : @@ -120,13 +120,13 @@ Par exemple : /// -## Intégrer un seul paramètre du body { #embed-a-single-body-parameter } +## Intégrer un seul paramètre du corps de la requête { #embed-a-single-body-parameter } -Supposons que vous n'ayez qu'un seul paramètre `item` dans le body, provenant d'un modèle Pydantic `Item`. +Supposons que vous n'ayez qu'un seul paramètre `item` dans le corps de la requête, provenant d'un modèle Pydantic `Item`. Par défaut, **FastAPI** attendra alors son contenu directement. -Mais si vous voulez qu'il attende un JSON avec une clé `item` contenant le contenu du modèle, comme lorsqu'on déclare des paramètres supplémentaires du body, vous pouvez utiliser le paramètre spécial `embed` de `Body` : +Mais si vous voulez qu'il attende un JSON avec une clé `item` contenant le contenu du modèle, comme lorsqu'on déclare des paramètres supplémentaires du corps de la requête, vous pouvez utiliser le paramètre spécial `embed` de `Body` : ```Python item: Item = Body(embed=True) @@ -136,7 +136,7 @@ comme dans : {* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} -Dans ce cas **FastAPI** s'attendra à un body semblable à : +Dans ce cas **FastAPI** s'attendra à un corps de la requête semblable à : ```JSON hl_lines="2" { @@ -162,10 +162,10 @@ au lieu de : ## Récapitulatif { #recap } -Vous pouvez ajouter plusieurs paramètres du body à votre fonction de chemin d'accès, même si une requête ne peut avoir qu'un seul body. +Vous pouvez ajouter plusieurs paramètres du corps de la requête à votre fonction de chemin d'accès, même si une requête ne peut avoir qu'un seul corps de la requête. Mais **FastAPI** s'en chargera, vous fournira les bonnes données dans votre fonction, et validera et documentera le schéma correct dans le chemin d'accès. -Vous pouvez également déclarer des valeurs singulières à recevoir dans le body. +Vous pouvez également déclarer des valeurs singulières à recevoir dans le corps de la requête. -Et vous pouvez indiquer à **FastAPI** d'intégrer le body sous une clé même lorsqu'un seul paramètre est déclaré. +Et vous pouvez indiquer à **FastAPI** d'intégrer le corps de la requête sous une clé même lorsqu'un seul paramètre est déclaré. From 512c3ad88c8f03a03154df317c65aa0ca4f1f78d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Feb 2026 17:24:56 +0000 Subject: [PATCH 239/243] =?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 4ba0491a5..75e0a5f56 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Update translations for fr (outdated pages). PR [#14839](https://github.com/fastapi/fastapi/pull/14839) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for tr (outdated and missing). PR [#14838](https://github.com/fastapi/fastapi/pull/14838) by [@YuriiMotov](https://github.com/YuriiMotov). ### Internal From 3c49346238643287780d9f0d673f88b0d02df109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Feb 2026 10:01:05 -0800 Subject: [PATCH 240/243] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20interna?= =?UTF-8?q?ls,=20cleanup=20unneeded=20Pydantic=20v1=20specific=20logic=20(?= =?UTF-8?q?#14856)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat/__init__.py | 1 - fastapi/_compat/shared.py | 1 - fastapi/_compat/v2.py | 10 ++-------- fastapi/routing.py | 15 +-------------- fastapi/utils.py | 16 ---------------- 5 files changed, 3 insertions(+), 40 deletions(-) diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py index 3dfaf9b71..b7701bd1a 100644 --- a/fastapi/_compat/__init__.py +++ b/fastapi/_compat/__init__.py @@ -1,4 +1,3 @@ -from .shared import PYDANTIC_V2 as PYDANTIC_V2 from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1 from .shared import field_annotation_is_scalar as field_annotation_is_scalar diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py index 68b9bbdf1..fdda481b8 100644 --- a/fastapi/_compat/shared.py +++ b/fastapi/_compat/shared.py @@ -28,7 +28,6 @@ else: ) # pyright: ignore[reportAttributeAccessIssue] PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) -PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 sequence_annotation_to_type = { diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index dae78a32e..6c2eec58b 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -500,14 +500,8 @@ def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: - all_flat_models: TypeModelSet = set() - - v2_model_fields = [field for field in fields if isinstance(field, ModelField)] - v2_flat_models = get_flat_models_from_fields(v2_model_fields, known_models=set()) - all_flat_models = all_flat_models.union(v2_flat_models) - - model_name_map = get_model_name_map(all_flat_models) - return model_name_map + flat_models = get_flat_models_from_fields(fields, known_models=set()) + return get_model_name_map(flat_models) def get_flat_models_from_model( diff --git a/fastapi/routing.py b/fastapi/routing.py index c95f624bd..b8d1f7624 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -59,7 +59,6 @@ from fastapi.exceptions import ( ) from fastapi.types import DecoratedCallable, IncEx from fastapi.utils import ( - create_cloned_field, create_model_field, generate_unique_id, get_value_or_default, @@ -652,20 +651,8 @@ class APIRoute(routing.Route): type_=self.response_model, mode="serialization", ) - # Create a clone of the field, so that a Pydantic submodel is not returned - # as is just because it's an instance of a subclass of a more limited class - # e.g. UserInDB (containing hashed_password) could be a subclass of User - # that doesn't have the hashed_password. But because it's a subclass, it - # would pass the validation and be returned as is. - # By being a new field, no inheritance will be passed as is. A new model - # will always be created. - # TODO: remove when deprecating Pydantic v1 - self.secure_cloned_response_field: Optional[ModelField] = ( - create_cloned_field(self.response_field) - ) else: self.response_field = None # type: ignore - self.secure_cloned_response_field = None self.dependencies = list(dependencies or []) self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "") # if a "form feed" character (page break) is found in the description text, @@ -720,7 +707,7 @@ class APIRoute(routing.Route): body_field=self.body_field, status_code=self.status_code, response_class=self.response_class, - response_field=self.secure_cloned_response_field, + response_field=self.response_field, response_model_include=self.response_model_include, response_model_exclude=self.response_model_exclude, response_model_by_alias=self.response_model_by_alias, diff --git a/fastapi/utils.py b/fastapi/utils.py index 1c3a0881f..da11fe2c7 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -1,13 +1,11 @@ import re import warnings -from collections.abc import MutableMapping from typing import ( TYPE_CHECKING, Any, Optional, Union, ) -from weakref import WeakKeyDictionary import fastapi from fastapi._compat import ( @@ -21,7 +19,6 @@ from fastapi._compat import ( ) from fastapi.datastructures import DefaultPlaceholder, DefaultType from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError -from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Literal @@ -30,11 +27,6 @@ from ._compat import v2 if TYPE_CHECKING: # pragma: nocover from .routing import APIRoute -# Cache for `create_cloned_field` -_CLONED_TYPES_CACHE: MutableMapping[type[BaseModel], type[BaseModel]] = ( - WeakKeyDictionary() -) - def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool: if status_code is None: @@ -97,14 +89,6 @@ def create_model_field( ) from None -def create_cloned_field( - field: ModelField, - *, - cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None, -) -> ModelField: - return field - - def generate_operation_id_for_path( *, name: str, path: str, method: str ) -> str: # pragma: nocover From ac8362c447b94247e9f7268c3d1e2807818647ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Feb 2026 18:01:30 +0000 Subject: [PATCH 241/243] =?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 75e0a5f56..d442ef9a1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ♻️ Refactor internals, cleanup unneeded Pydantic v1 specific logic. PR [#14856](https://github.com/fastapi/fastapi/pull/14856) by [@tiangolo](https://github.com/tiangolo). + ### Translations * 🌐 Update translations for fr (outdated pages). PR [#14839](https://github.com/fastapi/fastapi/pull/14839) by [@YuriiMotov](https://github.com/YuriiMotov). From cf55bade7ea6e4bf85a167e8aed1c6167e7a4196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Feb 2026 11:04:24 -0800 Subject: [PATCH 242/243] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20interna?= =?UTF-8?q?ls,=20remove=20Pydantic=20v1=20only=20logic,=20no=20longer=20ne?= =?UTF-8?q?eded=20(#14857)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat/__init__.py | 1 - fastapi/_compat/v2.py | 107 ++++------------------------------ fastapi/dependencies/utils.py | 8 +-- fastapi/openapi/utils.py | 8 ++- fastapi/routing.py | 7 +-- 5 files changed, 19 insertions(+), 112 deletions(-) diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py index b7701bd1a..22bc28dec 100644 --- a/fastapi/_compat/__init__.py +++ b/fastapi/_compat/__init__.py @@ -25,7 +25,6 @@ from .v2 import copy_field_info as copy_field_info from .v2 import create_body_model as create_body_model from .v2 import evaluate_forwardref as evaluate_forwardref from .v2 import get_cached_model_fields as get_cached_model_fields -from .v2 import get_compat_model_name_map as get_compat_model_name_map from .v2 import get_definitions as get_definitions from .v2 import get_missing_field_error as get_missing_field_error from .v2 import get_schema_from_model_field as get_schema_from_model_field diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 6c2eec58b..57b3d94ff 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -1,7 +1,7 @@ import re import warnings from collections.abc import Sequence -from copy import copy, deepcopy +from copy import copy from dataclasses import dataclass, is_dataclass from enum import Enum from functools import lru_cache @@ -169,11 +169,11 @@ class ModelField: values: dict[str, Any] = {}, # noqa: B006 *, loc: tuple[Union[int, str], ...] = (), - ) -> tuple[Any, Union[list[dict[str, Any]], None]]: + ) -> tuple[Any, list[dict[str, Any]]]: try: return ( self._type_adapter.validate_python(value, from_attributes=True), - None, + [], ) except ValidationError as exc: return None, _regenerate_error_with_loc( @@ -305,94 +305,12 @@ def get_definitions( if "description" in item_def: item_description = cast(str, item_def["description"]).split("\f")[0] item_def["description"] = item_description - new_mapping, new_definitions = _remap_definitions_and_field_mappings( - model_name_map=model_name_map, - definitions=definitions, # type: ignore[arg-type] - field_mapping=field_mapping, - ) - return new_mapping, new_definitions - - -def _replace_refs( - *, - schema: dict[str, Any], - old_name_to_new_name_map: dict[str, str], -) -> dict[str, Any]: - new_schema = deepcopy(schema) - for key, value in new_schema.items(): - if key == "$ref": - value = schema["$ref"] - if isinstance(value, str): - ref_name = schema["$ref"].split("/")[-1] - if ref_name in old_name_to_new_name_map: - new_name = old_name_to_new_name_map[ref_name] - new_schema["$ref"] = REF_TEMPLATE.format(model=new_name) - continue - if isinstance(value, dict): - new_schema[key] = _replace_refs( - schema=value, - old_name_to_new_name_map=old_name_to_new_name_map, - ) - elif isinstance(value, list): - new_value = [] - for item in value: - if isinstance(item, dict): - new_item = _replace_refs( - schema=item, - old_name_to_new_name_map=old_name_to_new_name_map, - ) - new_value.append(new_item) - - else: - new_value.append(item) - new_schema[key] = new_value - return new_schema - - -def _remap_definitions_and_field_mappings( - *, - model_name_map: ModelNameMap, - definitions: dict[str, Any], - field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], -) -> tuple[ - dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], - dict[str, Any], -]: - old_name_to_new_name_map = {} - for field_key, schema in field_mapping.items(): - model = field_key[0].type_ - if model not in model_name_map or "$ref" not in schema: - continue - new_name = model_name_map[model] - old_name = schema["$ref"].split("/")[-1] - if old_name in {f"{new_name}-Input", f"{new_name}-Output"}: - continue - old_name_to_new_name_map[old_name] = new_name - - new_field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ] = {} - for field_key, schema in field_mapping.items(): - new_schema = _replace_refs( - schema=schema, - old_name_to_new_name_map=old_name_to_new_name_map, - ) - new_field_mapping[field_key] = new_schema - - new_definitions = {} - for key, value in definitions.items(): - if key in old_name_to_new_name_map: - new_key = old_name_to_new_name_map[key] - else: - new_key = key - new_value = _replace_refs( - schema=value, - old_name_to_new_name_map=old_name_to_new_name_map, - ) - new_definitions[new_key] = new_value - return new_field_mapping, new_definitions + # definitions: dict[DefsRef, dict[str, Any]] + # but mypy complains about general str in other places that are not declared as + # DefsRef, although DefsRef is just str: + # DefsRef = NewType('DefsRef', str) + # So, a cast to simplify the types here + return field_mapping, cast(dict[str, dict[str, Any]], definitions) def is_scalar_field(field: ModelField) -> bool: @@ -441,7 +359,7 @@ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: return shared.sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return,index] -def get_missing_field_error(loc: tuple[str, ...]) -> dict[str, Any]: +def get_missing_field_error(loc: tuple[Union[int, str], ...]) -> dict[str, Any]: error = ValidationError.from_exception_data( "Field required", [{"type": "missing", "loc": loc, "input": {}}] ).errors(include_url=False)[0] @@ -499,11 +417,6 @@ def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str return {v: k for k, v in name_model_map.items()} -def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: - flat_models = get_flat_models_from_fields(fields, known_models=set()) - return get_model_name_map(flat_models) - - def get_flat_models_from_model( model: type["BaseModel"], known_models: Union[TypeModelSet, None] = None ) -> TypeModelSet: diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index b8f7f948c..dd42371ec 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -21,7 +21,6 @@ from fastapi._compat import ( ModelField, RequiredParam, Undefined, - _regenerate_error_with_loc, copy_field_info, create_body_model, evaluate_forwardref, @@ -718,12 +717,7 @@ def _validate_value_with_model_field( return None, [get_missing_field_error(loc=loc)] else: return deepcopy(field.default), [] - v_, errors_ = field.validate(value, values, loc=loc) - if isinstance(errors_, list): - new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) - return None, new_errors - else: - return v_, [] + return field.validate(value, values, loc=loc) def _is_json_field(field: ModelField) -> bool: diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 5736af3b7..c9b006a71 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -9,11 +9,14 @@ from fastapi import routing from fastapi._compat import ( ModelField, Undefined, - get_compat_model_name_map, get_definitions, get_schema_from_model_field, lenient_issubclass, ) +from fastapi._compat.v2 import ( + get_flat_models_from_fields, + get_model_name_map, +) from fastapi.datastructures import DefaultPlaceholder from fastapi.dependencies.models import Dependant from fastapi.dependencies.utils import ( @@ -512,7 +515,8 @@ def get_openapi( webhook_paths: dict[str, dict[str, Any]] = {} operation_ids: set[str] = set() all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or [])) - model_name_map = get_compat_model_name_map(all_fields) + flat_models = get_flat_models_from_fields(all_fields, known_models=set()) + model_name_map = get_model_name_map(flat_models) field_mapping, definitions = get_definitions( fields=all_fields, model_name_map=model_name_map, diff --git a/fastapi/routing.py b/fastapi/routing.py index b8d1f7624..fe8d88609 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -277,15 +277,12 @@ async def serialize_response( endpoint_ctx: Optional[EndpointContext] = None, ) -> Any: if field: - errors = [] if is_coroutine: - value, errors_ = field.validate(response_content, {}, loc=("response",)) + value, errors = field.validate(response_content, {}, loc=("response",)) else: - value, errors_ = await run_in_threadpool( + value, errors = await run_in_threadpool( field.validate, response_content, {}, loc=("response",) ) - if isinstance(errors_, list): - errors.extend(errors_) if errors: ctx = endpoint_ctx or EndpointContext() raise ResponseValidationError( From cc6ced6345f6be18038437b73866294697294f3e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Feb 2026 19:04:48 +0000 Subject: [PATCH 243/243] =?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 d442ef9a1..b9982e8c9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ♻️ Simplify internals, remove Pydantic v1 only logic, no longer needed. PR [#14857](https://github.com/fastapi/fastapi/pull/14857) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor internals, cleanup unneeded Pydantic v1 specific logic. PR [#14856](https://github.com/fastapi/fastapi/pull/14856) by [@tiangolo](https://github.com/tiangolo). ### Translations