diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index a5761361d..f78b6730e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -54,7 +54,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -96,7 +96,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -118,7 +118,7 @@ jobs: path: docs/${{ matrix.lang }}/.cache - name: Build Docs run: python ./scripts/docs.py build-lang ${{ matrix.lang }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: docs-site-${{ matrix.lang }} path: ./site/** diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml index ee8bfafb4..7d5449c6a 100644 --- a/.github/workflows/contributors.yml +++ b/.github/workflows/contributors.yml @@ -30,7 +30,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 2c432da8c..aa4fd6b65 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -29,7 +29,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -49,7 +49,7 @@ jobs: run: | rm -rf ./site mkdir ./site - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: path: ./site/ pattern: docs-site-* diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index b587d15e6..f40ec4dc4 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -38,7 +38,11 @@ jobs: }, "waiting": { "delay": 2628000, - "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." + "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR.", + "reminder": { + "before": "P3D", + "message": "Heads-up: this will be closed in 3 days unless there’s new activity." + } }, "invalid": { "delay": 0, diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index 76ac77298..e6ae3d963 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -26,7 +26,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index ef3990d31..04beeb64e 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -34,7 +34,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index e6e56bf04..f15b92137 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -30,7 +30,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index cde0ca308..eed5fbec0 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -26,7 +26,7 @@ jobs: with: python-version: '3.9' - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -34,7 +34,7 @@ jobs: requirements**.txt pyproject.toml - run: uv pip install -r requirements-github-actions.txt - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: coverage-html path: htmlcov diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 1e245346d..7d29469a5 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -30,7 +30,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b76afe01e..9c3e2218b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -48,6 +48,7 @@ jobs: strategy: matrix: python-version: + - "3.14" - "3.13" - "3.12" - "3.11" @@ -55,6 +56,9 @@ jobs: - "3.9" - "3.8" pydantic-version: ["pydantic-v1", "pydantic-v2"] + exclude: + - python-version: "3.14" + pydantic-version: "pydantic-v1" fail-fast: false steps: - name: Dump GitHub context @@ -67,7 +71,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -93,7 +97,7 @@ jobs: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - name: Store coverage files - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage @@ -112,7 +116,7 @@ jobs: with: python-version: '3.8' - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -122,7 +126,7 @@ jobs: - name: Install Dependencies run: uv pip install -r requirements-tests.txt - name: Get coverage files - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: pattern: coverage-* path: coverage @@ -132,7 +136,7 @@ jobs: - run: coverage report - run: coverage html --title "Coverage for ${{ github.sha }}" - name: Store coverage HTML - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: coverage-html path: htmlcov diff --git a/.github/workflows/topic-repos.yml b/.github/workflows/topic-repos.yml index cb98698d3..22b37d59d 100644 --- a/.github/workflows/topic-repos.yml +++ b/.github/workflows/topic-repos.yml @@ -25,7 +25,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index fa4e8f463..a7fcf84df 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -48,7 +48,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d9ab333ad..25dcd7b88 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.2 + rev: v0.14.2 hooks: - id: ruff args: diff --git a/README.md b/README.md index a8a0e37b5..09cd38da1 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ The key features are: + @@ -125,7 +126,7 @@ If you are building a CLI app to be FastAPI stands on the shoulders of giants: -* Starlette for the web parts. +* Starlette for the web parts. * Pydantic for the data parts. ## Installation @@ -231,7 +232,7 @@ INFO: Application startup complete.
About the command fastapi dev main.py... -The command `fastapi dev` reads your `main.py` file, detects the **FastAPI** app in it, and starts a server using Uvicorn. +The command `fastapi dev` reads your `main.py` file, detects the **FastAPI** app in it, and starts a server using Uvicorn. By default, `fastapi dev` will start with auto-reload enabled for local development. @@ -472,7 +473,7 @@ Used by Starlette: Used by FastAPI: -* uvicorn - for the server that loads and serves your application. This includes `uvicorn[standard]`, which includes some dependencies (e.g. `uvloop`) needed for high performance serving. +* uvicorn - for the server that loads and serves your application. This includes `uvicorn[standard]`, which includes some dependencies (e.g. `uvloop`) needed for high performance serving. * `fastapi-cli[standard]` - to provide the `fastapi` command. * This includes `fastapi-cloud-cli`, which allows you to deploy your FastAPI application to FastAPI Cloud. diff --git a/docs/de/docs/_llm-test.md b/docs/de/docs/_llm-test.md index 4a5e5392c..72846ef06 100644 --- a/docs/de/docs/_llm-test.md +++ b/docs/de/docs/_llm-test.md @@ -47,7 +47,7 @@ Das LLM wird dies wahrscheinlich falsch übersetzen. Interessant ist nur, ob es //// tab | Info -Der Prompt-Designer kann entscheiden, ob neutrale Anführungszeichen in typografische Anführungszeichen umgewandelt werden sollen. Es ist in Ordnung, sie unverändert zu lassen. +Der Promptdesigner kann entscheiden, ob neutrale Anführungszeichen in typografische Anführungszeichen umgewandelt werden sollen. Es ist in Ordnung, sie unverändert zu lassen. Siehe zum Beispiel den Abschnitt `### Quotes` in `docs/de/llm-prompt.md`. @@ -459,7 +459,7 @@ Für einige sprachspezifische Anweisungen, siehe z. B. den Abschnitt `### Headin * der Commit * der Contextmanager * die Coroutine -* die Datenbank-Session +* die Datenbanksession * die Festplatte * die Domain * die Engine @@ -496,7 +496,7 @@ Für einige sprachspezifische Anweisungen, siehe z. B. den Abschnitt `### Headin //// tab | Info -Dies ist eine nicht vollständige und nicht normative Liste von (meist) technischen Begriffen, die in der Dokumentation vorkommen. Sie kann dem Prompt-Designer helfen herauszufinden, bei welchen Begriffen das LLM Unterstützung braucht. Zum Beispiel, wenn es eine gute Übersetzung immer wieder auf eine suboptimale Übersetzung zurücksetzt. Oder wenn es Probleme hat, einen Begriff in Ihrer Sprache zu konjugieren/deklinieren. +Dies ist eine nicht vollständige und nicht normative Liste von (meist) technischen Begriffen, die in der Dokumentation vorkommen. Sie kann dem Promptdesigner helfen herauszufinden, bei welchen Begriffen das LLM Unterstützung braucht. Zum Beispiel, wenn es eine gute Übersetzung immer wieder auf eine suboptimale Übersetzung zurücksetzt. Oder wenn es Probleme hat, einen Begriff in Ihrer Sprache zu konjugieren/deklinieren. Siehe z. B. den Abschnitt `### List of English terms and their preferred German translations` in `docs/de/llm-prompt.md`. diff --git a/docs/de/docs/advanced/events.md b/docs/de/docs/advanced/events.md index 2ceef1190..f94526b4f 100644 --- a/docs/de/docs/advanced/events.md +++ b/docs/de/docs/advanced/events.md @@ -154,7 +154,7 @@ In der technischen ASGI-Spezifikation ist dies Teil des Starlettes Lifespan-Dokumentation. +Weitere Informationen zu Starlettes `lifespan`-Handlern finden Sie in Starlettes Lifespan-Dokumentation. Einschließlich, wie man Lifespan-Zustand handhabt, der in anderen Bereichen Ihres Codes verwendet werden kann. diff --git a/docs/de/docs/advanced/middleware.md b/docs/de/docs/advanced/middleware.md index 0a2a39699..8396a626b 100644 --- a/docs/de/docs/advanced/middleware.md +++ b/docs/de/docs/advanced/middleware.md @@ -94,4 +94,4 @@ Zum Beispiel: * Uvicorns `ProxyHeadersMiddleware` * MessagePack -Um mehr über weitere verfügbare Middlewares herauszufinden, besuchen Sie Starlettes Middleware-Dokumentation und die ASGI Awesome List. +Um mehr über weitere verfügbare Middlewares herauszufinden, besuchen Sie Starlettes Middleware-Dokumentation und die ASGI Awesome List. diff --git a/docs/de/docs/advanced/response-cookies.md b/docs/de/docs/advanced/response-cookies.md index 0dd4175dd..02fe99c26 100644 --- a/docs/de/docs/advanced/response-cookies.md +++ b/docs/de/docs/advanced/response-cookies.md @@ -48,4 +48,4 @@ Und da die `Response` häufig zum Setzen von Headern und Cookies verwendet wird, /// -Um alle verfügbaren Parameter und Optionen anzuzeigen, sehen Sie sich deren Dokumentation in Starlette an. +Um alle verfügbaren Parameter und Optionen anzuzeigen, sehen Sie sich deren Dokumentation in Starlette an. diff --git a/docs/de/docs/advanced/response-headers.md b/docs/de/docs/advanced/response-headers.md index a5e310d55..1dc7c0691 100644 --- a/docs/de/docs/advanced/response-headers.md +++ b/docs/de/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ Und da die `Response` häufig zum Setzen von Headern und Cookies verwendet wird, Beachten Sie, dass benutzerdefinierte proprietäre Header mittels des Präfix `X-` hinzugefügt werden können. -Wenn Sie jedoch benutzerdefinierte Header haben, die ein Client in einem Browser sehen können soll, müssen Sie diese zu Ihrer CORS-Konfiguration hinzufügen (weitere Informationen finden Sie unter [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), unter Verwendung des Parameters `expose_headers`, dokumentiert in Starlettes CORS-Dokumentation. +Wenn Sie jedoch benutzerdefinierte Header haben, die ein Client in einem Browser sehen können soll, müssen Sie diese zu Ihrer CORS-Konfiguration hinzufügen (weitere Informationen finden Sie unter [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), unter Verwendung des Parameters `expose_headers`, dokumentiert in Starlettes CORS-Dokumentation. diff --git a/docs/de/docs/advanced/templates.md b/docs/de/docs/advanced/templates.md index fdaeb3413..65c7998b8 100644 --- a/docs/de/docs/advanced/templates.md +++ b/docs/de/docs/advanced/templates.md @@ -123,4 +123,4 @@ Und da Sie `StaticFiles` verwenden, wird diese CSS-Datei automatisch von Ihrer * ## Mehr Details { #more-details } -Weitere Informationen, einschließlich, wie man Templates testet, finden Sie in Starlettes Dokumentation zu Templates. +Weitere Informationen, einschließlich, wie man Templates testet, finden Sie in Starlettes Dokumentation zu Templates. diff --git a/docs/de/docs/advanced/testing-events.md b/docs/de/docs/advanced/testing-events.md index 1a68b7714..569518c51 100644 --- a/docs/de/docs/advanced/testing-events.md +++ b/docs/de/docs/advanced/testing-events.md @@ -5,7 +5,7 @@ Wenn Sie `lifespan` in Ihren Tests ausführen müssen, können Sie den `TestClie {* ../../docs_src/app_testing/tutorial004.py hl[9:15,18,27:28,30:32,41:43] *} -Sie können mehr Details unter [„Lifespan in Tests ausführen in der offiziellen Starlette-Dokumentation.“](https://www.starlette.io/lifespan/#running-lifespan-in-tests) nachlesen. +Sie können mehr Details unter [„Lifespan in Tests ausführen in der offiziellen Starlette-Dokumentation.“](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) nachlesen. Für die deprecateten Events `startup` und `shutdown` können Sie den `TestClient` wie folgt verwenden: diff --git a/docs/de/docs/advanced/testing-websockets.md b/docs/de/docs/advanced/testing-websockets.md index a71310cbf..f25aa4fd0 100644 --- a/docs/de/docs/advanced/testing-websockets.md +++ b/docs/de/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ Dazu verwenden Sie den `TestClient` in einer `with`-Anweisung, eine Verbindung z /// note | Hinweis -Weitere Informationen finden Sie in Starlettes Dokumentation zum Testen von WebSockets. +Weitere Informationen finden Sie in Starlettes Dokumentation zum Testen von WebSockets. /// diff --git a/docs/de/docs/advanced/using-request-directly.md b/docs/de/docs/advanced/using-request-directly.md index 7782237ec..8ec6741d0 100644 --- a/docs/de/docs/advanced/using-request-directly.md +++ b/docs/de/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ Es gibt jedoch Situationen, in denen Sie möglicherweise direkt auf das `Request ## Details zum `Request`-Objekt { #details-about-the-request-object } -Da **FastAPI** unter der Haube eigentlich **Starlette** ist, mit einer Ebene von mehreren Tools darüber, können Sie Starlettes `Request`-Objekt direkt verwenden, wenn Sie es benötigen. +Da **FastAPI** unter der Haube eigentlich **Starlette** ist, mit einer Ebene von mehreren Tools darüber, können Sie Starlettes `Request`-Objekt direkt verwenden, wenn Sie es benötigen. Das bedeutet allerdings auch, dass, wenn Sie Daten direkt vom `Request`-Objekt nehmen (z. B. dessen Body lesen), diese von FastAPI nicht validiert, konvertiert oder dokumentiert werden (mit OpenAPI, für die automatische API-Benutzeroberfläche). @@ -45,7 +45,7 @@ Auf die gleiche Weise können Sie wie gewohnt jeden anderen Parameter deklariere ## `Request`-Dokumentation { #request-documentation } -Weitere Details zum `Request`-Objekt finden Sie in der offiziellen Starlette-Dokumentation. +Weitere Details zum `Request`-Objekt finden Sie in der offiziellen Starlette-Dokumentation. /// note | Technische Details diff --git a/docs/de/docs/advanced/websockets.md b/docs/de/docs/advanced/websockets.md index ad1f6f5b1..5f662770f 100644 --- a/docs/de/docs/advanced/websockets.md +++ b/docs/de/docs/advanced/websockets.md @@ -182,5 +182,5 @@ Wenn Sie etwas benötigen, das sich leicht in FastAPI integrieren lässt, aber r Weitere Informationen zu Optionen finden Sie in der Dokumentation von Starlette: -* Die `WebSocket`-Klasse. -* Klassen-basierte Handhabung von WebSockets. +* Die `WebSocket`-Klasse. +* Klassen-basierte Handhabung von WebSockets. diff --git a/docs/de/docs/alternatives.md b/docs/de/docs/alternatives.md index 15c0719fb..4dd127dba 100644 --- a/docs/de/docs/alternatives.md +++ b/docs/de/docs/alternatives.md @@ -417,7 +417,7 @@ Die gesamte Datenvalidierung, Datenserialisierung und automatische Modelldokumen /// -### Starlette { #starlette } +### Starlette { #starlette } Starlette ist ein leichtgewichtiges ASGI-Framework/Toolkit, welches sich ideal für die Erstellung hochperformanter asynchroner Dienste eignet. @@ -462,7 +462,7 @@ Alles, was Sie also mit Starlette machen können, können Sie direkt mit **FastA /// -### Uvicorn { #uvicorn } +### Uvicorn { #uvicorn } Uvicorn ist ein blitzschneller ASGI-Server, der auf uvloop und httptools basiert. diff --git a/docs/de/docs/deployment/manually.md b/docs/de/docs/deployment/manually.md index 6393f8ebc..2de2913a5 100644 --- a/docs/de/docs/deployment/manually.md +++ b/docs/de/docs/deployment/manually.md @@ -52,7 +52,7 @@ Das Wichtigste, was Sie benötigen, um eine **FastAPI**-Anwendung (oder eine and Es gibt mehrere Alternativen, einschließlich: -* Uvicorn: ein hochperformanter ASGI-Server. +* Uvicorn: ein hochperformanter ASGI-Server. * Hypercorn: ein ASGI-Server, der unter anderem kompatibel mit HTTP/2 und Trio ist. * Daphne: der für Django Channels entwickelte ASGI-Server. * Granian: Ein Rust HTTP-Server für Python-Anwendungen. diff --git a/docs/de/docs/fastapi-cli.md b/docs/de/docs/fastapi-cli.md index d41ed598e..ab9c8373e 100644 --- a/docs/de/docs/fastapi-cli.md +++ b/docs/de/docs/fastapi-cli.md @@ -52,7 +52,7 @@ FastAPI CLI nimmt den Pfad zu Ihrem Python-Programm (z. B. `main.py`), erkennt a Für die Produktion würden Sie stattdessen `fastapi run` verwenden. 🚀 -Intern verwendet das **FastAPI CLI** Uvicorn, einen leistungsstarken, produktionsreifen, ASGI-Server. 😎 +Intern verwendet das **FastAPI CLI** Uvicorn, einen leistungsstarken, produktionsreifen, ASGI-Server. 😎 ## `fastapi dev` { #fastapi-dev } diff --git a/docs/de/docs/features.md b/docs/de/docs/features.md index c52f6733e..0b51e9737 100644 --- a/docs/de/docs/features.md +++ b/docs/de/docs/features.md @@ -159,7 +159,7 @@ Jede Integration wurde so entworfen, dass sie so einfach zu nutzen ist (mit Abh ## Starlette Merkmale { #starlette-features } -**FastAPI** ist vollkommen kompatibel (und basiert auf) Starlette. Das bedeutet, wenn Sie eigenen Starlette Quellcode haben, funktioniert der. +**FastAPI** ist vollkommen kompatibel (und basiert auf) Starlette. Das bedeutet, wenn Sie eigenen Starlette Quellcode haben, funktioniert der. `FastAPI` ist tatsächlich eine Unterklasse von `Starlette`. Wenn Sie also bereits Starlette kennen oder benutzen, das meiste funktioniert genau so. diff --git a/docs/de/docs/history-design-future.md b/docs/de/docs/history-design-future.md index 40a7a8286..45198ff1c 100644 --- a/docs/de/docs/history-design-future.md +++ b/docs/de/docs/history-design-future.md @@ -58,7 +58,7 @@ Nachdem ich mehrere Alternativen getestet hatte, entschied ich, dass ich **Starlette** beigetragen, der anderen Schlüsselanforderung. +Während der Entwicklung habe ich auch zu **Starlette** beigetragen, der anderen Schlüsselanforderung. ## Entwicklung { #development } diff --git a/docs/de/docs/how-to/custom-request-and-route.md b/docs/de/docs/how-to/custom-request-and-route.md index 41a85f832..246717c04 100644 --- a/docs/de/docs/how-to/custom-request-and-route.md +++ b/docs/de/docs/how-to/custom-request-and-route.md @@ -66,7 +66,7 @@ Das `scope`-`dict` und die `receive`-Funktion sind beide Teil der ASGI-Spezifika Und diese beiden Dinge, `scope` und `receive`, werden benötigt, um eine neue `Request`-Instanz zu erstellen. -Um mehr über den `Request` zu erfahren, schauen Sie sich Starlettes Dokumentation zu Requests an. +Um mehr über den `Request` zu erfahren, schauen Sie sich Starlettes Dokumentation zu Requests an. /// diff --git a/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md new file mode 100644 index 000000000..7f60492ee --- /dev/null +++ b/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -0,0 +1,133 @@ +# Von Pydantic v1 zu Pydantic v2 migrieren { #migrate-from-pydantic-v1-to-pydantic-v2 } + +Wenn Sie eine ältere FastAPI-App haben, nutzen Sie möglicherweise Pydantic Version 1. + +FastAPI unterstützt seit Version 0.100.0 sowohl Pydantic v1 als auch v2. + +Wenn Sie Pydantic v2 installiert hatten, wurde dieses verwendet. Wenn stattdessen Pydantic v1 installiert war, wurde jenes verwendet. + +Pydantic v1 ist jetzt deprecatet und die Unterstützung dafür wird in den nächsten Versionen von FastAPI entfernt, Sie sollten also zu **Pydantic v2 migrieren**. Auf diese Weise erhalten Sie die neuesten Features, Verbesserungen und Fixes. + +/// warning | Achtung + +Außerdem hat das Pydantic-Team die Unterstützung für Pydantic v1 in den neuesten Python-Versionen eingestellt, beginnend mit **Python 3.14**. + +Wenn Sie die neuesten Features von Python nutzen möchten, müssen Sie sicherstellen, dass Sie Pydantic v2 verwenden. + +/// + +Wenn Sie eine ältere FastAPI-App mit Pydantic v1 haben, zeige ich Ihnen hier, wie Sie sie zu Pydantic v2 migrieren, und die **neuen Features in FastAPI 0.119.0**, die Ihnen bei einer schrittweisen Migration helfen. + +## Offizieller Leitfaden { #official-guide } + +Pydantic hat einen offiziellen Migrationsleitfaden von v1 zu v2. + +Er enthält auch, was sich geändert hat, wie Validierungen nun korrekter und strikter sind, mögliche Stolpersteine, usw. + +Sie können ihn lesen, um besser zu verstehen, was sich geändert hat. + +## Tests { #tests } + +Stellen Sie sicher, dass Sie [Tests](../tutorial/testing.md){.internal-link target=_blank} für Ihre App haben und diese in Continuous Integration (CI) ausführen. + +Auf diese Weise können Sie das Update durchführen und sicherstellen, dass weiterhin alles wie erwartet funktioniert. + +## `bump-pydantic` { #bump-pydantic } + +In vielen Fällen, wenn Sie reguläre Pydantic-Modelle ohne Anpassungen verwenden, können Sie den Großteil des Prozesses der Migration von Pydantic v1 auf Pydantic v2 automatisieren. + +Sie können `bump-pydantic` vom selben Pydantic-Team verwenden. + +Dieses Tool hilft Ihnen, den Großteil des zu ändernden Codes automatisch anzupassen. + +Danach können Sie die Tests ausführen und prüfen, ob alles funktioniert. Falls ja, sind Sie fertig. 😎 + +## Pydantic v1 in v2 { #pydantic-v1-in-v2 } + +Pydantic v2 enthält alles aus Pydantic v1 als Untermodul `pydantic.v1`. + +Das bedeutet, Sie können die neueste Version von Pydantic v2 installieren und die alten Pydantic‑v1‑Komponenten aus diesem Untermodul importieren und verwenden, als hätten Sie das alte Pydantic v1 installiert. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} + +### FastAPI-Unterstützung für Pydantic v1 in v2 { #fastapi-support-for-pydantic-v1-in-v2 } + +Seit FastAPI 0.119.0 gibt es außerdem eine teilweise Unterstützung für Pydantic v1 innerhalb von Pydantic v2, um die Migration auf v2 zu erleichtern. + +Sie könnten also Pydantic auf die neueste Version 2 aktualisieren und die Importe so ändern, dass das Untermodul `pydantic.v1` verwendet wird, und in vielen Fällen würde es einfach funktionieren. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} + +/// warning | Achtung + +Beachten Sie, dass, da das Pydantic‑Team Pydantic v1 in neueren Python‑Versionen nicht mehr unterstützt, beginnend mit Python 3.14, auch die Verwendung von `pydantic.v1` unter Python 3.14 und höher nicht unterstützt wird. + +/// + +### Pydantic v1 und v2 in derselben App { #pydantic-v1-and-v2-on-the-same-app } + +Es wird von Pydantic **nicht unterstützt**, dass ein Pydantic‑v2‑Modell Felder hat, die als Pydantic‑v1‑Modelle definiert sind, und umgekehrt. + +```mermaid +graph TB + subgraph "❌ Nicht unterstützt" + direction TB + subgraph V2["Pydantic-v2-Modell"] + V1Field["Pydantic-v1-Modell"] + end + subgraph V1["Pydantic-v1-Modell"] + V2Field["Pydantic-v2-Modell"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +... aber Sie können getrennte Modelle, die Pydantic v1 bzw. v2 nutzen, in derselben App verwenden. + +```mermaid +graph TB + subgraph "✅ Unterstützt" + direction TB + subgraph V2["Pydantic-v2-Modell"] + V2Field["Pydantic-v2-Modell"] + end + subgraph V1["Pydantic-v1-Modell"] + V1Field["Pydantic-v1-Modell"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +In einigen Fällen ist es sogar möglich, sowohl Pydantic‑v1‑ als auch Pydantic‑v2‑Modelle in derselben **Pfadoperation** Ihrer FastAPI‑App zu verwenden: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} + +Im obigen Beispiel ist das Eingabemodell ein Pydantic‑v1‑Modell, und das Ausgabemodell (definiert in `response_model=ItemV2`) ist ein Pydantic‑v2‑Modell. + +### Pydantic v1 Parameter { #pydantic-v1-parameters } + +Wenn Sie einige der FastAPI-spezifischen Tools für Parameter wie `Body`, `Query`, `Form`, usw. zusammen mit Pydantic‑v1‑Modellen verwenden müssen, können Sie die aus `fastapi.temp_pydantic_v1_params` importieren, während Sie die Migration zu Pydantic v2 abschließen: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} + +### In Schritten migrieren { #migrate-in-steps } + +/// tip | Tipp + +Probieren Sie zuerst `bump-pydantic` aus. Wenn Ihre Tests erfolgreich sind und das funktioniert, sind Sie mit einem einzigen Befehl fertig. ✨ + +/// + +Wenn `bump-pydantic` für Ihren Anwendungsfall nicht funktioniert, können Sie die Unterstützung für Pydantic‑v1‑ und Pydantic‑v2‑Modelle in derselben App nutzen, um die Migration zu Pydantic v2 schrittweise durchzuführen. + +Sie könnten zuerst Pydantic auf die neueste Version 2 aktualisieren und die Importe so ändern, dass für all Ihre Modelle `pydantic.v1` verwendet wird. + +Anschließend können Sie beginnen, Ihre Modelle gruppenweise von Pydantic v1 auf v2 zu migrieren – in kleinen, schrittweisen Etappen. 🚶 diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index edcb61b94..4be65071b 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -123,7 +123,7 @@ Wenn Sie eine Starlette für die Webanteile. +* Starlette für die Webanteile. * Pydantic für die Datenanteile. ## Installation { #installation } @@ -229,7 +229,7 @@ INFO: Application startup complete.
Was der Befehl fastapi dev main.py macht ... -Der Befehl `fastapi dev` liest Ihre `main.py`-Datei, erkennt die **FastAPI**-App darin und startet einen Server mit Uvicorn. +Der Befehl `fastapi dev` liest Ihre `main.py`-Datei, erkennt die **FastAPI**-App darin und startet einen Server mit Uvicorn. Standardmäßig wird `fastapi dev` mit aktiviertem Auto-Reload für die lokale Entwicklung gestartet. @@ -470,7 +470,7 @@ Verwendet von Starlette: Verwendet von FastAPI: -* uvicorn – für den Server, der Ihre Anwendung lädt und bereitstellt. Dies umfasst `uvicorn[standard]`, das einige Abhängigkeiten (z. B. `uvloop`) beinhaltet, die für eine Bereitstellung mit hoher Performanz benötigt werden. +* uvicorn – für den Server, der Ihre Anwendung lädt und bereitstellt. Dies umfasst `uvicorn[standard]`, das einige Abhängigkeiten (z. B. `uvloop`) beinhaltet, die für eine Bereitstellung mit hoher Performanz benötigt werden. * `fastapi-cli[standard]` – um den `fastapi`-Befehl bereitzustellen. * Dies beinhaltet `fastapi-cloud-cli`, das es Ihnen ermöglicht, Ihre FastAPI-Anwendung auf FastAPI Cloud bereitzustellen. diff --git a/docs/de/docs/tutorial/background-tasks.md b/docs/de/docs/tutorial/background-tasks.md index ea85207ce..2c381ccfa 100644 --- a/docs/de/docs/tutorial/background-tasks.md +++ b/docs/de/docs/tutorial/background-tasks.md @@ -63,7 +63,7 @@ Und dann schreibt ein weiterer Hintergrundtask, der in der *Pfadoperation-Funkti ## Technische Details { #technical-details } -Die Klasse `BackgroundTasks` stammt direkt von `starlette.background`. +Die Klasse `BackgroundTasks` stammt direkt von `starlette.background`. Sie wird direkt in FastAPI importiert/inkludiert, sodass Sie sie von `fastapi` importieren können und vermeiden, versehentlich das alternative `BackgroundTask` (ohne das `s` am Ende) von `starlette.background` zu importieren. @@ -71,7 +71,7 @@ Indem Sie nur `BackgroundTasks` (und nicht `BackgroundTask`) verwenden, ist es d Es ist immer noch möglich, `BackgroundTask` allein in FastAPI zu verwenden, aber Sie müssen das Objekt in Ihrem Code erstellen und eine Starlette-`Response` zurückgeben, die es enthält. -Weitere Details finden Sie in Starlettes offizieller Dokumentation für Hintergrundtasks. +Weitere Details finden Sie in Starlettes offizieller Dokumentation für Hintergrundtasks. ## Vorbehalt { #caveat } diff --git a/docs/de/docs/tutorial/first-steps.md b/docs/de/docs/tutorial/first-steps.md index 374127c17..7ec98c53b 100644 --- a/docs/de/docs/tutorial/first-steps.md +++ b/docs/de/docs/tutorial/first-steps.md @@ -155,7 +155,7 @@ Ebenfalls können Sie es verwenden, um automatisch Code für Clients zu generier `FastAPI` ist eine Klasse, die direkt von `Starlette` erbt. -Sie können alle Starlette-Funktionalitäten auch mit `FastAPI` nutzen. +Sie können alle Starlette-Funktionalitäten auch mit `FastAPI` nutzen. /// diff --git a/docs/de/docs/tutorial/handling-errors.md b/docs/de/docs/tutorial/handling-errors.md index 51294f44f..58e4607c5 100644 --- a/docs/de/docs/tutorial/handling-errors.md +++ b/docs/de/docs/tutorial/handling-errors.md @@ -81,7 +81,7 @@ Aber falls Sie es für ein fortgeschrittenes Szenario benötigen, können Sie be ## Benutzerdefinierte Exceptionhandler installieren { #install-custom-exception-handlers } -Sie können benutzerdefinierte Exceptionhandler mit den gleichen Exception-Werkzeugen von Starlette hinzufügen. +Sie können benutzerdefinierte Exceptionhandler mit den gleichen Exception-Werkzeugen von Starlette hinzufügen. Angenommen, Sie haben eine benutzerdefinierte Exception `UnicornException`, die Sie (oder eine Bibliothek, die Sie verwenden) `raise`n könnten. diff --git a/docs/de/docs/tutorial/middleware.md b/docs/de/docs/tutorial/middleware.md index a1e2ba9df..6410deba1 100644 --- a/docs/de/docs/tutorial/middleware.md +++ b/docs/de/docs/tutorial/middleware.md @@ -37,7 +37,7 @@ Die Middleware-Funktion erhält: Beachten Sie, dass benutzerdefinierte proprietäre Header hinzugefügt werden können unter Verwendung des `X-`-Präfixes. -Wenn Sie jedoch benutzerdefinierte Header haben, die ein Client in einem Browser sehen soll, müssen Sie sie zu Ihrer CORS-Konfiguration ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) hinzufügen, indem Sie den Parameter `expose_headers` verwenden, der in der Starlettes CORS-Dokumentation dokumentiert ist. +Wenn Sie jedoch benutzerdefinierte Header haben, die ein Client in einem Browser sehen soll, müssen Sie sie zu Ihrer CORS-Konfiguration ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) hinzufügen, indem Sie den Parameter `expose_headers` verwenden, der in der Starlettes CORS-Dokumentation dokumentiert ist. /// diff --git a/docs/de/docs/tutorial/static-files.md b/docs/de/docs/tutorial/static-files.md index 5a6cfcb2b..0c4e7c8ab 100644 --- a/docs/de/docs/tutorial/static-files.md +++ b/docs/de/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ Alle diese Parameter können anders als „`static`“ lauten, passen Sie sie an ## Weitere Informationen { #more-info } -Weitere Details und Optionen finden Sie in der Dokumentation von Starlette zu statischen Dateien. +Weitere Details und Optionen finden Sie in der Dokumentation von Starlette zu statischen Dateien. diff --git a/docs/de/docs/tutorial/testing.md b/docs/de/docs/tutorial/testing.md index 75ee9fade..9c28a2a22 100644 --- a/docs/de/docs/tutorial/testing.md +++ b/docs/de/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # Testen { #testing } -Dank Starlette ist das Testen von **FastAPI**-Anwendungen einfach und macht Spaß. +Dank Starlette ist das Testen von **FastAPI**-Anwendungen einfach und macht Spaß. Es basiert auf HTTPX, welches wiederum auf der Grundlage von Requests konzipiert wurde, es ist also sehr vertraut und intuitiv. diff --git a/docs/de/llm-prompt.md b/docs/de/llm-prompt.md index 23c111d2d..df202d2ff 100644 --- a/docs/de/llm-prompt.md +++ b/docs/de/llm-prompt.md @@ -185,7 +185,7 @@ Example: # FastAPI in Containern - Docker { #fastapi-in-containers-docker } »»» -3.1) Do not apply rule 3 when there is no space before or no space after the dash. +3.1) Do not apply rule 3 when there is no space before or no space after the hyphen. Example: @@ -195,13 +195,13 @@ Example: ## Type hints and annotations { #type-hints-and-annotations } »»» - Translate with (German) – use a short dash: + Translate with (German) – notice the hyphen: ««« ## Typhinweise und -annotationen { #type-hints-and-annotations } »»» - Do NOT translate with (German): + Do NOT translate with (German) – notice the dash: ««« ## Typhinweise und –annotationen { #type-hints-and-annotations } @@ -222,7 +222,7 @@ Ich versuche nicht, alles einzudeutschen. Das bezieht sich besonders auf Begriff ### List of English terms and their preferred German translations -Below is a list of English terms and their preferred German translations, separated by a colon («:»). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them. A term or a translation may be followed by an explanation in brackets, which explains when to translate the term this way. If a translation is preceded by «NOT», then that means: do NOT use this translation for this term. English nouns, starting with the word «the», have the German genus – «der», «die», «das» – prepended to their German translation, to help you to grammatically decline them in the translation. They are given in singular case, unless they have «(plural)» attached, which means they are given in plural case. Verbs are given in the full infinitive – starting with the word «to». +Below is a list of English terms and their preferred German translations, separated by a colon («:»). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them. In the below list, a term or a translation may be followed by an explanation in brackets, which explains when to translate the term this way. If a translation is preceded by «NOT», then that means: do NOT use this translation for this term. English nouns, starting with the word «the», have the German genus – «der», «die», «das» – prepended to their German translation, to help you to grammatically decline them in the translation. They are given in singular case, unless they have «(plural)» attached, which means they are given in plural case. Verbs are given in the full infinitive – starting with the word «to». * «/// check»: «/// check | Testen» * «/// danger»: «/// danger | Gefahr» diff --git a/docs/em/docs/advanced/events.md b/docs/em/docs/advanced/events.md index 68adb6d65..dcaac710e 100644 --- a/docs/em/docs/advanced/events.md +++ b/docs/em/docs/advanced/events.md @@ -140,7 +140,7 @@ async with lifespan(app): /// info -👆 💪 ✍ 🌅 🔃 👫 🎉 🐕‍🦺 💃 🎉' 🩺. +👆 💪 ✍ 🌅 🔃 👫 🎉 🐕‍🦺 💃 🎉' 🩺. /// diff --git a/docs/em/docs/advanced/middleware.md b/docs/em/docs/advanced/middleware.md index cb04fa3fb..22d707062 100644 --- a/docs/em/docs/advanced/middleware.md +++ b/docs/em/docs/advanced/middleware.md @@ -92,4 +92,4 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") * Uvicorn `ProxyHeadersMiddleware` * 🇸🇲 -👀 🎏 💪 🛠️ ✅ 💃 🛠️ 🩺 & 🔫 👌 📇. +👀 🎏 💪 🛠️ ✅ 💃 🛠️ 🩺 & 🔫 👌 📇. diff --git a/docs/em/docs/advanced/response-cookies.md b/docs/em/docs/advanced/response-cookies.md index d9fdbaa87..a6e37ad74 100644 --- a/docs/em/docs/advanced/response-cookies.md +++ b/docs/em/docs/advanced/response-cookies.md @@ -48,4 +48,4 @@ /// -👀 🌐 💪 🔢 & 🎛, ✅ 🧾 💃. +👀 🌐 💪 🔢 & 🎛, ✅ 🧾 💃. diff --git a/docs/em/docs/advanced/response-headers.md b/docs/em/docs/advanced/response-headers.md index e9e1b62d2..c255380d6 100644 --- a/docs/em/docs/advanced/response-headers.md +++ b/docs/em/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ ✔️ 🤯 👈 🛃 © 🎚 💪 🚮 ⚙️ '✖-' 🔡. -✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 (✍ 🌅 [⚜ (✖️-🇨🇳 ℹ 🤝)](../tutorial/cors.md){.internal-link target=_blank}), ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. +✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 (✍ 🌅 [⚜ (✖️-🇨🇳 ℹ 🤝)](../tutorial/cors.md){.internal-link target=_blank}), ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. diff --git a/docs/em/docs/advanced/templates.md b/docs/em/docs/advanced/templates.md index ad4d4fc71..2e8f56228 100644 --- a/docs/em/docs/advanced/templates.md +++ b/docs/em/docs/advanced/templates.md @@ -81,4 +81,4 @@ $ pip install jinja2 ## 🌅 ℹ -🌅 ℹ, 🔌 ❔ 💯 📄, ✅ 💃 🩺 🔛 📄. +🌅 ℹ, 🔌 ❔ 💯 📄, ✅ 💃 🩺 🔛 📄. diff --git a/docs/em/docs/advanced/testing-websockets.md b/docs/em/docs/advanced/testing-websockets.md index 2a01de629..96aa8b765 100644 --- a/docs/em/docs/advanced/testing-websockets.md +++ b/docs/em/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ /// note -🌅 ℹ, ✅ 💃 🧾 🔬 *️⃣ . +🌅 ℹ, ✅ 💃 🧾 🔬 *️⃣ . /// diff --git a/docs/em/docs/advanced/using-request-directly.md b/docs/em/docs/advanced/using-request-directly.md index 9530d49bc..238557e5e 100644 --- a/docs/em/docs/advanced/using-request-directly.md +++ b/docs/em/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ ## ℹ 🔃 `Request` 🎚 -**FastAPI** 🤙 **💃** 🔘, ⏮️ 🧽 📚 🧰 🔛 🔝, 👆 💪 ⚙️ 💃 `Request` 🎚 🔗 🕐❔ 👆 💪. +**FastAPI** 🤙 **💃** 🔘, ⏮️ 🧽 📚 🧰 🔛 🔝, 👆 💪 ⚙️ 💃 `Request` 🎚 🔗 🕐❔ 👆 💪. ⚫️ 🔜 ⛓ 👈 🚥 👆 🤚 📊 ⚪️➡️ `Request` 🎚 🔗 (🖼, ✍ 💪) ⚫️ 🏆 🚫 ✔, 🗜 ⚖️ 📄 (⏮️ 🗄, 🏧 🛠️ 👩‍💻 🔢) FastAPI. @@ -45,7 +45,7 @@ ## `Request` 🧾 -👆 💪 ✍ 🌅 ℹ 🔃 `Request` 🎚 🛂 💃 🧾 🕸. +👆 💪 ✍ 🌅 ℹ 🔃 `Request` 🎚 🛂 💃 🧾 🕸. /// note | 📡 ℹ diff --git a/docs/em/docs/advanced/websockets.md b/docs/em/docs/advanced/websockets.md index cc6e5c5f0..a097778c7 100644 --- a/docs/em/docs/advanced/websockets.md +++ b/docs/em/docs/advanced/websockets.md @@ -182,5 +182,5 @@ Client #1596980209979 left the chat 💡 🌅 🔃 🎛, ✅ 💃 🧾: -* `WebSocket` 🎓. -* 🎓-⚓️ *️⃣ 🚚. +* `WebSocket` 🎓. +* 🎓-⚓️ *️⃣ 🚚. diff --git a/docs/em/docs/alternatives.md b/docs/em/docs/alternatives.md index 59b587285..4cbac7539 100644 --- a/docs/em/docs/alternatives.md +++ b/docs/em/docs/alternatives.md @@ -417,7 +417,7 @@ Pydantic 🗃 🔬 💽 🔬, 🛠️ & 🧾 (⚙️ 🎻 🔗) ⚓️ 🔛 /// -### 💃 +### 💃 💃 💿 🔫 🛠️/🧰, ❔ 💯 🏗 ↕-🎭 ✳ 🐕‍🦺. @@ -462,7 +462,7 @@ Pydantic 🗃 🔬 💽 🔬, 🛠️ & 🧾 (⚙️ 🎻 🔗) ⚓️ 🔛 /// -### Uvicorn +### Uvicorn Uvicorn 🌩-⏩ 🔫 💽, 🏗 🔛 uvloop & httptool. diff --git a/docs/em/docs/deployment/manually.md b/docs/em/docs/deployment/manually.md index 8ebe00c7c..4fa2d13e2 100644 --- a/docs/em/docs/deployment/manually.md +++ b/docs/em/docs/deployment/manually.md @@ -4,7 +4,7 @@ 📤 3️⃣ 👑 🎛: -* Uvicorn: ↕ 🎭 🔫 💽. +* Uvicorn: ↕ 🎭 🔫 💽. * Hypercorn: 🔫 💽 🔗 ⏮️ 🇺🇸🔍/2️⃣ & 🎻 👪 🎏 ⚒. * 👸: 🔫 💽 🏗 ✳ 📻. @@ -24,7 +24,7 @@ //// tab | Uvicorn -* Uvicorn, 🌩-⏩ 🔫 💽, 🏗 🔛 uvloop & httptool. +* Uvicorn, 🌩-⏩ 🔫 💽, 🏗 🔛 uvloop & httptool.
diff --git a/docs/em/docs/features.md b/docs/em/docs/features.md index 13cafa72f..ccbed0cae 100644 --- a/docs/em/docs/features.md +++ b/docs/em/docs/features.md @@ -159,7 +159,7 @@ FastAPI 🔌 📶 ⏩ ⚙️, ✋️ 📶 🏋️ . -✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 ([⚜ (✖️-🇨🇳 ℹ 🤝)](cors.md){.internal-link target=_blank}) ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. +✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 ([⚜ (✖️-🇨🇳 ℹ 🤝)](cors.md){.internal-link target=_blank}) ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. /// diff --git a/docs/em/docs/tutorial/static-files.md b/docs/em/docs/tutorial/static-files.md index 6ff6e37a9..27685c06d 100644 --- a/docs/em/docs/tutorial/static-files.md +++ b/docs/em/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ ## 🌅 ℹ -🌖 ℹ & 🎛 ✅ 💃 🩺 🔃 🎻 📁. +🌖 ℹ & 🎛 ✅ 💃 🩺 🔃 🎻 📁. diff --git a/docs/em/docs/tutorial/testing.md b/docs/em/docs/tutorial/testing.md index cb4a1ca21..2e4a531f7 100644 --- a/docs/em/docs/tutorial/testing.md +++ b/docs/em/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # 🔬 -👏 💃, 🔬 **FastAPI** 🈸 ⏩ & 😌. +👏 💃, 🔬 **FastAPI** 🈸 ⏩ & 😌. ⚫️ ⚓️ 🔛 🇸🇲, ❔ 🔄 🏗 ⚓️ 🔛 📨, ⚫️ 📶 😰 & 🏋️. diff --git a/docs/en/data/contributors.yml b/docs/en/data/contributors.yml index c892d8baf..592c79af0 100644 --- a/docs/en/data/contributors.yml +++ b/docs/en/data/contributors.yml @@ -1,11 +1,11 @@ tiangolo: login: tiangolo - count: 782 + count: 794 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo dependabot: login: dependabot - count: 117 + count: 126 avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 url: https://github.com/apps/dependabot alejsdev: @@ -15,7 +15,7 @@ alejsdev: url: https://github.com/alejsdev pre-commit-ci: login: pre-commit-ci - count: 45 + count: 49 avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 url: https://github.com/apps/pre-commit-ci github-actions: @@ -25,7 +25,7 @@ github-actions: url: https://github.com/apps/github-actions Kludex: login: Kludex - count: 24 + count: 25 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex dmontagu: @@ -33,36 +33,36 @@ dmontagu: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu +YuriiMotov: + login: YuriiMotov + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + url: https://github.com/YuriiMotov +nilslindemann: + login: nilslindemann + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 + url: https://github.com/nilslindemann euri10: login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -nilslindemann: - login: nilslindemann +svlandeg: + login: svlandeg count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 - url: https://github.com/nilslindemann + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg kantandane: login: kantandane count: 13 avatarUrl: https://avatars.githubusercontent.com/u/3978368?u=cccc199291f991a73b1ebba5abc735a948e0bd16&v=4 url: https://github.com/kantandane -svlandeg: - login: svlandeg - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 - url: https://github.com/svlandeg zhaohan-dong: login: zhaohan-dong count: 11 avatarUrl: https://avatars.githubusercontent.com/u/65422392?u=8260f8781f50248410ebfa4c9bf70e143fe5c9f2&v=4 url: https://github.com/zhaohan-dong -YuriiMotov: - login: YuriiMotov - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov mariacamilagl: login: mariacamilagl count: 9 @@ -158,6 +158,11 @@ prostomarkeloff: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=6918e39a1224194ba636e897461a02a20126d7ad&v=4 url: https://github.com/prostomarkeloff +frankie567: + login: frankie567 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=f3e79acfe4ed207e15c2145161a8a9759925fcd2&v=4 + url: https://github.com/frankie567 nsidnev: login: nsidnev count: 3 @@ -191,7 +196,7 @@ Serrones: uriyyo: login: uriyyo count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/32038156?u=0c68019beb28381ce5205a838937c61e0fe3fee2&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/32038156?u=c26ca9b821fcf6499b84db75f553d4980bf8d023&v=4 url: https://github.com/uriyyo andrew222651: login: andrew222651 @@ -261,7 +266,7 @@ Nimitha-jagadeesha: lucaromagnoli: login: lucaromagnoli count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/38782977?u=e66396859f493b4ddcb3a837a1b2b2039c805417&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/38782977?u=15df02e806a2293af40ac619fba11dbe3c0c4fd4&v=4 url: https://github.com/lucaromagnoli salmantec: login: salmantec @@ -328,11 +333,6 @@ svalouch: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/54674660?v=4 url: https://github.com/svalouch -frankie567: - login: frankie567 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=f3e79acfe4ed207e15c2145161a8a9759925fcd2&v=4 - url: https://github.com/frankie567 marier-nico: login: marier-nico count: 2 @@ -346,7 +346,7 @@ Dustyposa: aviramha: login: aviramha count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/41201924?u=6883cc4fc13a7b2e60d4deddd4be06f9c5287880&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/41201924?u=ce5d3ea7037c2e6b3f82eff87e2217d4fb63214b&v=4 url: https://github.com/aviramha iwpnd: login: iwpnd diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index c1191b460..b8a5fdb3a 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ Articles: English: + - author: Apitally + author_link: https://apitally.io + link: https://apitally.io/blog/getting-started-with-logging-in-fastapi + title: Getting started with logging in FastAPI - author: Balthazar Rouberol author_link: https://balthazar-rouberol.com link: https://blog.balthazar-rouberol.com/how-to-profile-a-fastapi-asynchronous-request diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 7b34719b6..3d8ecdb7a 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -14,6 +14,9 @@ sponsors: - login: coderabbitai avatarUrl: https://avatars.githubusercontent.com/u/132028505?v=4 url: https://github.com/coderabbitai + - login: greptileai + avatarUrl: https://avatars.githubusercontent.com/u/140149887?v=4 + url: https://github.com/greptileai - login: subtotal avatarUrl: https://avatars.githubusercontent.com/u/176449348?v=4 url: https://github.com/subtotal @@ -41,9 +44,9 @@ sponsors: - login: permitio avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4 url: https://github.com/permitio -- - login: marvin-robot - avatarUrl: https://avatars.githubusercontent.com/u/41086007?u=b9fcab402d0cd0aec738b6574fe60855cb0cd36d&v=4 - url: https://github.com/marvin-robot +- - login: BoostryJP + avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 + url: https://github.com/BoostryJP - login: mercedes-benz avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 url: https://github.com/mercedes-benz @@ -53,9 +56,9 @@ sponsors: - login: LambdaTest-Inc avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4 url: https://github.com/LambdaTest-Inc - - login: BoostryJP - avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 - url: https://github.com/BoostryJP + - login: requestly + avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4 + url: https://github.com/requestly - login: acsone avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 url: https://github.com/acsone @@ -71,27 +74,39 @@ sponsors: - - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries - - login: yasyf - avatarUrl: https://avatars.githubusercontent.com/u/709645?u=f36736b3c6a85f578886ecc42a740e7b436e7a01&v=4 - url: https://github.com/yasyf - - login: alixlahuec avatarUrl: https://avatars.githubusercontent.com/u/29543316?u=44357eb2a93bccf30fb9d389b8befe94a3d00985&v=4 url: https://github.com/alixlahuec + - login: Partho + avatarUrl: https://avatars.githubusercontent.com/u/2034301?u=ce195ac36835cca0cdfe6dd6e897bd38873a1524&v=4 + url: https://github.com/Partho - - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io -- - login: nilslindemann - avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 - url: https://github.com/nilslindemann - - login: upciti + - login: xsalagarcia + avatarUrl: https://avatars.githubusercontent.com/u/66035908?v=4 + url: https://github.com/xsalagarcia +- - login: upciti avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4 url: https://github.com/upciti - - login: thisisfixer - avatarUrl: https://avatars.githubusercontent.com/u/14433035?u=076d52a5b7891c764904af9f462bfb45428e25df&v=4 - url: https://github.com/thisisfixer + - login: GonnaFlyMethod + avatarUrl: https://avatars.githubusercontent.com/u/60840539?u=edf70b373fd4f1a83d3eb7c6802f4b6addb572cf&v=4 + url: https://github.com/GonnaFlyMethod + - login: ChargeStorm + avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4 + url: https://github.com/ChargeStorm + - login: DanielYang59 + avatarUrl: https://avatars.githubusercontent.com/u/80093591?u=63873f701c7c74aac83c906800a1dddc0bc8c92f&v=4 + url: https://github.com/DanielYang59 + - login: nilslindemann + avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 + url: https://github.com/nilslindemann - - login: samuelcolvin avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 url: https://github.com/samuelcolvin + - login: vincentkoc + avatarUrl: https://avatars.githubusercontent.com/u/25068?u=fbd5b2d51142daa4bdbc21e21953a3b8b8188a4a&v=4 + url: https://github.com/vincentkoc - login: otosky avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4 url: https://github.com/otosky @@ -101,6 +116,9 @@ sponsors: - login: roboflow avatarUrl: https://avatars.githubusercontent.com/u/53104118?v=4 url: https://github.com/roboflow + - login: dudikbender + avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4 + url: https://github.com/dudikbender - login: ehaca avatarUrl: https://avatars.githubusercontent.com/u/25950317?u=cec1a3e0643b785288ae8260cc295a85ab344995&v=4 url: https://github.com/ehaca @@ -113,21 +131,15 @@ sponsors: - login: Leay15 avatarUrl: https://avatars.githubusercontent.com/u/32212558?u=c4aa9c1737e515959382a5515381757b1fd86c53&v=4 url: https://github.com/Leay15 - - login: kaoru0310 - avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 - url: https://github.com/kaoru0310 - - login: DelfinaCare - avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 - url: https://github.com/DelfinaCare - login: Karine-Bauch avatarUrl: https://avatars.githubusercontent.com/u/90465103?u=7feb1018abb1a5631cfd9a91fea723d1ceb5f49b&v=4 url: https://github.com/Karine-Bauch - login: jugeeem avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4 url: https://github.com/jugeeem - - login: dudikbender - avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4 - url: https://github.com/dudikbender + - login: connorpark24 + avatarUrl: https://avatars.githubusercontent.com/u/142128990?u=09b84a4beb1f629b77287a837bcf3729785cdd89&v=4 + url: https://github.com/connorpark24 - login: patsatsia avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 url: https://github.com/patsatsia @@ -140,9 +152,12 @@ sponsors: - login: chickenandstats avatarUrl: https://avatars.githubusercontent.com/u/79477966?u=ae2b894aa954070db1d7830dab99b49eba4e4567&v=4 url: https://github.com/chickenandstats - - login: dodo5522 - avatarUrl: https://avatars.githubusercontent.com/u/1362607?u=9bf1e0e520cccc547c046610c468ce6115bbcf9f&v=4 - url: https://github.com/dodo5522 + - login: kaoru0310 + avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 + url: https://github.com/kaoru0310 + - login: DelfinaCare + avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 + url: https://github.com/DelfinaCare - login: knallgelb avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 url: https://github.com/knallgelb @@ -170,9 +185,12 @@ sponsors: - login: Ryandaydev avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=679ff84cb7b988c5795a5fa583857f574a055763&v=4 url: https://github.com/Ryandaydev - - login: vincentkoc - avatarUrl: https://avatars.githubusercontent.com/u/25068?u=fbd5b2d51142daa4bdbc21e21953a3b8b8188a4a&v=4 - url: https://github.com/vincentkoc + - login: jaredtrog + avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 + url: https://github.com/jaredtrog + - login: oliverxchen + avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 + url: https://github.com/oliverxchen - login: jstanden avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 url: https://github.com/jstanden @@ -197,6 +215,9 @@ sponsors: - login: mintuhouse avatarUrl: https://avatars.githubusercontent.com/u/769950?u=ecfbd79a97d33177e0d093ddb088283cf7fe8444&v=4 url: https://github.com/mintuhouse + - login: dodo5522 + avatarUrl: https://avatars.githubusercontent.com/u/1362607?u=9bf1e0e520cccc547c046610c468ce6115bbcf9f&v=4 + url: https://github.com/dodo5522 - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=371272f2c69e680e0559a7b0a57385e83a5dc728&v=4 url: https://github.com/wdwinslow @@ -212,18 +233,15 @@ sponsors: - login: mjohnsey avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 url: https://github.com/mjohnsey + - login: enguy-hub + avatarUrl: https://avatars.githubusercontent.com/u/16822912?u=2c45f9e7f427b2f2f3b023d7fdb0d44764c92ae8&v=4 + url: https://github.com/enguy-hub - login: ashi-agrawal avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4 url: https://github.com/ashi-agrawal - login: RaamEEIL avatarUrl: https://avatars.githubusercontent.com/u/20320552?v=4 url: https://github.com/RaamEEIL - - login: jaredtrog - avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 - url: https://github.com/jaredtrog - - login: oliverxchen - avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 - url: https://github.com/oliverxchen - login: ternaus avatarUrl: https://avatars.githubusercontent.com/u/5481618?u=513a26b02a39e7a28d587cd37c6cc877ea368e6e&v=4 url: https://github.com/ternaus @@ -242,7 +260,10 @@ sponsors: - - login: manoelpqueiroz avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4 url: https://github.com/manoelpqueiroz -- - login: pawamoy +- - login: ceb10n + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n + - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy - login: siavashyj @@ -260,9 +281,9 @@ sponsors: - login: hgalytoby avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=6cc9028f3db63f8f60ad21c17b1ce4b88c4e2e60&v=4 url: https://github.com/hgalytoby - - login: nisutec - avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 - url: https://github.com/nisutec + - login: johnl28 + avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 + url: https://github.com/johnl28 - login: hoenie-ams avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 url: https://github.com/hoenie-ams @@ -278,33 +299,21 @@ sponsors: - login: petercool avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4 url: https://github.com/petercool - - login: JulioPeixoto - avatarUrl: https://avatars.githubusercontent.com/u/96303574?u=27d4614350cae33653f1be35cb47c92a12627ac9&v=4 - url: https://github.com/JulioPeixoto - - login: johnl28 - avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 - url: https://github.com/johnl28 - login: PunRabbit avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4 url: https://github.com/PunRabbit - login: PelicanQ avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 url: https://github.com/PelicanQ - - login: miguelgr - avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 - url: https://github.com/miguelgr - - login: WillHogan - avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 - url: https://github.com/WillHogan - login: my3 avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 url: https://github.com/my3 - - login: Alisa-lisa - avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 - url: https://github.com/Alisa-lisa - - login: moonape1226 - avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 - url: https://github.com/moonape1226 + - login: danielunderwood + avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 + url: https://github.com/danielunderwood + - login: rangulvers + avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 + url: https://github.com/rangulvers - login: ddanier avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 url: https://github.com/ddanier @@ -314,24 +323,18 @@ sponsors: - login: slafs avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 url: https://github.com/slafs - - login: ceb10n - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n - login: tochikuji avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 url: https://github.com/tochikuji - - login: xncbf - avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 - url: https://github.com/xncbf - - login: DMantis - avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4 - url: https://github.com/DMantis + - login: miguelgr + avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 + url: https://github.com/miguelgr + - login: WillHogan + avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 + url: https://github.com/WillHogan - login: hard-coders avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders - - login: supdann - avatarUrl: https://avatars.githubusercontent.com/u/9986994?u=9671810f4ae9504c063227fee34fd47567ff6954&v=4 - url: https://github.com/supdann - login: mntolia avatarUrl: https://avatars.githubusercontent.com/u/10390224?v=4 url: https://github.com/mntolia @@ -344,12 +347,9 @@ sponsors: - login: joshuatz avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 url: https://github.com/joshuatz - - login: danielunderwood - avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 - url: https://github.com/danielunderwood - - login: rangulvers - avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 - url: https://github.com/rangulvers + - login: nisutec + avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 + url: https://github.com/nisutec - login: sdevkota avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 url: https://github.com/sdevkota @@ -365,39 +365,45 @@ sponsors: - login: harsh183 avatarUrl: https://avatars.githubusercontent.com/u/7780198?v=4 url: https://github.com/harsh183 -- - login: KOZ39 - avatarUrl: https://avatars.githubusercontent.com/u/38822500?u=9dfc0a697df1c9628f08e20dc3fb17b1afc4e5a7&v=4 - url: https://github.com/KOZ39 - - login: rwxd - avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 - url: https://github.com/rwxd - - login: morzan1001 + - login: moonape1226 + avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 + url: https://github.com/moonape1226 + - login: xncbf + avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 + url: https://github.com/xncbf + - login: DMantis + avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4 + url: https://github.com/DMantis +- - login: morzan1001 avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4 url: https://github.com/morzan1001 - - login: azharthegeek - avatarUrl: https://avatars.githubusercontent.com/u/51288109?u=0987b2a9f39c21ccb071b6bdce0fc60d8492f8e8&v=4 - url: https://github.com/azharthegeek - - login: Olegt0rr - avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4 - url: https://github.com/Olegt0rr - login: larsyngvelundin avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4 url: https://github.com/larsyngvelundin - login: andrecorumba avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4 url: https://github.com/andrecorumba - - login: ChenPu2002 - avatarUrl: https://avatars.githubusercontent.com/u/113831763?v=4 - url: https://github.com/ChenPu2002 + - login: KOZ39 + avatarUrl: https://avatars.githubusercontent.com/u/38822500?u=9dfc0a697df1c9628f08e20dc3fb17b1afc4e5a7&v=4 + url: https://github.com/KOZ39 + - login: rwxd + avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 + url: https://github.com/rwxd + - login: hippoley + avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4 + url: https://github.com/hippoley - login: CoderDeltaLAN avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4 url: https://github.com/CoderDeltaLAN - - login: aghents - avatarUrl: https://avatars.githubusercontent.com/u/60949885?u=d8616ddf22cf998a712cdceefd6a0256a178fe9d&v=4 - url: https://github.com/aghents - - login: 0ne-stone + - login: chris1ding1 + avatarUrl: https://avatars.githubusercontent.com/u/194386334?u=5500604b50e35ed8a5aeb82ce34aa5d3ee3f88c7&v=4 + url: https://github.com/chris1ding1 + - login: onestn avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4 - url: https://github.com/0ne-stone + url: https://github.com/onestn + - login: Rubinskiy + avatarUrl: https://avatars.githubusercontent.com/u/62457878?u=f2e35ed3d196a99cfadb5a29a91950342af07e34&v=4 + url: https://github.com/Rubinskiy - login: nayasinghania avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4 url: https://github.com/nayasinghania @@ -407,6 +413,9 @@ sponsors: - login: andreagrandi avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4 url: https://github.com/andreagrandi + - login: Olegt0rr + avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4 + url: https://github.com/Olegt0rr - login: msserpa avatarUrl: https://avatars.githubusercontent.com/u/6334934?u=82c4489eb1559d88d2990d60001901b14f722bbb&v=4 url: https://github.com/msserpa diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index ae28410e7..943b92adb 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -26,6 +26,9 @@ gold: - url: https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi title: Deploy enterprise applications at startup speed img: https://fastapi.tiangolo.com/img/sponsors/railway.png + - url: https://serpapi.com/?utm_source=fastapi_website + title: "SerpApi: Web Search API" + img: https://fastapi.tiangolo.com/img/sponsors/serpapi.png silver: - url: https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display title: Pay as you go for market data @@ -58,3 +61,6 @@ bronze: - url: https://lambdatest.com/?utm_source=fastapi&utm_medium=partner&utm_campaign=sponsor&utm_term=opensource&utm_content=webpage title: LambdaTest, AI-Powered Cloud-based Test Orchestration Platform img: https://fastapi.tiangolo.com/img/sponsors/lambdatest.png + - url: https://requestly.com/fastapi + title: All-in-one platform to Test, Mock and Intercept APIs. Built for speed, privacy and offline support. + img: https://fastapi.tiangolo.com/img/sponsors/requestly.png diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 62ba6a84c..14f55805c 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -46,3 +46,4 @@ logins: - madisonredtfeldt - railwayapp - subtotal + - requestly diff --git a/docs/en/data/topic_repos.yml b/docs/en/data/topic_repos.yml index 9d95fb8b1..1bb6fd70d 100644 --- a/docs/en/data/topic_repos.yml +++ b/docs/en/data/topic_repos.yml @@ -1,81 +1,86 @@ - name: full-stack-fastapi-template html_url: https://github.com/fastapi/full-stack-fastapi-template - stars: 38085 + stars: 38779 owner_login: fastapi owner_html_url: https://github.com/fastapi - name: Hello-Python html_url: https://github.com/mouredev/Hello-Python - stars: 32243 + stars: 32726 owner_login: mouredev owner_html_url: https://github.com/mouredev - name: serve html_url: https://github.com/jina-ai/serve - stars: 21754 + stars: 21779 owner_login: jina-ai owner_html_url: https://github.com/jina-ai - name: HivisionIDPhotos html_url: https://github.com/Zeyi-Lin/HivisionIDPhotos - stars: 19400 + stars: 20028 owner_login: Zeyi-Lin owner_html_url: https://github.com/Zeyi-Lin - name: sqlmodel html_url: https://github.com/fastapi/sqlmodel - stars: 16859 + stars: 17038 owner_login: fastapi owner_html_url: https://github.com/fastapi - name: Douyin_TikTok_Download_API html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API - stars: 14452 + stars: 14786 owner_login: Evil0ctal owner_html_url: https://github.com/Evil0ctal - name: fastapi-best-practices html_url: https://github.com/zhanymkanov/fastapi-best-practices - stars: 13613 + stars: 13968 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov +- name: machine-learning-zoomcamp + html_url: https://github.com/DataTalksClub/machine-learning-zoomcamp + stars: 12171 + owner_login: DataTalksClub + owner_html_url: https://github.com/DataTalksClub - name: fastapi_mcp html_url: https://github.com/tadata-org/fastapi_mcp - stars: 10624 + stars: 10976 owner_login: tadata-org owner_html_url: https://github.com/tadata-org - name: awesome-fastapi html_url: https://github.com/mjhea0/awesome-fastapi - stars: 10415 + stars: 10618 owner_login: mjhea0 owner_html_url: https://github.com/mjhea0 -- name: FastUI - html_url: https://github.com/pydantic/FastUI - stars: 8879 - owner_login: pydantic - owner_html_url: https://github.com/pydantic -- name: XHS-Downloader - html_url: https://github.com/JoeanAmier/XHS-Downloader - stars: 8824 - owner_login: JoeanAmier - owner_html_url: https://github.com/JoeanAmier - name: SurfSense html_url: https://github.com/MODSetter/SurfSense - stars: 8257 + stars: 10243 owner_login: MODSetter owner_html_url: https://github.com/MODSetter -- name: FileCodeBox - html_url: https://github.com/vastsa/FileCodeBox - stars: 7367 - owner_login: vastsa - owner_html_url: https://github.com/vastsa +- name: XHS-Downloader + html_url: https://github.com/JoeanAmier/XHS-Downloader + stars: 9062 + owner_login: JoeanAmier + owner_html_url: https://github.com/JoeanAmier +- name: FastUI + html_url: https://github.com/pydantic/FastUI + stars: 8892 + owner_login: pydantic + owner_html_url: https://github.com/pydantic - name: polar html_url: https://github.com/polarsource/polar - stars: 7291 + stars: 8084 owner_login: polarsource owner_html_url: https://github.com/polarsource +- name: FileCodeBox + html_url: https://github.com/vastsa/FileCodeBox + stars: 7494 + owner_login: vastsa + owner_html_url: https://github.com/vastsa - name: nonebot2 html_url: https://github.com/nonebot/nonebot2 - stars: 7065 + stars: 7128 owner_login: nonebot owner_html_url: https://github.com/nonebot - name: hatchet html_url: https://github.com/hatchet-dev/hatchet - stars: 6070 + stars: 6155 owner_login: hatchet-dev owner_html_url: https://github.com/hatchet-dev - name: serge @@ -85,27 +90,27 @@ owner_html_url: https://github.com/serge-chat - name: fastapi-users html_url: https://github.com/fastapi-users/fastapi-users - stars: 5599 + stars: 5683 owner_login: fastapi-users owner_html_url: https://github.com/fastapi-users - name: strawberry html_url: https://github.com/strawberry-graphql/strawberry - stars: 4422 + stars: 4452 owner_login: strawberry-graphql owner_html_url: https://github.com/strawberry-graphql - name: chatgpt-web-share html_url: https://github.com/chatpire/chatgpt-web-share - stars: 4301 + stars: 4296 owner_login: chatpire owner_html_url: https://github.com/chatpire - name: poem html_url: https://github.com/poem-web/poem - stars: 4197 + stars: 4235 owner_login: poem-web owner_html_url: https://github.com/poem-web - name: dynaconf html_url: https://github.com/dynaconf/dynaconf - stars: 4144 + stars: 4174 owner_login: dynaconf owner_html_url: https://github.com/dynaconf - name: atrilabs-engine @@ -115,42 +120,42 @@ owner_html_url: https://github.com/Atri-Labs - name: Kokoro-FastAPI html_url: https://github.com/remsky/Kokoro-FastAPI - stars: 3739 + stars: 3875 owner_login: remsky owner_html_url: https://github.com/remsky - name: logfire html_url: https://github.com/pydantic/logfire - stars: 3614 + stars: 3717 owner_login: pydantic owner_html_url: https://github.com/pydantic - name: LitServe html_url: https://github.com/Lightning-AI/LitServe - stars: 3578 + stars: 3615 owner_login: Lightning-AI owner_html_url: https://github.com/Lightning-AI - name: datamodel-code-generator html_url: https://github.com/koxudaxi/datamodel-code-generator - stars: 3496 + stars: 3554 owner_login: koxudaxi owner_html_url: https://github.com/koxudaxi -- name: farfalle - html_url: https://github.com/rashadphz/farfalle - stars: 3459 - owner_login: rashadphz - owner_html_url: https://github.com/rashadphz -- name: fastapi-admin - html_url: https://github.com/fastapi-admin/fastapi-admin - stars: 3456 - owner_login: fastapi-admin - owner_html_url: https://github.com/fastapi-admin - name: huma html_url: https://github.com/danielgtaylor/huma - stars: 3447 + stars: 3521 owner_login: danielgtaylor owner_html_url: https://github.com/danielgtaylor +- name: fastapi-admin + html_url: https://github.com/fastapi-admin/fastapi-admin + stars: 3497 + owner_login: fastapi-admin + owner_html_url: https://github.com/fastapi-admin +- name: farfalle + html_url: https://github.com/rashadphz/farfalle + stars: 3476 + owner_login: rashadphz + owner_html_url: https://github.com/rashadphz - name: tracecat html_url: https://github.com/TracecatHQ/tracecat - stars: 3254 + stars: 3310 owner_login: TracecatHQ owner_html_url: https://github.com/TracecatHQ - name: opyrator @@ -160,336 +165,331 @@ owner_html_url: https://github.com/ml-tooling - name: docarray html_url: https://github.com/docarray/docarray - stars: 3107 + stars: 3108 owner_login: docarray owner_html_url: https://github.com/docarray - name: fastapi-realworld-example-app html_url: https://github.com/nsidnev/fastapi-realworld-example-app - stars: 2936 + stars: 2945 owner_login: nsidnev owner_html_url: https://github.com/nsidnev - name: uvicorn-gunicorn-fastapi-docker html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker - stars: 2804 + stars: 2809 owner_login: tiangolo owner_html_url: https://github.com/tiangolo -- name: best-of-web-python - html_url: https://github.com/ml-tooling/best-of-web-python - stars: 2610 - owner_login: ml-tooling - owner_html_url: https://github.com/ml-tooling +- name: devpush + html_url: https://github.com/hunvreus/devpush + stars: 2784 + owner_login: hunvreus + owner_html_url: https://github.com/hunvreus - name: mcp-context-forge html_url: https://github.com/IBM/mcp-context-forge - stars: 2572 + stars: 2763 owner_login: IBM owner_html_url: https://github.com/IBM +- name: best-of-web-python + html_url: https://github.com/ml-tooling/best-of-web-python + stars: 2630 + owner_login: ml-tooling + owner_html_url: https://github.com/ml-tooling - name: fastapi-react html_url: https://github.com/Buuntu/fastapi-react - stars: 2451 + stars: 2464 owner_login: Buuntu owner_html_url: https://github.com/Buuntu -- name: RasaGPT - html_url: https://github.com/paulpierre/RasaGPT - stars: 2441 - owner_login: paulpierre - owner_html_url: https://github.com/paulpierre - name: FastAPI-template html_url: https://github.com/s3rius/FastAPI-template - stars: 2424 + stars: 2453 owner_login: s3rius owner_html_url: https://github.com/s3rius +- name: RasaGPT + html_url: https://github.com/paulpierre/RasaGPT + stars: 2444 + owner_login: paulpierre + owner_html_url: https://github.com/paulpierre - name: sqladmin html_url: https://github.com/aminalaee/sqladmin - stars: 2357 + stars: 2423 owner_login: aminalaee owner_html_url: https://github.com/aminalaee - name: nextpy html_url: https://github.com/dot-agent/nextpy - stars: 2324 + stars: 2325 owner_login: dot-agent owner_html_url: https://github.com/dot-agent - name: supabase-py html_url: https://github.com/supabase/supabase-py - stars: 2236 + stars: 2292 owner_login: supabase owner_html_url: https://github.com/supabase - name: 30-Days-of-Python html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python - stars: 2210 + stars: 2214 owner_login: codingforentrepreneurs owner_html_url: https://github.com/codingforentrepreneurs +- name: Yuxi-Know + html_url: https://github.com/xerrors/Yuxi-Know + stars: 2212 + owner_login: xerrors + owner_html_url: https://github.com/xerrors - name: langserve html_url: https://github.com/langchain-ai/langserve - stars: 2171 + stars: 2191 owner_login: langchain-ai owner_html_url: https://github.com/langchain-ai - name: fastapi-utils html_url: https://github.com/fastapiutils/fastapi-utils - stars: 2164 + stars: 2185 owner_login: fastapiutils owner_html_url: https://github.com/fastapiutils - name: solara html_url: https://github.com/widgetti/solara - stars: 2102 + stars: 2111 owner_login: widgetti owner_html_url: https://github.com/widgetti -- name: Yuxi-Know - html_url: https://github.com/xerrors/Yuxi-Know - stars: 1995 - owner_login: xerrors - owner_html_url: https://github.com/xerrors - name: mangum html_url: https://github.com/Kludex/mangum - stars: 1989 + stars: 2011 owner_login: Kludex owner_html_url: https://github.com/Kludex -- name: python-week-2022 - html_url: https://github.com/rochacbruno/python-week-2022 - stars: 1816 - owner_login: rochacbruno - owner_html_url: https://github.com/rochacbruno - name: agentkit html_url: https://github.com/BCG-X-Official/agentkit - stars: 1789 + stars: 1826 owner_login: BCG-X-Official owner_html_url: https://github.com/BCG-X-Official +- name: python-week-2022 + html_url: https://github.com/rochacbruno/python-week-2022 + stars: 1815 + owner_login: rochacbruno + owner_html_url: https://github.com/rochacbruno - name: manage-fastapi html_url: https://github.com/ycd/manage-fastapi - stars: 1780 + stars: 1787 owner_login: ycd owner_html_url: https://github.com/ycd - name: ormar html_url: https://github.com/collerek/ormar - stars: 1777 + stars: 1780 owner_login: collerek owner_html_url: https://github.com/collerek +- name: vue-fastapi-admin + html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin + stars: 1758 + owner_login: mizhexiaoxiao + owner_html_url: https://github.com/mizhexiaoxiao - name: openapi-python-client html_url: https://github.com/openapi-generators/openapi-python-client - stars: 1707 + stars: 1731 owner_login: openapi-generators owner_html_url: https://github.com/openapi-generators - name: piccolo html_url: https://github.com/piccolo-orm/piccolo - stars: 1695 + stars: 1711 owner_login: piccolo-orm owner_html_url: https://github.com/piccolo-orm -- name: vue-fastapi-admin - html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin - stars: 1695 - owner_login: mizhexiaoxiao - owner_html_url: https://github.com/mizhexiaoxiao - name: fastapi-cache html_url: https://github.com/long2ice/fastapi-cache - stars: 1653 + stars: 1677 owner_login: long2ice owner_html_url: https://github.com/long2ice +- name: slowapi + html_url: https://github.com/laurentS/slowapi + stars: 1669 + owner_login: laurentS + owner_html_url: https://github.com/laurentS - name: langchain-serve html_url: https://github.com/jina-ai/langchain-serve - stars: 1635 + stars: 1632 owner_login: jina-ai owner_html_url: https://github.com/jina-ai - name: termpair html_url: https://github.com/cs01/termpair - stars: 1624 + stars: 1621 owner_login: cs01 owner_html_url: https://github.com/cs01 -- name: slowapi - html_url: https://github.com/laurentS/slowapi - stars: 1620 - owner_login: laurentS - owner_html_url: https://github.com/laurentS +- name: FastAPI-boilerplate + html_url: https://github.com/benavlabs/FastAPI-boilerplate + stars: 1596 + owner_login: benavlabs + owner_html_url: https://github.com/benavlabs - name: coronavirus-tracker-api html_url: https://github.com/ExpDev07/coronavirus-tracker-api - stars: 1576 + stars: 1573 owner_login: ExpDev07 owner_html_url: https://github.com/ExpDev07 - name: fastapi-crudrouter html_url: https://github.com/awtkns/fastapi-crudrouter - stars: 1546 + stars: 1553 owner_login: awtkns owner_html_url: https://github.com/awtkns -- name: FastAPI-boilerplate - html_url: https://github.com/benavlabs/FastAPI-boilerplate - stars: 1516 - owner_login: benavlabs - owner_html_url: https://github.com/benavlabs - name: awesome-fastapi-projects html_url: https://github.com/Kludex/awesome-fastapi-projects - stars: 1481 + stars: 1485 owner_login: Kludex owner_html_url: https://github.com/Kludex - name: fastapi-pagination html_url: https://github.com/uriyyo/fastapi-pagination - stars: 1453 + stars: 1473 owner_login: uriyyo owner_html_url: https://github.com/uriyyo - name: bracket html_url: https://github.com/evroon/bracket - stars: 1415 + stars: 1470 owner_login: evroon owner_html_url: https://github.com/evroon -- name: awesome-python-resources - html_url: https://github.com/DjangoEx/awesome-python-resources - stars: 1413 - owner_login: DjangoEx - owner_html_url: https://github.com/DjangoEx -- name: fastapi-boilerplate - html_url: https://github.com/teamhide/fastapi-boilerplate - stars: 1406 - owner_login: teamhide - owner_html_url: https://github.com/teamhide -- name: budgetml - html_url: https://github.com/ebhy/budgetml - stars: 1346 - owner_login: ebhy - owner_html_url: https://github.com/ebhy -- name: fastapi-amis-admin - html_url: https://github.com/amisadmin/fastapi-amis-admin - stars: 1342 - owner_login: amisadmin - owner_html_url: https://github.com/amisadmin - name: fastapi-langgraph-agent-production-ready-template html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template - stars: 1334 + stars: 1456 owner_login: wassim249 owner_html_url: https://github.com/wassim249 +- name: fastapi-boilerplate + html_url: https://github.com/teamhide/fastapi-boilerplate + stars: 1424 + owner_login: teamhide + owner_html_url: https://github.com/teamhide +- name: awesome-python-resources + html_url: https://github.com/DjangoEx/awesome-python-resources + stars: 1420 + owner_login: DjangoEx + owner_html_url: https://github.com/DjangoEx +- name: fastapi-amis-admin + html_url: https://github.com/amisadmin/fastapi-amis-admin + stars: 1363 + owner_login: amisadmin + owner_html_url: https://github.com/amisadmin +- name: fastcrud + html_url: https://github.com/benavlabs/fastcrud + stars: 1362 + owner_login: benavlabs + owner_html_url: https://github.com/benavlabs +- name: budgetml + html_url: https://github.com/ebhy/budgetml + stars: 1345 + owner_login: ebhy + owner_html_url: https://github.com/ebhy - name: fastapi-tutorial html_url: https://github.com/liaogx/fastapi-tutorial - stars: 1303 + stars: 1315 owner_login: liaogx owner_html_url: https://github.com/liaogx - name: fastapi_best_architecture html_url: https://github.com/fastapi-practices/fastapi_best_architecture - stars: 1276 + stars: 1311 owner_login: fastapi-practices owner_html_url: https://github.com/fastapi-practices -- name: fastcrud - html_url: https://github.com/benavlabs/fastcrud - stars: 1272 - owner_login: benavlabs - owner_html_url: https://github.com/benavlabs - name: fastapi-code-generator html_url: https://github.com/koxudaxi/fastapi-code-generator - stars: 1253 + stars: 1270 owner_login: koxudaxi owner_html_url: https://github.com/koxudaxi - name: prometheus-fastapi-instrumentator html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator - stars: 1246 + stars: 1264 owner_login: trallnag owner_html_url: https://github.com/trallnag -- name: bolt-python - html_url: https://github.com/slackapi/bolt-python - stars: 1221 - owner_login: slackapi - owner_html_url: https://github.com/slackapi - name: bedrock-chat html_url: https://github.com/aws-samples/bedrock-chat - stars: 1220 + stars: 1243 owner_login: aws-samples owner_html_url: https://github.com/aws-samples +- name: bolt-python + html_url: https://github.com/slackapi/bolt-python + stars: 1238 + owner_login: slackapi + owner_html_url: https://github.com/slackapi - name: fastapi_production_template html_url: https://github.com/zhanymkanov/fastapi_production_template - stars: 1202 + stars: 1209 owner_login: zhanymkanov owner_html_url: https://github.com/zhanymkanov - name: fastapi-scaff html_url: https://github.com/atpuxiner/fastapi-scaff - stars: 1193 + stars: 1200 owner_login: atpuxiner owner_html_url: https://github.com/atpuxiner - name: langchain-extract html_url: https://github.com/langchain-ai/langchain-extract - stars: 1164 + stars: 1173 owner_login: langchain-ai owner_html_url: https://github.com/langchain-ai - name: fastapi-alembic-sqlmodel-async html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async - stars: 1149 + stars: 1162 owner_login: jonra1993 owner_html_url: https://github.com/jonra1993 - name: odmantic html_url: https://github.com/art049/odmantic - stars: 1133 + stars: 1137 owner_login: art049 owner_html_url: https://github.com/art049 - name: restish html_url: https://github.com/rest-sh/restish - stars: 1122 + stars: 1129 owner_login: rest-sh owner_html_url: https://github.com/rest-sh -- name: runhouse - html_url: https://github.com/run-house/runhouse - stars: 1047 +- name: kubetorch + html_url: https://github.com/run-house/kubetorch + stars: 1065 owner_login: run-house owner_html_url: https://github.com/run-house - name: flock html_url: https://github.com/Onelevenvy/flock - stars: 1027 + stars: 1039 owner_login: Onelevenvy owner_html_url: https://github.com/Onelevenvy - name: authx html_url: https://github.com/yezz123/authx - stars: 999 + stars: 1017 owner_login: yezz123 owner_html_url: https://github.com/yezz123 - name: autollm html_url: https://github.com/viddexa/autollm - stars: 999 + stars: 997 owner_login: viddexa owner_html_url: https://github.com/viddexa - name: lanarky html_url: https://github.com/ajndkr/lanarky - stars: 995 + stars: 993 owner_login: ajndkr owner_html_url: https://github.com/ajndkr -- name: titiler - html_url: https://github.com/developmentseed/titiler - stars: 952 - owner_login: developmentseed - owner_html_url: https://github.com/developmentseed -- name: energy-forecasting - html_url: https://github.com/iusztinpaul/energy-forecasting - stars: 946 - owner_login: iusztinpaul - owner_html_url: https://github.com/iusztinpaul -- name: secure - html_url: https://github.com/TypeError/secure - stars: 944 - owner_login: TypeError - owner_html_url: https://github.com/TypeError -- name: langcorn - html_url: https://github.com/msoedov/langcorn - stars: 934 - owner_login: msoedov - owner_html_url: https://github.com/msoedov - name: RuoYi-Vue3-FastAPI html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI - stars: 930 + stars: 974 owner_login: insistence owner_html_url: https://github.com/insistence - name: aktools html_url: https://github.com/akfamily/aktools - stars: 916 + stars: 972 owner_login: akfamily owner_html_url: https://github.com/akfamily +- name: titiler + html_url: https://github.com/developmentseed/titiler + stars: 965 + owner_login: developmentseed + owner_html_url: https://github.com/developmentseed +- name: secure + html_url: https://github.com/TypeError/secure + stars: 953 + owner_login: TypeError + owner_html_url: https://github.com/TypeError +- name: energy-forecasting + html_url: https://github.com/iusztinpaul/energy-forecasting + stars: 949 + owner_login: iusztinpaul + owner_html_url: https://github.com/iusztinpaul - name: every-pdf html_url: https://github.com/DDULDDUCK/every-pdf - stars: 907 + stars: 942 owner_login: DDULDDUCK owner_html_url: https://github.com/DDULDDUCK -- name: marker-api - html_url: https://github.com/adithya-s-k/marker-api - stars: 903 - owner_login: adithya-s-k - owner_html_url: https://github.com/adithya-s-k +- name: langcorn + html_url: https://github.com/msoedov/langcorn + stars: 933 + owner_login: msoedov + owner_html_url: https://github.com/msoedov - name: fastapi-observability html_url: https://github.com/blueswen/fastapi-observability - stars: 902 + stars: 923 owner_login: blueswen owner_html_url: https://github.com/blueswen -- name: fastapi-do-zero - html_url: https://github.com/dunossauro/fastapi-do-zero - stars: 900 - owner_login: dunossauro - owner_html_url: https://github.com/dunossauro diff --git a/docs/en/data/translation_reviewers.yml b/docs/en/data/translation_reviewers.yml index 68ef01f6d..45aa55e5e 100644 --- a/docs/en/data/translation_reviewers.yml +++ b/docs/en/data/translation_reviewers.yml @@ -776,7 +776,7 @@ pablocm83: d2a-raudenaerde: login: d2a-raudenaerde count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/5213150?v=4 + avatarUrl: https://avatars.githubusercontent.com/u/5213150?u=e6d0ef65c571c7e544fc1c7ec151c7c0a72fb6bb&v=4 url: https://github.com/d2a-raudenaerde valentinDruzhinin: login: valentinDruzhinin @@ -1206,7 +1206,7 @@ akagaeng: phamquanganh31101998: login: phamquanganh31101998 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/43257497?u=36fa4ee689415d869a98453083a7c4213d2136ee&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/43257497?u=6b3419ea9e318c356c42a973fb947682590bd8d3&v=4 url: https://github.com/phamquanganh31101998 peebbv6364: login: peebbv6364 @@ -1806,7 +1806,7 @@ MrL8199: ivintoiu: login: ivintoiu count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=5e3d0977f44661fb9712fa297cc8f7608ea6ce48&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=b537c905ad08b69993de8796fb235c8d4d47f039&v=4 url: https://github.com/ivintoiu TechnoService2: login: TechnoService2 @@ -1841,7 +1841,7 @@ NavesSapnis: eqsdxr: login: eqsdxr count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=58fddf77ed76966eaa8c73eea9bea4bb0c53b673&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=d7aaffb29f542b647cf0f6b0e05722490863658a&v=4 url: https://github.com/eqsdxr syedasamina56: login: syedasamina56 diff --git a/docs/en/data/translators.yml b/docs/en/data/translators.yml index cf61eee8e..a4b87e1bf 100644 --- a/docs/en/data/translators.yml +++ b/docs/en/data/translators.yml @@ -1,6 +1,6 @@ nilslindemann: login: nilslindemann - count: 122 + count: 124 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann jaystone776: @@ -103,6 +103,11 @@ pablocm83: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/28315068?u=3310fbb05bb8bfc50d2c48b6cb64ac9ee4a14549&v=4 url: https://github.com/pablocm83 +tiangolo: + login: tiangolo + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo ptt3199: login: ptt3199 count: 7 @@ -118,11 +123,6 @@ batlopes: count: 6 avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 url: https://github.com/batlopes -tiangolo: - login: tiangolo - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo lucasbalieiro: login: lucasbalieiro count: 6 diff --git a/docs/en/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md index e0404b389..5d6a40f46 100644 --- a/docs/en/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -70,12 +70,22 @@ If you understood all this, you already know how those utility tools for securit You most probably don't need these technical details. -These details are useful mainly if you had a FastAPI application older than 0.118.0 and you are facing issues with dependencies with `yield`. +These details are useful mainly if you had a FastAPI application older than 0.121.0 and you are facing issues with dependencies with `yield`. /// Dependencies with `yield` have evolved over time to account for the different use cases and to fix some issues, here's a summary of what has changed. +### Dependencies with `yield` and `scope` { #dependencies-with-yield-and-scope } + +In version 0.121.0, FastAPI added support for `Depends(scope="function")` for dependencies with `yield`. + +Using `Depends(scope="function")`, the exit code after `yield` is executed right after the *path operation function* is finished, before the response is sent back to the client. + +And when using `Depends(scope="request")` (the default), the exit code after `yield` is executed after the response is sent. + +You can read more about it in the docs for [Dependencies with `yield` - Early exit and `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope). + ### Dependencies with `yield` and `StreamingResponse`, Technical Details { #dependencies-with-yield-and-streamingresponse-technical-details } Before FastAPI 0.118.0, if you used a dependency with `yield`, it would run the exit code after the *path operation function* returned but right before sending the response. diff --git a/docs/en/docs/advanced/events.md b/docs/en/docs/advanced/events.md index c805e81ee..d9e3cb52e 100644 --- a/docs/en/docs/advanced/events.md +++ b/docs/en/docs/advanced/events.md @@ -154,7 +154,7 @@ Underneath, in the ASGI technical specification, this is part of the Starlette's Lifespan' docs. +You can read more about the Starlette `lifespan` handlers in Starlette's Lifespan' docs. Including how to handle lifespan state that can be used in other areas of your code. diff --git a/docs/en/docs/advanced/middleware.md b/docs/en/docs/advanced/middleware.md index d1be4afff..8deb0d917 100644 --- a/docs/en/docs/advanced/middleware.md +++ b/docs/en/docs/advanced/middleware.md @@ -94,4 +94,4 @@ For example: * Uvicorn's `ProxyHeadersMiddleware` * MessagePack -To see other available middlewares check Starlette's Middleware docs and the ASGI Awesome List. +To see other available middlewares check Starlette's Middleware docs and the ASGI Awesome List. diff --git a/docs/en/docs/advanced/response-cookies.md b/docs/en/docs/advanced/response-cookies.md index d8f77b56a..1f41d84b7 100644 --- a/docs/en/docs/advanced/response-cookies.md +++ b/docs/en/docs/advanced/response-cookies.md @@ -48,4 +48,4 @@ And as the `Response` can be used frequently to set headers and cookies, **FastA /// -To see all the available parameters and options, check the documentation in Starlette. +To see all the available parameters and options, check the documentation in Starlette. diff --git a/docs/en/docs/advanced/response-headers.md b/docs/en/docs/advanced/response-headers.md index 19c9ff2ad..855ba05f8 100644 --- a/docs/en/docs/advanced/response-headers.md +++ b/docs/en/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ And as the `Response` can be used frequently to set headers and cookies, **FastA Keep in mind that custom proprietary headers can be added using the `X-` prefix. -But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations (read more in [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), using the parameter `expose_headers` documented in Starlette's CORS docs. +But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations (read more in [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), using the parameter `expose_headers` documented in Starlette's CORS docs. diff --git a/docs/en/docs/advanced/templates.md b/docs/en/docs/advanced/templates.md index f41c47fe8..356f4d9ca 100644 --- a/docs/en/docs/advanced/templates.md +++ b/docs/en/docs/advanced/templates.md @@ -123,4 +123,4 @@ And because you are using `StaticFiles`, that CSS file would be served automatic ## More details { #more-details } -For more details, including how to test templates, check Starlette's docs on templates. +For more details, including how to test templates, check Starlette's docs on templates. diff --git a/docs/en/docs/advanced/testing-events.md b/docs/en/docs/advanced/testing-events.md index cb8881a09..dd93374c4 100644 --- a/docs/en/docs/advanced/testing-events.md +++ b/docs/en/docs/advanced/testing-events.md @@ -5,7 +5,7 @@ When you need `lifespan` to run in your tests, you can use the `TestClient` with {* ../../docs_src/app_testing/tutorial004.py hl[9:15,18,27:28,30:32,41:43] *} -You can read more details about the ["Running lifespan in tests in the official Starlette documentation site."](https://www.starlette.io/lifespan/#running-lifespan-in-tests) +You can read more details about the ["Running lifespan in tests in the official Starlette documentation site."](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) For the deprecated `startup` and `shutdown` events, you can use the `TestClient` as follows: diff --git a/docs/en/docs/advanced/testing-websockets.md b/docs/en/docs/advanced/testing-websockets.md index 22f9bb4a0..27eb2de2f 100644 --- a/docs/en/docs/advanced/testing-websockets.md +++ b/docs/en/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ For this, you use the `TestClient` in a `with` statement, connecting to the WebS /// note -For more details, check Starlette's documentation for testing WebSockets. +For more details, check Starlette's documentation for testing WebSockets. /// diff --git a/docs/en/docs/advanced/using-request-directly.md b/docs/en/docs/advanced/using-request-directly.md index e412ad462..c71d3b05d 100644 --- a/docs/en/docs/advanced/using-request-directly.md +++ b/docs/en/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ But there are situations where you might need to access the `Request` object dir ## Details about the `Request` object { #details-about-the-request-object } -As **FastAPI** is actually **Starlette** underneath, with a layer of several tools on top, you can use Starlette's `Request` object directly when you need to. +As **FastAPI** is actually **Starlette** underneath, with a layer of several tools on top, you can use Starlette's `Request` object directly when you need to. It would also mean that if you get data from the `Request` object directly (for example, read the body) it won't be validated, converted or documented (with OpenAPI, for the automatic API user interface) by FastAPI. @@ -45,7 +45,7 @@ The same way, you can declare any other parameter as normally, and additionally, ## `Request` documentation { #request-documentation } -You can read more details about the `Request` object in the official Starlette documentation site. +You can read more details about the `Request` object in the official Starlette documentation site. /// note | Technical Details diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index 917dd79bd..ce11485a8 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -182,5 +182,5 @@ If you need something easy to integrate with FastAPI but that is more robust, su To learn more about the options, check Starlette's documentation for: -* The `WebSocket` class. -* Class-based WebSocket handling. +* The `WebSocket` class. +* Class-based WebSocket handling. diff --git a/docs/en/docs/alternatives.md b/docs/en/docs/alternatives.md index f0576bc47..e65681543 100644 --- a/docs/en/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -417,7 +417,7 @@ Handle all the data validation, data serialization and automatic model documenta /// -### Starlette { #starlette } +### Starlette { #starlette } Starlette is a lightweight ASGI framework/toolkit, which is ideal for building high-performance asyncio services. @@ -462,7 +462,7 @@ So, anything that you can do with Starlette, you can do it directly with **FastA /// -### Uvicorn { #uvicorn } +### Uvicorn { #uvicorn } Uvicorn is a lightning-fast ASGI server, built on uvloop and httptools. diff --git a/docs/en/docs/deployment/manually.md b/docs/en/docs/deployment/manually.md index 8bb3945bc..311efb99f 100644 --- a/docs/en/docs/deployment/manually.md +++ b/docs/en/docs/deployment/manually.md @@ -52,7 +52,7 @@ The main thing you need to run a **FastAPI** application (or any other ASGI appl There are several alternatives, including: -* Uvicorn: a high performance ASGI server. +* Uvicorn: a high performance ASGI server. * Hypercorn: an ASGI server compatible with HTTP/2 and Trio among other features. * Daphne: the ASGI server built for Django Channels. * Granian: A Rust HTTP server for Python applications. diff --git a/docs/en/docs/fastapi-cli.md b/docs/en/docs/fastapi-cli.md index 0fb7789db..3e5f4e350 100644 --- a/docs/en/docs/fastapi-cli.md +++ b/docs/en/docs/fastapi-cli.md @@ -52,7 +52,7 @@ FastAPI CLI takes the path to your Python program (e.g. `main.py`) and automatic For production you would use `fastapi run` instead. 🚀 -Internally, **FastAPI CLI** uses Uvicorn, a high-performance, production-ready, ASGI server. 😎 +Internally, **FastAPI CLI** uses Uvicorn, a high-performance, production-ready, ASGI server. 😎 ## `fastapi dev` { #fastapi-dev } diff --git a/docs/en/docs/features.md b/docs/en/docs/features.md index d44d7a6ac..a345e4a0e 100644 --- a/docs/en/docs/features.md +++ b/docs/en/docs/features.md @@ -159,7 +159,7 @@ Any integration is designed to be so simple to use (with dependencies) that you ## Starlette features { #starlette-features } -**FastAPI** is fully compatible with (and based on) Starlette. So, any additional Starlette code you have, will also work. +**FastAPI** is fully compatible with (and based on) Starlette. So, any additional Starlette code you have, will also work. `FastAPI` is actually a sub-class of `Starlette`. So, if you already know or use Starlette, most of the functionality will work the same way. diff --git a/docs/en/docs/history-design-future.md b/docs/en/docs/history-design-future.md index 2182c415c..6175dcbbe 100644 --- a/docs/en/docs/history-design-future.md +++ b/docs/en/docs/history-design-future.md @@ -58,7 +58,7 @@ After testing several alternatives, I decided that I was going to use **Starlette**, the other key requirement. +During the development, I also contributed to **Starlette**, the other key requirement. ## Development { #development } diff --git a/docs/en/docs/how-to/custom-request-and-route.md b/docs/en/docs/how-to/custom-request-and-route.md index 6df24a080..884c8ed04 100644 --- a/docs/en/docs/how-to/custom-request-and-route.md +++ b/docs/en/docs/how-to/custom-request-and-route.md @@ -66,7 +66,7 @@ The `scope` `dict` and `receive` function are both part of the ASGI specificatio And those two things, `scope` and `receive`, are what is needed to create a new `Request` instance. -To learn more about the `Request` check Starlette's docs about Requests. +To learn more about the `Request` check Starlette's docs about Requests. /// diff --git a/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md new file mode 100644 index 000000000..e85d122be --- /dev/null +++ b/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -0,0 +1,133 @@ +# Migrate from Pydantic v1 to Pydantic v2 { #migrate-from-pydantic-v1-to-pydantic-v2 } + +If you have an old FastAPI app, you might be using Pydantic version 1. + +FastAPI has had support for either Pydantic v1 or v2 since version 0.100.0. + +If you had installed Pydantic v2, it would use it. If instead you had Pydantic v1, it would use that. + +Pydantic v1 is now deprecated and support for it will be removed in the next versions of FastAPI, you should **migrate to Pydantic v2**. This way you will get the latest features, improvements, and fixes. + +/// warning + +Also, the Pydantic team stopped support for Pydantic v1 for the latest versions of Python, starting with **Python 3.14**. + +If you want to use the latest features of Python, you will need to make sure you use Pydantic v2. + +/// + +If you have an old FastAPI app with Pydantic v1, here I'll show you how to migrate it to Pydantic v2, and the **new features in FastAPI 0.119.0** to help you with a gradual migration. + +## Official Guide { #official-guide } + +Pydantic has an official Migration Guide from v1 to v2. + +It also includes what has changed, how validations are now more correct and strict, possible caveats, etc. + +You can read it to understand better what has changed. + +## Tests { #tests } + +Make sure you have [tests](../tutorial/testing.md){.internal-link target=_blank} for your app and you run them on continuous integration (CI). + +This way, you can do the upgrade and make sure everything is still working as expected. + +## `bump-pydantic` { #bump-pydantic } + +In many cases, when you use regular Pydantic models without customizations, you will be able to automate most of the process of migrating from Pydantic v1 to Pydantic v2. + +You can use `bump-pydantic` from the same Pydantic team. + +This tool will help you to automatically change most of the code that needs to be changed. + +After this, you can run the tests and check if everything works. If it does, you are done. 😎 + +## Pydantic v1 in v2 { #pydantic-v1-in-v2 } + +Pydantic v2 includes everything from Pydantic v1 as a submodule `pydantic.v1`. + +This means that you can install the latest version of Pydantic v2 and import and use the old Pydantic v1 components from this submodule, as if you had the old Pydantic v1 installed. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} + +### FastAPI support for Pydantic v1 in v2 { #fastapi-support-for-pydantic-v1-in-v2 } + +Since FastAPI 0.119.0, there's also partial support for Pydantic v1 from inside of Pydantic v2, to facilitate the migration to v2. + +So, you could upgrade Pydantic to the latest version 2, and change the imports to use the `pydantic.v1` submodule, and in many cases it would just work. + +{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} + +/// warning + +Have in mind that as the Pydantic team no longer supports Pydantic v1 in recent versions of Python, starting from Python 3.14, using `pydantic.v1` is also not supported in Python 3.14 and above. + +/// + +### Pydantic v1 and v2 on the same app { #pydantic-v1-and-v2-on-the-same-app } + +It's **not supported** by Pydantic to have a model of Pydantic v2 with its own fields defined as Pydantic v1 models or vice versa. + +```mermaid +graph TB + subgraph "❌ Not Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V1Field["Pydantic v1 Model"] + end + subgraph V1["Pydantic v1 Model"] + V2Field["Pydantic v2 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +...but, you can have separated models using Pydantic v1 and v2 in the same app. + +```mermaid +graph TB + subgraph "✅ Supported" + direction TB + subgraph V2["Pydantic v2 Model"] + V2Field["Pydantic v2 Model"] + end + subgraph V1["Pydantic v1 Model"] + V1Field["Pydantic v1 Model"] + end + end + + style V2 fill:#f9fff3 + style V1 fill:#fff6f0 + style V1Field fill:#fff6f0 + style V2Field fill:#f9fff3 +``` + +In some cases, it's even possible to have both Pydantic v1 and v2 models in the same **path operation** in your FastAPI app: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} + +In this example above, the input model is a Pydantic v1 model, and the output model (defined in `response_model=ItemV2`) is a Pydantic v2 model. + +### Pydantic v1 parameters { #pydantic-v1-parameters } + +If you need to use some of the FastAPI-specific tools for parameters like `Body`, `Query`, `Form`, etc. with Pydantic v1 models, you can import them from `fastapi.temp_pydantic_v1_params` while you finish the migration to Pydantic v2: + +{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} + +### Migrate in steps { #migrate-in-steps } + +/// tip + +First try with `bump-pydantic`, if your tests pass and that works, then you're done in one command. ✨ + +/// + +If `bump-pydantic` doesn't work for your use case, you can use the support for both Pydantic v1 and v2 models in the same app to do the migration to Pydantic v2 gradually. + +You could fist upgrade Pydantic to use the latest version 2, and change the imports to use `pydantic.v1` for all your models. + +Then, you can start migrating your models from Pydantic v1 to v2 in groups, in gradual steps. 🚶 diff --git a/docs/en/docs/img/sponsors/requestly.png b/docs/en/docs/img/sponsors/requestly.png new file mode 100644 index 000000000..a167aa017 Binary files /dev/null and b/docs/en/docs/img/sponsors/requestly.png differ diff --git a/docs/en/docs/img/sponsors/serpapi-banner.png b/docs/en/docs/img/sponsors/serpapi-banner.png new file mode 100644 index 000000000..3c3fd629e Binary files /dev/null and b/docs/en/docs/img/sponsors/serpapi-banner.png differ diff --git a/docs/en/docs/img/sponsors/serpapi.png b/docs/en/docs/img/sponsors/serpapi.png new file mode 100644 index 000000000..d7258ef70 Binary files /dev/null and b/docs/en/docs/img/sponsors/serpapi.png differ diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index aaadf3cc2..35c46d15f 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -123,7 +123,7 @@ If you are building a CLI app to be FastAPI stands on the shoulders of giants: -* Starlette for the web parts. +* Starlette for the web parts. * Pydantic for the data parts. ## Installation { #installation } @@ -229,7 +229,7 @@ INFO: Application startup complete.
About the command fastapi dev main.py... -The command `fastapi dev` reads your `main.py` file, detects the **FastAPI** app in it, and starts a server using Uvicorn. +The command `fastapi dev` reads your `main.py` file, detects the **FastAPI** app in it, and starts a server using Uvicorn. By default, `fastapi dev` will start with auto-reload enabled for local development. @@ -470,7 +470,7 @@ Used by Starlette: Used by FastAPI: -* uvicorn - for the server that loads and serves your application. This includes `uvicorn[standard]`, which includes some dependencies (e.g. `uvloop`) needed for high performance serving. +* uvicorn - for the server that loads and serves your application. This includes `uvicorn[standard]`, which includes some dependencies (e.g. `uvloop`) needed for high performance serving. * `fastapi-cli[standard]` - to provide the `fastapi` command. * This includes `fastapi-cloud-cli`, which allows you to deploy your FastAPI application to FastAPI Cloud. diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index cbbb173cc..24b9f4ca3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,8 +7,170 @@ hide: ## Latest Changes +### Docs + +* 📝 Upate docs for advanced dependencies with `yield`, noting the changes in 0.121.0, adding `scope`. PR [#14287](https://github.com/fastapi/fastapi/pull/14287) by [@tiangolo](https://github.com/tiangolo). + +## 0.121.0 + +### Features + +* ✨ Add support for dependencies with scopes, support `scope="request"` for dependencies with `yield` that exit before the response is sent. PR [#14262](https://github.com/fastapi/fastapi/pull/14262) by [@tiangolo](https://github.com/tiangolo). + * New docs: [Dependencies with `yield` - Early exit and `scope`](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#early-exit-and-scope). + +### Internal + +* 👥 Update FastAPI People - Contributors and Translators. PR [#14273](https://github.com/fastapi/fastapi/pull/14273) by [@tiangolo](https://github.com/tiangolo). +* 👥 Update FastAPI People - Sponsors. PR [#14274](https://github.com/fastapi/fastapi/pull/14274) by [@tiangolo](https://github.com/tiangolo). +* 👥 Update FastAPI GitHub topic repositories. PR [#14280](https://github.com/fastapi/fastapi/pull/14280) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#14277](https://github.com/fastapi/fastapi/pull/14277) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocstrings[python] from 0.26.1 to 0.30.1. PR [#14279](https://github.com/fastapi/fastapi/pull/14279) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.120.4 + +### Fixes + +* 🐛 Fix security schemes in OpenAPI when added at the top level app. PR [#14266](https://github.com/fastapi/fastapi/pull/14266) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.120.3 + +### Refactors + +* ♻️ Reduce internal cyclic recursion in dependencies, from 2 functions calling each other to 1 calling itself. PR [#14256](https://github.com/fastapi/fastapi/pull/14256) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Refactor internals of dependencies, simplify code and remove `get_param_sub_dependant`. PR [#14255](https://github.com/fastapi/fastapi/pull/14255) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Refactor internals of dependencies, simplify using dataclasses. PR [#14254](https://github.com/fastapi/fastapi/pull/14254) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Update note for untranslated pages. PR [#14257](https://github.com/fastapi/fastapi/pull/14257) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.120.2 + +### Fixes + +* 🐛 Fix separation of schemas with nested models introduced in 0.119.0. PR [#14246](https://github.com/fastapi/fastapi/pull/14246) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🔧 Add sponsor: SerpApi. PR [#14248](https://github.com/fastapi/fastapi/pull/14248) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump actions/download-artifact from 5 to 6. PR [#14236](https://github.com/fastapi/fastapi/pull/14236) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14237](https://github.com/fastapi/fastapi/pull/14237) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump actions/upload-artifact from 4 to 5. PR [#14235](https://github.com/fastapi/fastapi/pull/14235) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.120.1 + +### Upgrades + +* ⬆️ Bump Starlette to <`0.50.0`. PR [#14234](https://github.com/fastapi/fastapi/pull/14234) by [@YuriiMotov](https://github.com/YuriiMotov). + +### Internal + +* 🔧 Add `license` and `license-files` to `pyproject.toml`, remove `License` from `classifiers`. PR [#14230](https://github.com/fastapi/fastapi/pull/14230) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.120.0 + +There are no major nor breaking changes in this release. ☕️ + +The internal reference documentation now uses `annotated_doc.Doc` instead of `typing_extensions.Doc`, this adds a new (very small) dependency on [`annotated-doc`](https://github.com/fastapi/annotated-doc), a package made just to provide that `Doc` documentation utility class. + +I would expect `typing_extensions.Doc` to be deprecated and then removed at some point from `typing_extensions`, for that reason there's the new `annotated-doc` micro-package. If you are curious about this, you can read more in the repo for [`annotated-doc`](https://github.com/fastapi/annotated-doc). + +This new version `0.120.0` only contains that transition to the new home package for that utility class `Doc`. + ### Translations +* 🌐 Sync German docs. PR [#14188](https://github.com/fastapi/fastapi/pull/14188) by [@nilslindemann](https://github.com/nilslindemann). + +### Internal + +* ➕ Migrate internal reference documentation from `typing_extensions.Doc` to `annotated_doc.Doc`. PR [#14222](https://github.com/fastapi/fastapi/pull/14222) by [@tiangolo](https://github.com/tiangolo). +* 🛠️ Update German LLM prompt and test file. PR [#14189](https://github.com/fastapi/fastapi/pull/14189) by [@nilslindemann](https://github.com/nilslindemann). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14181](https://github.com/fastapi/fastapi/pull/14181) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + +## 0.119.1 + +### Fixes + +* 🐛 Fix internal Pydantic v1 compatibility (warnings) for Python 3.14 and Pydantic 2.12.1. PR [#14186](https://github.com/fastapi/fastapi/pull/14186) by [@svlandeg](https://github.com/svlandeg). + +### Docs + +* 📝 Replace `starlette.io` by `starlette.dev` and `uvicorn.org` by `uvicorn.dev`. PR [#14176](https://github.com/fastapi/fastapi/pull/14176) by [@Kludex](https://github.com/Kludex). + +### Internal + +* 🔧 Add sponsor Requestly. PR [#14205](https://github.com/fastapi/fastapi/pull/14205) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Configure reminder for `waiting` label in `issue-manager`. PR [#14156](https://github.com/fastapi/fastapi/pull/14156) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.119.0 + +FastAPI now (temporarily) supports both Pydantic v2 models and `pydantic.v1` models at the same time in the same app, to make it easier for any FastAPI apps still using Pydantic v1 to gradually but quickly **migrate to Pydantic v2**. + +```Python +from fastapi import FastAPI +from pydantic import BaseModel as BaseModelV2 +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + + +class ItemV2(BaseModelV2): + title: str + summary: str | None = None + + +app = FastAPI() + + +@app.post("/items/", response_model=ItemV2) +def create_item(item: Item): + return {"title": item.name, "summary": item.description} +``` + +Adding this feature was a big effort with the main objective of making it easier for the few applications still stuck in Pydantic v1 to migrate to Pydantic v2. + +And with this, support for **Pydantic v1 is now deprecated** and will be **removed** from FastAPI in a future version soon. + +**Note**: have in mind that the Pydantic team already stopped supporting Pydantic v1 for recent versions of Python, starting with Python 3.14. + +You can read in the docs more about how to [Migrate from Pydantic v1 to Pydantic v2](https://fastapi.tiangolo.com/how-to/migrate-from-pydantic-v1-to-pydantic-v2/). + +### Features + +* ✨ Add support for `from pydantic.v1 import BaseModel`, mixed Pydantic v1 and v2 models in the same app. PR [#14168](https://github.com/fastapi/fastapi/pull/14168) by [@tiangolo](https://github.com/tiangolo). + +## 0.118.3 + +### Upgrades + +* ⬆️ Add support for Python 3.14. PR [#14165](https://github.com/fastapi/fastapi/pull/14165) by [@svlandeg](https://github.com/svlandeg). + +## 0.118.2 + +### Fixes + +* 🐛 Fix tagged discriminated union not recognized as body field. PR [#12942](https://github.com/fastapi/fastapi/pull/12942) by [@frankie567](https://github.com/frankie567). + +### Internal + +* ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#14167](https://github.com/fastapi/fastapi/pull/14167) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.118.1 + +### Upgrades + +* 👽️ Ensure compatibility with Pydantic 2.12.0. PR [#14036](https://github.com/fastapi/fastapi/pull/14036) by [@cjwatson](https://github.com/cjwatson). + +### Docs + +* 📝 Add External Link: Getting started with logging in FastAPI. PR [#14152](https://github.com/fastapi/fastapi/pull/14152) by [@itssimon](https://github.com/itssimon). + +### Translations + +* 🔨 Add Russian translations LLM prompt. PR [#13936](https://github.com/fastapi/fastapi/pull/13936) by [@tiangolo](https://github.com/tiangolo). * 🌐 Sync German docs. PR [#14149](https://github.com/fastapi/fastapi/pull/14149) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Russian translations for missing pages (LLM-generated). PR [#14135](https://github.com/fastapi/fastapi/pull/14135) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update Russian translations for existing pages (LLM-generated). PR [#14123](https://github.com/fastapi/fastapi/pull/14123) by [@YuriiMotov](https://github.com/YuriiMotov). @@ -16,6 +178,8 @@ hide: ### Internal +* 🔨 Move local coverage logic to its own script. PR [#14166](https://github.com/fastapi/fastapi/pull/14166) by [@tiangolo](https://github.com/tiangolo). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14161](https://github.com/fastapi/fastapi/pull/14161) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump griffe-typingdoc from 0.2.8 to 0.2.9. PR [#14144](https://github.com/fastapi/fastapi/pull/14144) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-macros-plugin from 1.3.9 to 1.4.0. PR [#14145](https://github.com/fastapi/fastapi/pull/14145) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump markdown-include-variants from 0.0.4 to 0.0.5. PR [#14146](https://github.com/fastapi/fastapi/pull/14146) by [@dependabot[bot]](https://github.com/apps/dependabot). @@ -5457,7 +5621,7 @@ Note: all the previous parameters are still there, so it's still possible to dec * Upgrade the compatible version of Starlette to `0.12.0`. * This includes support for ASGI 3 (the latest version of the standard). - * It's now possible to use [Starlette's `StreamingResponse`](https://www.starlette.io/responses/#streamingresponse) with iterators, like [file-like](https://docs.python.org/3/glossary.html#term-file-like-object) objects (as those returned by `open()`). + * It's now possible to use [Starlette's `StreamingResponse`](https://www.starlette.dev/responses/#streamingresponse) with iterators, like [file-like](https://docs.python.org/3/glossary.html#term-file-like-object) objects (as those returned by `open()`). * It's now possible to use the low level utility `iterate_in_threadpool` from `starlette.concurrency` (for advanced scenarios). * PR [#243](https://github.com/tiangolo/fastapi/pull/243). diff --git a/docs/en/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md index 6e16410a3..ab44f89c1 100644 --- a/docs/en/docs/tutorial/background-tasks.md +++ b/docs/en/docs/tutorial/background-tasks.md @@ -63,7 +63,7 @@ And then another background task generated at the *path operation function* will ## Technical Details { #technical-details } -The class `BackgroundTasks` comes directly from `starlette.background`. +The class `BackgroundTasks` comes directly from `starlette.background`. It is imported/included directly into FastAPI so that you can import it from `fastapi` and avoid accidentally importing the alternative `BackgroundTask` (without the `s` at the end) from `starlette.background`. @@ -71,7 +71,7 @@ By only using `BackgroundTasks` (and not `BackgroundTask`), it's then possible t It's still possible to use `BackgroundTask` alone in FastAPI, but you have to create the object in your code and return a Starlette `Response` including it. -You can see more details in Starlette's official docs for Background Tasks. +You can see more details in Starlette's official docs for Background Tasks. ## Caveat { #caveat } diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index adc1afa8d..494c40efa 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -184,6 +184,51 @@ If you raise any exception in the code from the *path operation function*, it wi /// +## Early exit and `scope` { #early-exit-and-scope } + +Normally the exit code of dependencies with `yield` is executed **after the response** is sent to the client. + +But if you know that you won't need to use the dependency after returning from the *path operation function*, you can use `Depends(scope="function")` to tell FastAPI that it should close the dependency after the *path operation function* returns, but **before** the **response is sent**. + +{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *} + +`Depends()` receives a `scope` parameter that can be: + +* `"function"`: start the dependency before the *path operation function* that handles the request, end the dependency after the *path operation function* ends, but **before** the response is sent back to the client. So, the dependency function will be executed **around** the *path operation **function***. +* `"request"`: start the dependency before the *path operation function* that handles the request (similar to when using `"function"`), but end **after** the response is sent back to the client. So, the dependency function will be executed **around** the **request** and response cycle. + +If not specified and the dependency has `yield`, it will have a `scope` of `"request"` by default. + +### `scope` for sub-dependencies { #scope-for-sub-dependencies } + +When you declare a dependency with a `scope="request"` (the default), any sub-dependency needs to also have a `scope` of `"request"`. + +But a dependency with `scope` of `"function"` can have dependencies with `scope` of `"function"` and `scope` of `"request"`. + +This is because any dependency needs to be able to run its exit code before the sub-dependencies, as it might need to still use them during its exit code. + +```mermaid +sequenceDiagram + +participant client as Client +participant dep_req as Dep scope="request" +participant dep_func as Dep scope="function" +participant operation as Path Operation + + client ->> dep_req: Start request + Note over dep_req: Run code up to yield + dep_req ->> dep_func: Pass dependency + Note over dep_func: Run code up to yield + dep_func ->> operation: Run path operation with dependency + operation ->> dep_func: Return from path operation + Note over dep_func: Run code after yield + Note over dep_func: ✅ Dependency closed + dep_func ->> client: Send response to client + Note over client: Response sent + Note over dep_req: Run code after yield + Note over dep_req: ✅ Dependency closed +``` + ## Dependencies with `yield`, `HTTPException`, `except` and Background Tasks { #dependencies-with-yield-httpexception-except-and-background-tasks } Dependencies with `yield` have evolved over time to cover different use cases and fix some issues. diff --git a/docs/en/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md index e75e40991..7d4c12de8 100644 --- a/docs/en/docs/tutorial/first-steps.md +++ b/docs/en/docs/tutorial/first-steps.md @@ -155,7 +155,7 @@ You could also use it to generate code automatically, for clients that communica `FastAPI` is a class that inherits directly from `Starlette`. -You can use all the Starlette functionality with `FastAPI` too. +You can use all the Starlette functionality with `FastAPI` too. /// diff --git a/docs/en/docs/tutorial/handling-errors.md b/docs/en/docs/tutorial/handling-errors.md index 58bf8ffa7..53501837c 100644 --- a/docs/en/docs/tutorial/handling-errors.md +++ b/docs/en/docs/tutorial/handling-errors.md @@ -81,7 +81,7 @@ But in case you needed it for an advanced scenario, you can add custom headers: ## Install custom exception handlers { #install-custom-exception-handlers } -You can add custom exception handlers with the same exception utilities from Starlette. +You can add custom exception handlers with the same exception utilities from Starlette. Let's say you have a custom exception `UnicornException` that you (or a library you use) might `raise`. diff --git a/docs/en/docs/tutorial/middleware.md b/docs/en/docs/tutorial/middleware.md index bc0519c67..d8889fc63 100644 --- a/docs/en/docs/tutorial/middleware.md +++ b/docs/en/docs/tutorial/middleware.md @@ -37,7 +37,7 @@ The middleware function receives: Keep in mind that custom proprietary headers can be added using the `X-` prefix. -But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) using the parameter `expose_headers` documented in Starlette's CORS docs. +But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) using the parameter `expose_headers` documented in Starlette's CORS docs. /// diff --git a/docs/en/docs/tutorial/static-files.md b/docs/en/docs/tutorial/static-files.md index 5b75d048b..66b934d4f 100644 --- a/docs/en/docs/tutorial/static-files.md +++ b/docs/en/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ All these parameters can be different than "`static`", adjust them with the need ## More info { #more-info } -For more details and options check Starlette's docs about Static Files. +For more details and options check Starlette's docs about Static Files. diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index 1e333c8f1..3dcf5dc4a 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # Testing { #testing } -Thanks to Starlette, testing **FastAPI** applications is easy and enjoyable. +Thanks to Starlette, testing **FastAPI** applications is easy and enjoyable. It is based on HTTPX, which in turn is designed based on Requests, so it's very familiar and intuitive. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index e85f31102..323035240 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -201,6 +201,7 @@ nav: - How To - Recipes: - how-to/index.md - how-to/general.md + - how-to/migrate-from-pydantic-v1-to-pydantic-v2.md - how-to/graphql.md - how-to/custom-request-and-route.md - how-to/conditional-openapi.md diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index c7ffaef5d..be31bd75c 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -80,6 +80,12 @@
+ {% endblock %} diff --git a/docs/es/docs/advanced/events.md b/docs/es/docs/advanced/events.md index 022fb5a42..a33b51791 100644 --- a/docs/es/docs/advanced/events.md +++ b/docs/es/docs/advanced/events.md @@ -154,7 +154,7 @@ Por debajo, en la especificación técnica ASGI, esto es parte del la documentación de `Lifespan` de Starlette. +Puedes leer más sobre los manejadores `lifespan` de Starlette en la documentación de `Lifespan` de Starlette. Incluyendo cómo manejar el estado de lifespan que puede ser usado en otras áreas de tu código. diff --git a/docs/es/docs/advanced/middleware.md b/docs/es/docs/advanced/middleware.md index b8fd86185..0c8c44b88 100644 --- a/docs/es/docs/advanced/middleware.md +++ b/docs/es/docs/advanced/middleware.md @@ -93,4 +93,4 @@ Por ejemplo: * `ProxyHeadersMiddleware` de Uvicorn * MessagePack -Para ver otros middlewares disponibles, revisa la documentación de Middleware de Starlette y la Lista ASGI Awesome. +Para ver otros middlewares disponibles, revisa la documentación de Middleware de Starlette y la Lista ASGI Awesome. diff --git a/docs/es/docs/advanced/response-cookies.md b/docs/es/docs/advanced/response-cookies.md index c4472eaa1..05b78528e 100644 --- a/docs/es/docs/advanced/response-cookies.md +++ b/docs/es/docs/advanced/response-cookies.md @@ -48,4 +48,4 @@ Y como el `Response` se puede usar frecuentemente para establecer headers y cook /// -Para ver todos los parámetros y opciones disponibles, revisa la documentación en Starlette. +Para ver todos los parámetros y opciones disponibles, revisa la documentación en Starlette. diff --git a/docs/es/docs/advanced/response-headers.md b/docs/es/docs/advanced/response-headers.md index 49eaa53c1..31a135c40 100644 --- a/docs/es/docs/advanced/response-headers.md +++ b/docs/es/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ Y como el `Response` se puede usar frecuentemente para establecer headers y cook Ten en cuenta que los headers propietarios personalizados se pueden agregar usando el prefijo 'X-'. -Pero si tienes headers personalizados que quieres que un cliente en un navegador pueda ver, necesitas agregarlos a tus configuraciones de CORS (leer más en [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), usando el parámetro `expose_headers` documentado en la documentación CORS de Starlette. +Pero si tienes headers personalizados que quieres que un cliente en un navegador pueda ver, necesitas agregarlos a tus configuraciones de CORS (leer más en [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), usando el parámetro `expose_headers` documentado en la documentación CORS de Starlette. diff --git a/docs/es/docs/advanced/templates.md b/docs/es/docs/advanced/templates.md index 9de866c2b..101819737 100644 --- a/docs/es/docs/advanced/templates.md +++ b/docs/es/docs/advanced/templates.md @@ -123,4 +123,4 @@ Y porque estás usando `StaticFiles`, ese archivo CSS sería servido automática ## Más detalles -Para más detalles, incluyendo cómo testear plantillas, revisa la documentación de Starlette sobre plantillas. +Para más detalles, incluyendo cómo testear plantillas, revisa la documentación de Starlette sobre plantillas. diff --git a/docs/es/docs/advanced/testing-websockets.md b/docs/es/docs/advanced/testing-websockets.md index 6d2eaf94d..190e3a224 100644 --- a/docs/es/docs/advanced/testing-websockets.md +++ b/docs/es/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ Para esto, usas el `TestClient` en un statement `with`, conectándote al WebSock /// note | Nota -Para más detalles, revisa la documentación de Starlette sobre probando sesiones WebSocket. +Para más detalles, revisa la documentación de Starlette sobre probando sesiones WebSocket. /// diff --git a/docs/es/docs/advanced/using-request-directly.md b/docs/es/docs/advanced/using-request-directly.md index be8afffcc..f61e49849 100644 --- a/docs/es/docs/advanced/using-request-directly.md +++ b/docs/es/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ Pero hay situaciones donde podrías necesitar acceder al objeto `Request` direct ## Detalles sobre el objeto `Request` -Como **FastAPI** es en realidad **Starlette** por debajo, con una capa de varias herramientas encima, puedes usar el objeto `Request` de Starlette directamente cuando lo necesites. +Como **FastAPI** es en realidad **Starlette** por debajo, con una capa de varias herramientas encima, puedes usar el objeto `Request` de Starlette directamente cuando lo necesites. También significa que si obtienes datos del objeto `Request` directamente (por ejemplo, leyendo el cuerpo) no serán validados, convertidos o documentados (con OpenAPI, para la interfaz automática de usuario de la API) por FastAPI. @@ -45,7 +45,7 @@ De la misma manera, puedes declarar cualquier otro parámetro como normalmente, ## Documentación de `Request` -Puedes leer más detalles sobre el objeto `Request` en el sitio de documentación oficial de Starlette. +Puedes leer más detalles sobre el objeto `Request` en el sitio de documentación oficial de Starlette. /// note | Detalles Técnicos diff --git a/docs/es/docs/advanced/websockets.md b/docs/es/docs/advanced/websockets.md index 95141c1ca..1320f8bb7 100644 --- a/docs/es/docs/advanced/websockets.md +++ b/docs/es/docs/advanced/websockets.md @@ -182,5 +182,5 @@ Si necesitas algo fácil de integrar con FastAPI pero que sea más robusto, sopo Para aprender más sobre las opciones, revisa la documentación de Starlette para: -* La clase `WebSocket`. -* Manejo de WebSocket basado en clases. +* La clase `WebSocket`. +* Manejo de WebSocket basado en clases. diff --git a/docs/es/docs/alternatives.md b/docs/es/docs/alternatives.md index 753b827c0..6605b7bb0 100644 --- a/docs/es/docs/alternatives.md +++ b/docs/es/docs/alternatives.md @@ -417,7 +417,7 @@ Manejar toda la validación de datos, serialización de datos y documentación a /// -### Starlette +### Starlette Starlette es un framework/toolkit ASGI liviano, ideal para construir servicios asyncio de alto rendimiento. @@ -462,7 +462,7 @@ Por lo tanto, cualquier cosa que puedas hacer con Starlette, puedes hacerlo dire /// -### Uvicorn +### Uvicorn Uvicorn es un servidor ASGI extremadamente rápido, construido sobre uvloop y httptools. diff --git a/docs/es/docs/deployment/manually.md b/docs/es/docs/deployment/manually.md index 509b9ebdb..b56cf9514 100644 --- a/docs/es/docs/deployment/manually.md +++ b/docs/es/docs/deployment/manually.md @@ -64,7 +64,7 @@ Lo principal que necesitas para ejecutar una aplicación **FastAPI** (o cualquie Hay varias alternativas, incluyendo: -* Uvicorn: un servidor ASGI de alto rendimiento. +* Uvicorn: un servidor ASGI de alto rendimiento. * Hypercorn: un servidor ASGI compatible con HTTP/2 y Trio entre otras funcionalidades. * Daphne: el servidor ASGI construido para Django Channels. * Granian: Un servidor HTTP Rust para aplicaciones en Python. diff --git a/docs/es/docs/fastapi-cli.md b/docs/es/docs/fastapi-cli.md index 9d7629fdb..34b18ee3d 100644 --- a/docs/es/docs/fastapi-cli.md +++ b/docs/es/docs/fastapi-cli.md @@ -52,7 +52,7 @@ FastAPI CLI toma el path de tu programa Python (por ejemplo, `main.py`), detecta Para producción usarías `fastapi run` en su lugar. 🚀 -Internamente, **FastAPI CLI** usa Uvicorn, un servidor ASGI de alto rendimiento y listo para producción. 😎 +Internamente, **FastAPI CLI** usa Uvicorn, un servidor ASGI de alto rendimiento y listo para producción. 😎 ## `fastapi dev` diff --git a/docs/es/docs/features.md b/docs/es/docs/features.md index 472fdd736..ac73bee16 100644 --- a/docs/es/docs/features.md +++ b/docs/es/docs/features.md @@ -159,7 +159,7 @@ Cualquier integración está diseñada para ser tan simple de usar (con dependen ## Funcionalidades de Starlette -**FastAPI** es totalmente compatible con (y está basado en) Starlette. Así que, cualquier código adicional de Starlette que tengas, también funcionará. +**FastAPI** es totalmente compatible con (y está basado en) Starlette. Así que, cualquier código adicional de Starlette que tengas, también funcionará. `FastAPI` es en realidad una subclase de `Starlette`. Así que, si ya conoces o usas Starlette, la mayoría de las funcionalidades funcionarán de la misma manera. diff --git a/docs/es/docs/history-design-future.md b/docs/es/docs/history-design-future.md index 8beb4f400..7ed4fdf05 100644 --- a/docs/es/docs/history-design-future.md +++ b/docs/es/docs/history-design-future.md @@ -58,7 +58,7 @@ Después de probar varias alternativas, decidí que iba a usar **Starlette**, el otro requisito clave. +Durante el desarrollo, también contribuí a **Starlette**, el otro requisito clave. ## Desarrollo diff --git a/docs/es/docs/how-to/custom-request-and-route.md b/docs/es/docs/how-to/custom-request-and-route.md index 0b479bf00..a6ea657d1 100644 --- a/docs/es/docs/how-to/custom-request-and-route.md +++ b/docs/es/docs/how-to/custom-request-and-route.md @@ -66,7 +66,7 @@ El `dict` `scope` y la función `receive` son ambos parte de la especificación Y esas dos cosas, `scope` y `receive`, son lo que se necesita para crear una nueva *Request instance*. -Para aprender más sobre el `Request`, revisa la documentación de Starlette sobre Requests. +Para aprender más sobre el `Request`, revisa la documentación de Starlette sobre Requests. /// diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 4c8c703b3..a965fa4b5 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -123,7 +123,7 @@ Si estás construyendo una aplicación de Starlette para las partes web. +* Starlette para las partes web. * Pydantic para las partes de datos. ## Instalación @@ -229,7 +229,7 @@ INFO: Application startup complete.
Acerca del comando fastapi dev main.py... -El comando `fastapi dev` lee tu archivo `main.py`, detecta la app **FastAPI** en él y arranca un servidor usando Uvicorn. +El comando `fastapi dev` lee tu archivo `main.py`, detecta la app **FastAPI** en él y arranca un servidor usando Uvicorn. Por defecto, `fastapi dev` comenzará con auto-recarga habilitada para el desarrollo local. @@ -470,7 +470,7 @@ Usadas por Starlette: Usadas por FastAPI / Starlette: -* uvicorn - para el servidor que carga y sirve tu aplicación. Esto incluye `uvicorn[standard]`, que incluye algunas dependencias (por ejemplo, `uvloop`) necesarias para servir con alto rendimiento. +* uvicorn - para el servidor que carga y sirve tu aplicación. Esto incluye `uvicorn[standard]`, que incluye algunas dependencias (por ejemplo, `uvloop`) necesarias para servir con alto rendimiento. * `fastapi-cli` - para proporcionar el comando `fastapi`. ### Sin Dependencias `standard` diff --git a/docs/es/docs/tutorial/background-tasks.md b/docs/es/docs/tutorial/background-tasks.md index 3fe961e41..783db20a4 100644 --- a/docs/es/docs/tutorial/background-tasks.md +++ b/docs/es/docs/tutorial/background-tasks.md @@ -61,7 +61,7 @@ Y luego otra tarea en segundo plano generada en la *path operation function* esc ## Detalles Técnicos -La clase `BackgroundTasks` proviene directamente de `starlette.background`. +La clase `BackgroundTasks` proviene directamente de `starlette.background`. Se importa/incluye directamente en FastAPI para que puedas importarla desde `fastapi` y evitar importar accidentalmente la alternativa `BackgroundTask` (sin la `s` al final) de `starlette.background`. @@ -69,7 +69,7 @@ Al usar solo `BackgroundTasks` (y no `BackgroundTask`), es posible usarla como u Todavía es posible usar `BackgroundTask` solo en FastAPI, pero debes crear el objeto en tu código y devolver una `Response` de Starlette incluyéndolo. -Puedes ver más detalles en la documentación oficial de Starlette sobre Background Tasks. +Puedes ver más detalles en la documentación oficial de Starlette sobre Background Tasks. ## Advertencia diff --git a/docs/es/docs/tutorial/first-steps.md b/docs/es/docs/tutorial/first-steps.md index 5d869c22f..b451782ad 100644 --- a/docs/es/docs/tutorial/first-steps.md +++ b/docs/es/docs/tutorial/first-steps.md @@ -163,7 +163,7 @@ También podrías usarlo para generar código automáticamente, para clientes qu `FastAPI` es una clase que hereda directamente de `Starlette`. -Puedes usar toda la funcionalidad de Starlette con `FastAPI` también. +Puedes usar toda la funcionalidad de Starlette con `FastAPI` también. /// diff --git a/docs/es/docs/tutorial/handling-errors.md b/docs/es/docs/tutorial/handling-errors.md index 2e4464989..107af2a70 100644 --- a/docs/es/docs/tutorial/handling-errors.md +++ b/docs/es/docs/tutorial/handling-errors.md @@ -81,7 +81,7 @@ Pero en caso de que los necesites para un escenario avanzado, puedes agregar hea ## Instalar manejadores de excepciones personalizados -Puedes agregar manejadores de excepciones personalizados con las mismas utilidades de excepciones de Starlette. +Puedes agregar manejadores de excepciones personalizados con las mismas utilidades de excepciones de Starlette. Supongamos que tienes una excepción personalizada `UnicornException` que tú (o un paquete que usas) podría lanzar. diff --git a/docs/es/docs/tutorial/middleware.md b/docs/es/docs/tutorial/middleware.md index 296374525..c42e4eaa5 100644 --- a/docs/es/docs/tutorial/middleware.md +++ b/docs/es/docs/tutorial/middleware.md @@ -37,7 +37,7 @@ La función middleware recibe: Ten en cuenta que los custom proprietary headers se pueden añadir usando el prefijo 'X-'. -Pero si tienes custom headers que deseas que un cliente en un navegador pueda ver, necesitas añadirlos a tus configuraciones de CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) usando el parámetro `expose_headers` documentado en la documentación de CORS de Starlette. +Pero si tienes custom headers que deseas que un cliente en un navegador pueda ver, necesitas añadirlos a tus configuraciones de CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) usando el parámetro `expose_headers` documentado en la documentación de CORS de Starlette. /// diff --git a/docs/es/docs/tutorial/static-files.md b/docs/es/docs/tutorial/static-files.md index 6aefecc4b..8c5855d86 100644 --- a/docs/es/docs/tutorial/static-files.md +++ b/docs/es/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ Todos estos parámetros pueden ser diferentes a "`static`", ajústalos según la ## Más info -Para más detalles y opciones revisa la documentación de Starlette sobre Archivos Estáticos. +Para más detalles y opciones revisa la documentación de Starlette sobre Archivos Estáticos. diff --git a/docs/es/docs/tutorial/testing.md b/docs/es/docs/tutorial/testing.md index 62ad89d58..c68e83ae3 100644 --- a/docs/es/docs/tutorial/testing.md +++ b/docs/es/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # Testing -Gracias a Starlette, escribir pruebas para aplicaciones de **FastAPI** es fácil y agradable. +Gracias a Starlette, escribir pruebas para aplicaciones de **FastAPI** es fácil y agradable. Está basado en HTTPX, que a su vez está diseñado basado en Requests, por lo que es muy familiar e intuitivo. diff --git a/docs/fa/docs/features.md b/docs/fa/docs/features.md index a5ab1597e..c265d2970 100644 --- a/docs/fa/docs/features.md +++ b/docs/fa/docs/features.md @@ -167,7 +167,7 @@ FastAPI شامل یک سیستم Uvicorn : un serveur ASGI haute performance. +* Uvicorn : un serveur ASGI haute performance. * Hypercorn : un serveur ASGI compatible avec HTTP/2 et Trio entre autres fonctionnalités. * Daphne : le serveur ASGI @@ -27,7 +27,7 @@ Vous pouvez installer un serveur compatible ASGI avec : //// tab | Uvicorn -* Uvicorn, un serveur ASGI rapide comme l'éclair, basé sur uvloop et httptools. +* Uvicorn, un serveur ASGI rapide comme l'éclair, basé sur uvloop et httptools.
diff --git a/docs/fr/docs/features.md b/docs/fr/docs/features.md index afb1de243..bc63e11b4 100644 --- a/docs/fr/docs/features.md +++ b/docs/fr/docs/features.md @@ -158,7 +158,7 @@ Tout intégration est conçue pour être si simple à utiliser (avec des dépend ## Fonctionnalités de Starlette -**FastAPI** est complètement compatible (et basé sur) Starlette. Le code utilisant Starlette que vous ajouterez fonctionnera donc aussi. +**FastAPI** est complètement compatible (et basé sur) Starlette. Le code utilisant Starlette que vous ajouterez fonctionnera donc aussi. En fait, `FastAPI` est un sous composant de `Starlette`. Donc, si vous savez déjà comment utiliser Starlette, la plupart des fonctionnalités fonctionneront de la même manière. diff --git a/docs/fr/docs/history-design-future.md b/docs/fr/docs/history-design-future.md index 6b26dd079..15be545ee 100644 --- a/docs/fr/docs/history-design-future.md +++ b/docs/fr/docs/history-design-future.md @@ -58,7 +58,7 @@ Après avoir testé plusieurs alternatives, j'ai décidé que j'allais utiliser J'y ai ensuite contribué, pour le rendre entièrement compatible avec JSON Schema, pour supporter différentes manières de définir les déclarations de contraintes, et pour améliorer le support des éditeurs (vérifications de type, autocomplétion) sur la base des tests effectués dans plusieurs éditeurs. -Pendant le développement, j'ai également contribué à **Starlette**, l'autre exigence clé. +Pendant le développement, j'ai également contribué à **Starlette**, l'autre exigence clé. ## Développement diff --git a/docs/fr/docs/index.md b/docs/fr/docs/index.md index 015c9574a..99ea8dda1 100644 --- a/docs/fr/docs/index.md +++ b/docs/fr/docs/index.md @@ -123,7 +123,7 @@ Si vous souhaitez construire une application Starlette pour les parties web. +* Starlette pour les parties web. * Pydantic pour les parties données. ## Installation @@ -138,7 +138,7 @@ $ pip install fastapi
-Vous aurez également besoin d'un serveur ASGI pour la production tel que Uvicorn ou Hypercorn. +Vous aurez également besoin d'un serveur ASGI pour la production tel que Uvicorn ou Hypercorn.
@@ -461,7 +461,7 @@ Utilisées par Starlette : Utilisées par FastAPI / Starlette : -* uvicorn - Pour le serveur qui charge et sert votre application. +* uvicorn - Pour le serveur qui charge et sert votre application. * orjson - Obligatoire si vous voulez utiliser `ORJSONResponse`. * ujson - Obligatoire si vous souhaitez utiliser `UJSONResponse`. diff --git a/docs/fr/docs/tutorial/background-tasks.md b/docs/fr/docs/tutorial/background-tasks.md index 2065ca58e..6efd16e07 100644 --- a/docs/fr/docs/tutorial/background-tasks.md +++ b/docs/fr/docs/tutorial/background-tasks.md @@ -61,7 +61,7 @@ Et ensuite une autre tâche d'arrière-plan (générée dans les paramètres de ## Détails techniques -La classe `BackgroundTasks` provient directement de `starlette.background`. +La classe `BackgroundTasks` provient directement de `starlette.background`. Elle est importée/incluse directement dans **FastAPI** pour que vous puissiez l'importer depuis `fastapi` et éviter d'importer accidentellement `BackgroundTask` (sans `s` à la fin) depuis `starlette.background`. @@ -69,7 +69,7 @@ En utilisant seulement `BackgroundTasks` (et non `BackgroundTask`), il est possi Il est tout de même possible d'utiliser `BackgroundTask` seul dans **FastAPI**, mais dans ce cas il faut créer l'objet dans le code et renvoyer une `Response` Starlette l'incluant. -Plus de détails sont disponibles dans la documentation officielle de Starlette sur les tâches d'arrière-plan (via leurs classes `BackgroundTasks`et `BackgroundTask`). +Plus de détails sont disponibles dans la documentation officielle de Starlette sur les tâches d'arrière-plan (via leurs classes `BackgroundTasks`et `BackgroundTask`). ## Avertissement diff --git a/docs/fr/docs/tutorial/first-steps.md b/docs/fr/docs/tutorial/first-steps.md index 758145362..96ea56e62 100644 --- a/docs/fr/docs/tutorial/first-steps.md +++ b/docs/fr/docs/tutorial/first-steps.md @@ -140,7 +140,7 @@ Vous pourriez aussi l'utiliser pour générer du code automatiquement, pour les `FastAPI` est une classe héritant directement de `Starlette`. -Vous pouvez donc aussi utiliser toutes les fonctionnalités de Starlette depuis `FastAPI`. +Vous pouvez donc aussi utiliser toutes les fonctionnalités de Starlette depuis `FastAPI`. /// diff --git a/docs/ja/docs/advanced/websockets.md b/docs/ja/docs/advanced/websockets.md index 43009eba8..2517530ab 100644 --- a/docs/ja/docs/advanced/websockets.md +++ b/docs/ja/docs/advanced/websockets.md @@ -184,5 +184,5 @@ Client #1596980209979 left the chat オプションの詳細については、Starletteのドキュメントを確認してください。 -* `WebSocket` クラス -* クラスベースのWebSocket処理 +* `WebSocket` クラス +* クラスベースのWebSocket処理 diff --git a/docs/ja/docs/alternatives.md b/docs/ja/docs/alternatives.md index 8129a7002..9f5152c08 100644 --- a/docs/ja/docs/alternatives.md +++ b/docs/ja/docs/alternatives.md @@ -419,7 +419,7 @@ Marshmallowに匹敵しますが、ベンチマークではMarshmallowよりも /// -### Starlette +### Starlette Starletteは、軽量なASGIフレームワーク/ツールキットで、高性能な非同期サービスの構築に最適です。 @@ -465,7 +465,7 @@ webに関するコアな部分を全て扱います。その上に機能を追 /// -### Uvicorn +### Uvicorn Uvicornは非常に高速なASGIサーバーで、uvloopとhttptoolsにより構成されています。 diff --git a/docs/ja/docs/deployment/manually.md b/docs/ja/docs/deployment/manually.md index 4ea6bd8ff..da382a9c5 100644 --- a/docs/ja/docs/deployment/manually.md +++ b/docs/ja/docs/deployment/manually.md @@ -6,7 +6,7 @@ //// tab | Uvicorn -* Uvicorn, uvloopとhttptoolsを基にした高速なASGIサーバ。 +* Uvicorn, uvloopとhttptoolsを基にした高速なASGIサーバ。
@@ -78,7 +78,7 @@ Running on 0.0.0.0:8080 over http (CTRL + C to quit) 停止した場合に自動的に再起動させるツールを設定したいかもしれません。 -さらに、GunicornをインストールしてUvicornのマネージャーとして使用したり、複数のワーカーでHypercornを使用したいかもしれません。 +さらに、GunicornをインストールしてUvicornのマネージャーとして使用したり、複数のワーカーでHypercornを使用したいかもしれません。 ワーカー数などの微調整も行いたいかもしれません。 diff --git a/docs/ja/docs/features.md b/docs/ja/docs/features.md index 4024590cf..f78eab430 100644 --- a/docs/ja/docs/features.md +++ b/docs/ja/docs/features.md @@ -160,7 +160,7 @@ FastAPIには非常に使いやすく、非常に強力なしてカスタムの独自ヘッダーを追加できます。 -ただし、ブラウザのクライアントに表示させたいカスタムヘッダーがある場合は、StarletteのCORSドキュメントに記載されているパラメータ `expose_headers` を使用して、それらをCORS設定に追加する必要があります ([CORS (オリジン間リソース共有)](cors.md){.internal-link target=_blank}) +ただし、ブラウザのクライアントに表示させたいカスタムヘッダーがある場合は、StarletteのCORSドキュメントに記載されているパラメータ `expose_headers` を使用して、それらをCORS設定に追加する必要があります ([CORS (オリジン間リソース共有)](cors.md){.internal-link target=_blank}) /// diff --git a/docs/ja/docs/tutorial/static-files.md b/docs/ja/docs/tutorial/static-files.md index f63f3f3b1..f910d7e36 100644 --- a/docs/ja/docs/tutorial/static-files.md +++ b/docs/ja/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ ## より詳しい情報 -詳細とオプションについては、Starletteの静的ファイルに関するドキュメントを確認してください。 +詳細とオプションについては、Starletteの静的ファイルに関するドキュメントを確認してください。 diff --git a/docs/ja/docs/tutorial/testing.md b/docs/ja/docs/tutorial/testing.md index fe6c8c6b4..4e8ad4f7c 100644 --- a/docs/ja/docs/tutorial/testing.md +++ b/docs/ja/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # テスト -Starlette のおかげで、**FastAPI** アプリケーションのテストは簡単で楽しいものになっています。 +Starlette のおかげで、**FastAPI** アプリケーションのテストは簡単で楽しいものになっています。 HTTPX がベースなので、非常に使いやすく直感的です。 diff --git a/docs/ko/docs/advanced/events.md b/docs/ko/docs/advanced/events.md index 5f8fe0f1e..4318ada54 100644 --- a/docs/ko/docs/advanced/events.md +++ b/docs/ko/docs/advanced/events.md @@ -154,7 +154,7 @@ ASGI 기술 사양에 따르면, 이는 Starlette의 Lifespan 문서에서 확인할 수 있습니다. +Starlette의 `lifespan` 핸들러에 대해 더 읽고 싶다면 Starlette의 Lifespan 문서에서 확인할 수 있습니다. 이 문서에는 코드의 다른 영역에서 사용할 수 있는 lifespan 상태를 처리하는 방법도 포함되어 있습니다. diff --git a/docs/ko/docs/advanced/middlewares.md b/docs/ko/docs/advanced/middlewares.md index c00aedeaf..5778528a8 100644 --- a/docs/ko/docs/advanced/middlewares.md +++ b/docs/ko/docs/advanced/middlewares.md @@ -93,4 +93,4 @@ HTTP 호스트 헤더 공격을 방지하기 위해 모든 수신 요청에 올 유비콘의 `ProxyHeadersMiddleware`> MessagePack -사용 가능한 다른 미들웨어를 확인하려면 스타렛의 미들웨어 문서ASGI Awesome List를 참조하세요. +사용 가능한 다른 미들웨어를 확인하려면 스타렛의 미들웨어 문서ASGI Awesome List를 참조하세요. diff --git a/docs/ko/docs/advanced/response-cookies.md b/docs/ko/docs/advanced/response-cookies.md index 327f20afe..50da713fe 100644 --- a/docs/ko/docs/advanced/response-cookies.md +++ b/docs/ko/docs/advanced/response-cookies.md @@ -46,4 +46,4 @@ /// -사용 가능한 모든 매개변수와 옵션은 Starlette 문서에서 확인할 수 있습니다. +사용 가능한 모든 매개변수와 옵션은 Starlette 문서에서 확인할 수 있습니다. diff --git a/docs/ko/docs/advanced/response-headers.md b/docs/ko/docs/advanced/response-headers.md index e8abe0be2..e4e022c9b 100644 --- a/docs/ko/docs/advanced/response-headers.md +++ b/docs/ko/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ ‘X-’ 접두어를 사용하여 커스텀 사설 헤더를 추가할 수 있습니다. -하지만, 여러분이 브라우저에서 클라이언트가 볼 수 있기를 원하는 커스텀 헤더가 있는 경우, CORS 설정에 이를 추가해야 합니다([CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}에서 자세히 알아보세요). `expose_headers` 매개변수를 사용하여 Starlette의 CORS 설명서에 문서화된 대로 설정할 수 있습니다. +하지만, 여러분이 브라우저에서 클라이언트가 볼 수 있기를 원하는 커스텀 헤더가 있는 경우, CORS 설정에 이를 추가해야 합니다([CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}에서 자세히 알아보세요). `expose_headers` 매개변수를 사용하여 Starlette의 CORS 설명서에 문서화된 대로 설정할 수 있습니다. diff --git a/docs/ko/docs/advanced/templates.md b/docs/ko/docs/advanced/templates.md index 4cb4cbe0d..612635713 100644 --- a/docs/ko/docs/advanced/templates.md +++ b/docs/ko/docs/advanced/templates.md @@ -124,4 +124,4 @@ Item ID: 42 ## 더 많은 세부 사항 -템플릿 테스트를 포함한 더 많은 세부 사항은 Starlette의 템플릿 문서를 확인하세요. +템플릿 테스트를 포함한 더 많은 세부 사항은 Starlette의 템플릿 문서를 확인하세요. diff --git a/docs/ko/docs/advanced/testing-websockets.md b/docs/ko/docs/advanced/testing-websockets.md index 9f3b4a451..9b6782429 100644 --- a/docs/ko/docs/advanced/testing-websockets.md +++ b/docs/ko/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ /// note | 참고 -자세한 내용은 Starlette의 WebSocket 테스트에 관한 설명서를 참고하시길 바랍니다. +자세한 내용은 Starlette의 WebSocket 테스트에 관한 설명서를 참고하시길 바랍니다. /// diff --git a/docs/ko/docs/advanced/using-request-directly.md b/docs/ko/docs/advanced/using-request-directly.md index bfa4fa4db..b88a83bf4 100644 --- a/docs/ko/docs/advanced/using-request-directly.md +++ b/docs/ko/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ ## `Request` 객체에 대한 세부 사항 -**FastAPI**는 실제로 내부에 **Starlette**을 사용하며, 그 위에 여러 도구를 덧붙인 구조입니다. 따라서 여러분이 필요할 때 Starlette의 `Request` 객체를 직접 사용할 수 있습니다. +**FastAPI**는 실제로 내부에 **Starlette**을 사용하며, 그 위에 여러 도구를 덧붙인 구조입니다. 따라서 여러분이 필요할 때 Starlette의 `Request` 객체를 직접 사용할 수 있습니다. `Request` 객체에서 데이터를 직접 가져오는 경우(예: 본문을 읽기)에는 FastAPI가 해당 데이터를 검증하거나 변환하지 않으며, 문서화(OpenAPI를 통한 문서 자동화(로 생성된) API 사용자 인터페이스)도 되지 않습니다. @@ -45,7 +45,7 @@ ## `Request` 설명서 -여러분은 `Request` 객체에 대한 더 자세한 내용을 공식 Starlette 설명서 사이트에서 읽어볼 수 있습니다. +여러분은 `Request` 객체에 대한 더 자세한 내용을 공식 Starlette 설명서 사이트에서 읽어볼 수 있습니다. /// note | 기술 세부사항 diff --git a/docs/ko/docs/advanced/websockets.md b/docs/ko/docs/advanced/websockets.md index fa60a428b..d9d0dd95c 100644 --- a/docs/ko/docs/advanced/websockets.md +++ b/docs/ko/docs/advanced/websockets.md @@ -182,5 +182,5 @@ FastAPI와 쉽게 통합할 수 있으면서 더 견고하고 Redis, PostgreSQL 다음 옵션에 대한 자세한 내용을 보려면 Starlette의 문서를 확인하세요: -* `WebSocket` 클래스. -* 클래스 기반 WebSocket 처리. +* `WebSocket` 클래스. +* 클래스 기반 WebSocket 처리. diff --git a/docs/ko/docs/fastapi-cli.md b/docs/ko/docs/fastapi-cli.md index 3a976af36..a1160c71f 100644 --- a/docs/ko/docs/fastapi-cli.md +++ b/docs/ko/docs/fastapi-cli.md @@ -60,7 +60,7 @@ FastAPI CLI는 Python 프로그램의 경로(예: `main.py`)를 인수로 받아 프로덕션 환경에서는 `fastapi run` 명령어를 사용합니다. 🚀 -내부적으로, **FastAPI CLI**는 고성능의, 프로덕션에 적합한, ASGI 서버인 Uvicorn을 사용합니다. 😎 +내부적으로, **FastAPI CLI**는 고성능의, 프로덕션에 적합한, ASGI 서버인 Uvicorn을 사용합니다. 😎 ## `fastapi dev` diff --git a/docs/ko/docs/features.md b/docs/ko/docs/features.md index 5e880c298..dfbf47999 100644 --- a/docs/ko/docs/features.md +++ b/docs/ko/docs/features.md @@ -159,7 +159,7 @@ FastAPI는 사용하기 매우 간편하지만, 엄청난 하여 추가할 수 있습니다. -그러나 만약 클라이언트의 브라우저에서 볼 수 있는 사용자 정의 헤더를 가지고 있다면, 그것들을 CORS 설정([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})에 Starlette CORS 문서에 명시된 `expose_headers` 매개변수를 이용하여 헤더들을 추가하여야합니다. +그러나 만약 클라이언트의 브라우저에서 볼 수 있는 사용자 정의 헤더를 가지고 있다면, 그것들을 CORS 설정([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})에 Starlette CORS 문서에 명시된 `expose_headers` 매개변수를 이용하여 헤더들을 추가하여야합니다. /// diff --git a/docs/ko/docs/tutorial/static-files.md b/docs/ko/docs/tutorial/static-files.md index 9db5e1c67..4f3e3ab28 100644 --- a/docs/ko/docs/tutorial/static-files.md +++ b/docs/ko/docs/tutorial/static-files.md @@ -38,4 +38,4 @@ ## 추가 정보 -자세한 내용과 선택 사항을 보려면 Starlette의 정적 파일에 관한 문서를 확인하십시오. +자세한 내용과 선택 사항을 보려면 Starlette의 정적 파일에 관한 문서를 확인하십시오. diff --git a/docs/ko/docs/tutorial/testing.md b/docs/ko/docs/tutorial/testing.md index a483cbf00..915ff6d22 100644 --- a/docs/ko/docs/tutorial/testing.md +++ b/docs/ko/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # 테스팅 -Starlette 덕분에 **FastAPI** 를 테스트하는 일은 쉽고 즐거운 일이 되었습니다. +Starlette 덕분에 **FastAPI** 를 테스트하는 일은 쉽고 즐거운 일이 되었습니다. Starlette는 HTTPX를 기반으로 하며, 이는 Requests를 기반으로 설계되었기 때문에 매우 친숙하고 직관적입니다. diff --git a/docs/missing-translation.md b/docs/missing-translation.md index c2882e90e..bfff84766 100644 --- a/docs/missing-translation.md +++ b/docs/missing-translation.md @@ -1,7 +1,9 @@ /// warning -The current page still doesn't have a translation for this language. +This page hasn’t been translated into your language yet. 🌍 -But you can help translating it: [Contributing](https://fastapi.tiangolo.com/contributing/){.internal-link target=_blank}. +We’re currently switching to an automated translation system 🤖, which will help keep all translations complete and up to date. + +Learn more: [Contributing – Translations](https://fastapi.tiangolo.com/contributing/#translations){.internal-link target=_blank} /// diff --git a/docs/pt/docs/advanced/events.md b/docs/pt/docs/advanced/events.md index 504b6db57..2d38e0899 100644 --- a/docs/pt/docs/advanced/events.md +++ b/docs/pt/docs/advanced/events.md @@ -155,7 +155,7 @@ Por baixo, na especificação técnica ASGI, essa é a parte do Documentação do Lifespan Starlette. +Você pode ler mais sobre o manipulador `lifespan` do Starlette na Documentação do Lifespan Starlette. Incluindo como manipular estado do lifespan que pode ser usado em outras áreas do seu código. diff --git a/docs/pt/docs/advanced/middleware.md b/docs/pt/docs/advanced/middleware.md index 8167f7d27..7700939f0 100644 --- a/docs/pt/docs/advanced/middleware.md +++ b/docs/pt/docs/advanced/middleware.md @@ -93,4 +93,4 @@ Por exemplo: * Uvicorn's `ProxyHeadersMiddleware` * MessagePack -Para checar outros middlewares disponíveis, confira Documentação de Middlewares do Starlette e a Lista Incrível do ASGI. +Para checar outros middlewares disponíveis, confira Documentação de Middlewares do Starlette e a Lista Incrível do ASGI. diff --git a/docs/pt/docs/advanced/response-cookies.md b/docs/pt/docs/advanced/response-cookies.md index eed69f222..f005f0b9b 100644 --- a/docs/pt/docs/advanced/response-cookies.md +++ b/docs/pt/docs/advanced/response-cookies.md @@ -48,4 +48,4 @@ E como o `Response` pode ser usado frequentemente para definir cabeçalhos e coo /// -Para ver todos os parâmetros e opções disponíveis, verifique a documentação no Starlette. +Para ver todos os parâmetros e opções disponíveis, verifique a documentação no Starlette. diff --git a/docs/pt/docs/advanced/response-headers.md b/docs/pt/docs/advanced/response-headers.md index a8034a7a4..a1fc84cc0 100644 --- a/docs/pt/docs/advanced/response-headers.md +++ b/docs/pt/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ E como a `Response` pode ser usada frequentemente para definir cabeçalhos e coo Tenha em mente que cabeçalhos personalizados proprietários podem ser adicionados usando o prefixo 'X-'. -Porém, se voce tiver cabeçalhos personalizados que deseja que um cliente no navegador possa ver, você precisa adicioná-los às suas configurações de CORS (saiba mais em [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), usando o parâmetro `expose_headers` descrito na documentação de CORS do Starlette. +Porém, se voce tiver cabeçalhos personalizados que deseja que um cliente no navegador possa ver, você precisa adicioná-los às suas configurações de CORS (saiba mais em [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), usando o parâmetro `expose_headers` descrito na documentação de CORS do Starlette. diff --git a/docs/pt/docs/advanced/templates.md b/docs/pt/docs/advanced/templates.md index 4d22bfbbf..65ff89fae 100644 --- a/docs/pt/docs/advanced/templates.md +++ b/docs/pt/docs/advanced/templates.md @@ -121,4 +121,4 @@ E como você está usando `StaticFiles`, este arquivo CSS será automaticamente ## Mais detalhes -Para obter mais detalhes, incluindo como testar templates, consulte a documentação da Starlette sobre templates. +Para obter mais detalhes, incluindo como testar templates, consulte a documentação da Starlette sobre templates. diff --git a/docs/pt/docs/advanced/testing-websockets.md b/docs/pt/docs/advanced/testing-websockets.md index 942771bc9..9b8193655 100644 --- a/docs/pt/docs/advanced/testing-websockets.md +++ b/docs/pt/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ Para isso, você utiliza o `TestClient` dentro de uma instrução `with`, conect /// note | Nota -Para mais detalhes, confira a documentação do Starlette para testar WebSockets. +Para mais detalhes, confira a documentação do Starlette para testar WebSockets. /// diff --git a/docs/pt/docs/advanced/using-request-directly.md b/docs/pt/docs/advanced/using-request-directly.md index f31e2ed15..f4fb0ed8f 100644 --- a/docs/pt/docs/advanced/using-request-directly.md +++ b/docs/pt/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ Porém há situações em que você possa precisar acessar o objeto `Request` di ## Detalhes sobre o objeto `Request` -Como o **FastAPI** é na verdade o **Starlette** por baixo, com camadas de diversas funcionalidades por cima, você pode utilizar o objeto `Request` do Starlette diretamente quando precisar. +Como o **FastAPI** é na verdade o **Starlette** por baixo, com camadas de diversas funcionalidades por cima, você pode utilizar o objeto `Request` do Starlette diretamente quando precisar. Isso significaria também que se você obtiver informações do objeto `Request` diretamente (ler o corpo da requisição por exemplo), as informações não serão validadas, convertidas ou documentadas (com o OpenAPI, para a interface de usuário automática da API) pelo FastAPI. @@ -45,7 +45,7 @@ Do mesmo jeito, você pode declarar qualquer outro parâmetro normalmente, e al ## Documentação do `Request` -Você pode ler mais sobre os detalhes do objeto `Request` no site da documentação oficial do Starlette.. +Você pode ler mais sobre os detalhes do objeto `Request` no site da documentação oficial do Starlette.. /// note | Detalhes Técnicos diff --git a/docs/pt/docs/advanced/websockets.md b/docs/pt/docs/advanced/websockets.md index 82e443886..721c0b403 100644 --- a/docs/pt/docs/advanced/websockets.md +++ b/docs/pt/docs/advanced/websockets.md @@ -182,5 +182,5 @@ Se você precisa de algo fácil de integrar com o FastAPI, mas que seja mais rob Para aprender mais sobre as opções, verifique a documentação do Starlette para: -* A classe `WebSocket`. -* Manipulação de WebSockets baseada em classes. +* A classe `WebSocket`. +* Manipulação de WebSockets baseada em classes. diff --git a/docs/pt/docs/alternatives.md b/docs/pt/docs/alternatives.md index 29c9693bb..66cf3fe12 100644 --- a/docs/pt/docs/alternatives.md +++ b/docs/pt/docs/alternatives.md @@ -419,7 +419,7 @@ Controlar toda a validação de dados, serialização de dados e modelo de docum /// -### Starlette +### Starlette Starlette é um framework/caixa de ferramentas ASGI peso leve, o que é ideal para construir serviços assíncronos de alta performance. @@ -465,7 +465,7 @@ Então, qualquer coisa que você faz com Starlette, você pode fazer diretamente /// -### Uvicorn +### Uvicorn Uvicorn é um servidor ASGI peso leve, construído com uvloop e httptools. diff --git a/docs/pt/docs/deployment/manually.md b/docs/pt/docs/deployment/manually.md index 46e580807..c7caabbcd 100644 --- a/docs/pt/docs/deployment/manually.md +++ b/docs/pt/docs/deployment/manually.md @@ -52,7 +52,7 @@ A principal coisa que você precisa para executar uma aplicação **FastAPI** (o Existem diversas alternativas, incluindo: -* Uvicorn: um servidor ASGI de alta performance. +* Uvicorn: um servidor ASGI de alta performance. * Hypercorn: um servidor ASGI compátivel com HTTP/2, Trio e outros recursos. * Daphne: servidor ASGI construído para Django Channels. * Granian: um servidor HTTP Rust para aplicações Python. diff --git a/docs/pt/docs/fastapi-cli.md b/docs/pt/docs/fastapi-cli.md index 829686631..f33c2ba2a 100644 --- a/docs/pt/docs/fastapi-cli.md +++ b/docs/pt/docs/fastapi-cli.md @@ -60,7 +60,7 @@ O FastAPI CLI recebe o caminho do seu programa Python, detecta automaticamente a Para produção você usaria `fastapi run` no lugar. 🚀 -Internamente, **FastAPI CLI** usa Uvicorn, um servidor ASGI de alta performance e pronto para produção. 😎 +Internamente, **FastAPI CLI** usa Uvicorn, um servidor ASGI de alta performance e pronto para produção. 😎 ## `fastapi dev` diff --git a/docs/pt/docs/features.md b/docs/pt/docs/features.md index a90a8094b..ccc3300d6 100644 --- a/docs/pt/docs/features.md +++ b/docs/pt/docs/features.md @@ -159,7 +159,7 @@ Qualquer integração é projetada para ser tão simples de usar (com dependênc ## Recursos do Starlette -**FastAPI** é totalmente compatível com (e baseado no) Starlette. Então, qualquer código adicional Starlette que você tiver, também funcionará. +**FastAPI** é totalmente compatível com (e baseado no) Starlette. Então, qualquer código adicional Starlette que você tiver, também funcionará. `FastAPI` é na verdade uma sub-classe do `Starlette`. Então, se você já conhece ou usa Starlette, a maioria das funcionalidades se comportará da mesma forma. diff --git a/docs/pt/docs/history-design-future.md b/docs/pt/docs/history-design-future.md index 4ec217405..1d0768c62 100644 --- a/docs/pt/docs/history-design-future.md +++ b/docs/pt/docs/history-design-future.md @@ -58,7 +58,7 @@ Após testar várias alternativas, eu decidi que usaria o **Starlette**, outro requisito chave. +Durante o desenvolvimento, eu também contribuí com o **Starlette**, outro requisito chave. ## Desenvolvimento diff --git a/docs/pt/docs/how-to/custom-request-and-route.md b/docs/pt/docs/how-to/custom-request-and-route.md index 8f432f6fe..151a0f5d4 100644 --- a/docs/pt/docs/how-to/custom-request-and-route.md +++ b/docs/pt/docs/how-to/custom-request-and-route.md @@ -66,7 +66,7 @@ O dicionário `scope` e a função `receive` são ambos parte da especificação E essas duas coisas, `scope` e `receive`, são o que é necessário para criar uma nova instância de `Request`. -Para aprender mais sobre o `Request` confira a documentação do Starlette sobre Requests. +Para aprender mais sobre o `Request` confira a documentação do Starlette sobre Requests. /// diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index ce9929bf4..a361913c3 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -123,7 +123,7 @@ Se você estiver construindo uma aplicação Starlette para as partes web. +* Starlette para as partes web. * Pydantic para a parte de dados. ## Instalação @@ -229,7 +229,7 @@ INFO: Application startup complete.
Sobre o comando fastapi dev main.py... -O comando `fastapi dev` lê o seu arquivo `main.py`, identifica o aplicativo **FastAPI** nele, e inicia um servidor usando o Uvicorn. +O comando `fastapi dev` lê o seu arquivo `main.py`, identifica o aplicativo **FastAPI** nele, e inicia um servidor usando o Uvicorn. Por padrão, o `fastapi dev` iniciará com *auto-reload* habilitado para desenvolvimento local. @@ -471,7 +471,7 @@ Utilizado pelo Starlette: Utilizado pelo FastAPI / Starlette: -* uvicorn - para o servidor que carrega e serve a sua aplicação. Isto inclui `uvicorn[standard]`, que inclui algumas dependências (e.g. `uvloop`) necessárias para servir em alta performance. +* uvicorn - para o servidor que carrega e serve a sua aplicação. Isto inclui `uvicorn[standard]`, que inclui algumas dependências (e.g. `uvloop`) necessárias para servir em alta performance. * `fastapi-cli` - que disponibiliza o comando `fastapi`. ### Sem as dependências `standard` diff --git a/docs/pt/docs/tutorial/background-tasks.md b/docs/pt/docs/tutorial/background-tasks.md index 0f3796371..b8ab58cda 100644 --- a/docs/pt/docs/tutorial/background-tasks.md +++ b/docs/pt/docs/tutorial/background-tasks.md @@ -61,7 +61,7 @@ E então outra tarefa em segundo plano gerada na _função de operação de cami ## Detalhes técnicos -A classe `BackgroundTasks` vem diretamente de `starlette.background`. +A classe `BackgroundTasks` vem diretamente de `starlette.background`. Ela é importada/incluída diretamente no FastAPI para que você possa importá-la do `fastapi` e evitar a importação acidental da alternativa `BackgroundTask` (sem o `s` no final) de `starlette.background`. @@ -69,7 +69,7 @@ Usando apenas `BackgroundTasks` (e não `BackgroundTask`), é então possível u Ainda é possível usar `BackgroundTask` sozinho no FastAPI, mas você deve criar o objeto em seu código e retornar uma Starlette `Response` incluindo-o. -Você pode ver mais detalhes na documentação oficiais da Starlette para tarefas em segundo plano . +Você pode ver mais detalhes na documentação oficiais da Starlette para tarefas em segundo plano . ## Ressalva diff --git a/docs/pt/docs/tutorial/first-steps.md b/docs/pt/docs/tutorial/first-steps.md index 5184d2d5f..e696bbbb7 100644 --- a/docs/pt/docs/tutorial/first-steps.md +++ b/docs/pt/docs/tutorial/first-steps.md @@ -155,7 +155,7 @@ Você também pode usá-lo para gerar código automaticamente para clientes que `FastAPI` é uma classe que herda diretamente de `Starlette`. -Você pode usar todas as funcionalidades do Starlette com `FastAPI` também. +Você pode usar todas as funcionalidades do Starlette com `FastAPI` também. /// diff --git a/docs/pt/docs/tutorial/handling-errors.md b/docs/pt/docs/tutorial/handling-errors.md index 098195db7..5cb92c744 100644 --- a/docs/pt/docs/tutorial/handling-errors.md +++ b/docs/pt/docs/tutorial/handling-errors.md @@ -83,7 +83,7 @@ Mas caso você precise, para um cenário mais complexo, você pode adicionar hea ## Instalando manipuladores de exceções customizados -Você pode adicionar manipuladores de exceção customizados com a mesma seção de utilidade de exceções presentes no Starlette +Você pode adicionar manipuladores de exceção customizados com a mesma seção de utilidade de exceções presentes no Starlette Digamos que você tenha uma exceção customizada `UnicornException` que você (ou uma biblioteca que você use) precise lançar (`raise`). diff --git a/docs/pt/docs/tutorial/middleware.md b/docs/pt/docs/tutorial/middleware.md index 32b81c646..0f5009b6d 100644 --- a/docs/pt/docs/tutorial/middleware.md +++ b/docs/pt/docs/tutorial/middleware.md @@ -37,7 +37,7 @@ A função middleware recebe: Tenha em mente que cabeçalhos proprietários personalizados podem ser adicionados usando o prefixo 'X-'. -Mas se você tiver cabeçalhos personalizados desejando que um cliente em um navegador esteja apto a ver, você precisa adicioná-los às suas configurações CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) usando o parâmetro `expose_headers` documentado em Documentos CORS da Starlette. +Mas se você tiver cabeçalhos personalizados desejando que um cliente em um navegador esteja apto a ver, você precisa adicioná-los às suas configurações CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) usando o parâmetro `expose_headers` documentado em Documentos CORS da Starlette. /// diff --git a/docs/pt/docs/tutorial/static-files.md b/docs/pt/docs/tutorial/static-files.md index 0660078f4..30e1af8e6 100644 --- a/docs/pt/docs/tutorial/static-files.md +++ b/docs/pt/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ Todos esses parâmetros podem ser diferentes de "`static`", ajuste-os de acordo ## Mais informações -Para mais detalhes e opções, verifique Starlette's docs about Static Files. +Para mais detalhes e opções, verifique Starlette's docs about Static Files. diff --git a/docs/pt/docs/tutorial/testing.md b/docs/pt/docs/tutorial/testing.md index 8eb2f29b7..dc505105a 100644 --- a/docs/pt/docs/tutorial/testing.md +++ b/docs/pt/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # Testando -Graças ao Starlette, testar aplicativos **FastAPI** é fácil e agradável. +Graças ao Starlette, testar aplicativos **FastAPI** é fácil e agradável. Ele é baseado no HTTPX, que por sua vez é projetado com base em Requests, por isso é muito familiar e intuitivo. diff --git a/docs/ru/docs/advanced/events.md b/docs/ru/docs/advanced/events.md index 6e1b49035..20d1df98a 100644 --- a/docs/ru/docs/advanced/events.md +++ b/docs/ru/docs/advanced/events.md @@ -154,7 +154,7 @@ async with lifespan(app): /// info | Информация -Вы можете прочитать больше про обработчики `lifespan` в Starlette в документации Starlette по Lifespan. +Вы можете прочитать больше про обработчики `lifespan` в Starlette в документации Starlette по Lifespan. Включая то, как работать с состоянием lifespan, которое можно использовать в других частях вашего кода. diff --git a/docs/ru/docs/advanced/middleware.md b/docs/ru/docs/advanced/middleware.md index 28802fd57..82c86b231 100644 --- a/docs/ru/docs/advanced/middleware.md +++ b/docs/ru/docs/advanced/middleware.md @@ -94,4 +94,4 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") - `ProxyHeadersMiddleware` от Uvicorn - MessagePack -Чтобы увидеть другие доступные middleware, посмотрите документацию по middleware в Starlette и список ASGI Awesome. +Чтобы увидеть другие доступные middleware, посмотрите документацию по middleware в Starlette и список ASGI Awesome. diff --git a/docs/ru/docs/advanced/response-cookies.md b/docs/ru/docs/advanced/response-cookies.md index 3aa32b9bb..9319aba6e 100644 --- a/docs/ru/docs/advanced/response-cookies.md +++ b/docs/ru/docs/advanced/response-cookies.md @@ -48,4 +48,4 @@ /// -Чтобы увидеть все доступные параметры и настройки, ознакомьтесь с документацией Starlette. +Чтобы увидеть все доступные параметры и настройки, ознакомьтесь с документацией Starlette. diff --git a/docs/ru/docs/advanced/response-headers.md b/docs/ru/docs/advanced/response-headers.md index 81e52cb69..1c9360b31 100644 --- a/docs/ru/docs/advanced/response-headers.md +++ b/docs/ru/docs/advanced/response-headers.md @@ -38,4 +38,4 @@ Помните, что собственные проприетарные заголовки можно добавлять, используя префикс `X-`. -Но если у вас есть пользовательские заголовки, которые вы хотите показывать клиенту в браузере, вам нужно добавить их в настройки CORS (подробнее см. в [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), используя параметр `expose_headers`, описанный в документации Starlette по CORS. +Но если у вас есть пользовательские заголовки, которые вы хотите показывать клиенту в браузере, вам нужно добавить их в настройки CORS (подробнее см. в [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), используя параметр `expose_headers`, описанный в документации Starlette по CORS. diff --git a/docs/ru/docs/advanced/templates.md b/docs/ru/docs/advanced/templates.md index 5675ff48a..204e88760 100644 --- a/docs/ru/docs/advanced/templates.md +++ b/docs/ru/docs/advanced/templates.md @@ -123,4 +123,4 @@ Item ID: 42 ## Подробнее { #more-details } -Больше подробностей, включая то, как тестировать шаблоны, смотрите в документации Starlette по шаблонам. +Больше подробностей, включая то, как тестировать шаблоны, смотрите в документации Starlette по шаблонам. diff --git a/docs/ru/docs/advanced/testing-events.md b/docs/ru/docs/advanced/testing-events.md index 1bf8e4723..e0ec77439 100644 --- a/docs/ru/docs/advanced/testing-events.md +++ b/docs/ru/docs/advanced/testing-events.md @@ -5,7 +5,7 @@ {* ../../docs_src/app_testing/tutorial004.py hl[9:15,18,27:28,30:32,41:43] *} -Вы можете узнать больше подробностей в статье [Запуск lifespan в тестах на официальном сайте документации Starlette.](https://www.starlette.io/lifespan/#running-lifespan-in-tests) +Вы можете узнать больше подробностей в статье [Запуск lifespan в тестах на официальном сайте документации Starlette.](https://www.starlette.dev/lifespan/#running-lifespan-in-tests) Для устаревших событий `startup` и `shutdown` вы можете использовать `TestClient` следующим образом: diff --git a/docs/ru/docs/advanced/testing-websockets.md b/docs/ru/docs/advanced/testing-websockets.md index 7c0ca2594..e840a03f2 100644 --- a/docs/ru/docs/advanced/testing-websockets.md +++ b/docs/ru/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ /// note | Примечание -Подробности смотрите в документации Starlette по тестированию WebSocket. +Подробности смотрите в документации Starlette по тестированию WebSocket. /// diff --git a/docs/ru/docs/advanced/using-request-directly.md b/docs/ru/docs/advanced/using-request-directly.md index bff2ddcb7..b92221610 100644 --- a/docs/ru/docs/advanced/using-request-directly.md +++ b/docs/ru/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ ## Подробности об объекте `Request` { #details-about-the-request-object } -Так как под капотом **FastAPI** — это **Starlette** с дополнительным слоем инструментов, вы можете при необходимости напрямую использовать объект `Request` из Starlette. +Так как под капотом **FastAPI** — это **Starlette** с дополнительным слоем инструментов, вы можете при необходимости напрямую использовать объект `Request` из Starlette. Это также означает, что если вы получаете данные напрямую из объекта `Request` (например, читаете тело запроса), то они не будут валидироваться, конвертироваться или документироваться (с OpenAPI, для автоматического пользовательского интерфейса API) средствами FastAPI. @@ -45,7 +45,7 @@ ## Документация по `Request` { #request-documentation } -Подробнее об объекте `Request` на официальном сайте документации Starlette. +Подробнее об объекте `Request` на официальном сайте документации Starlette. /// note | Технические детали diff --git a/docs/ru/docs/advanced/websockets.md b/docs/ru/docs/advanced/websockets.md index b73fa1ddb..f26185bea 100644 --- a/docs/ru/docs/advanced/websockets.md +++ b/docs/ru/docs/advanced/websockets.md @@ -182,5 +182,5 @@ Client #1596980209979 left the chat Для более глубокого изучения темы воспользуйтесь документацией Starlette: -* The `WebSocket` class. -* Class-based WebSocket handling. +* The `WebSocket` class. +* Class-based WebSocket handling. diff --git a/docs/ru/docs/alternatives.md b/docs/ru/docs/alternatives.md index 6380bcc45..17b54aad2 100644 --- a/docs/ru/docs/alternatives.md +++ b/docs/ru/docs/alternatives.md @@ -417,7 +417,7 @@ Pydantic — это библиотека для определения вали /// -### Starlette { #starlette } +### Starlette { #starlette } Starlette — это лёгкий ASGI фреймворк/набор инструментов, идеально подходящий для создания высокопроизводительных asyncio‑сервисов. @@ -462,7 +462,7 @@ ASGI — это новый «стандарт», разрабатываемый /// -### Uvicorn { #uvicorn } +### Uvicorn { #uvicorn } Uvicorn — молниеносный ASGI-сервер, построенный на uvloop и httptools. diff --git a/docs/ru/docs/deployment/manually.md b/docs/ru/docs/deployment/manually.md index 37fed5780..93287372a 100644 --- a/docs/ru/docs/deployment/manually.md +++ b/docs/ru/docs/deployment/manually.md @@ -52,7 +52,7 @@ FastAPI использует стандарт для построения Python Есть несколько альтернатив, например: -* Uvicorn: высокопроизводительный ASGI‑сервер. +* Uvicorn: высокопроизводительный ASGI‑сервер. * Hypercorn: ASGI‑сервер, среди прочего совместимый с HTTP/2 и Trio. * Daphne: ASGI‑сервер, созданный для Django Channels. * Granian: HTTP‑сервер на Rust для Python‑приложений. diff --git a/docs/ru/docs/fastapi-cli.md b/docs/ru/docs/fastapi-cli.md index 156e3d200..72cf55e7b 100644 --- a/docs/ru/docs/fastapi-cli.md +++ b/docs/ru/docs/fastapi-cli.md @@ -52,7 +52,7 @@ FastAPI CLI берет путь к вашей Python-программе (нап Для работы в режиме продакшн вместо `fastapi dev` нужно использовать `fastapi run`. 🚀 -Внутри **FastAPI CLI** используется Uvicorn, высокопроизводительный, готовый к работе в продакшне ASGI-сервер. 😎 +Внутри **FastAPI CLI** используется Uvicorn, высокопроизводительный, готовый к работе в продакшне ASGI-сервер. 😎 ## `fastapi dev` { #fastapi-dev } diff --git a/docs/ru/docs/features.md b/docs/ru/docs/features.md index 91ffe331b..703ff951e 100644 --- a/docs/ru/docs/features.md +++ b/docs/ru/docs/features.md @@ -159,7 +159,7 @@ FastAPI включает в себя чрезвычайно простую в и ## Возможности Starlette { #starlette-features } -**FastAPI** основан на Starlette и полностью совместим с ним. Так что любой дополнительный код Starlette, который у вас есть, также будет работать. +**FastAPI** основан на Starlette и полностью совместим с ним. Так что любой дополнительный код Starlette, который у вас есть, также будет работать. На самом деле, `FastAPI` — это подкласс `Starlette`. Таким образом, если вы уже знаете или используете Starlette, большая часть функционала будет работать так же. diff --git a/docs/ru/docs/history-design-future.md b/docs/ru/docs/history-design-future.md index d679af3e3..9cdd53376 100644 --- a/docs/ru/docs/history-design-future.md +++ b/docs/ru/docs/history-design-future.md @@ -58,7 +58,7 @@ По моим предложениям был изменён код этого фреймворка, чтобы сделать его полностью совместимым с JSON Schema, поддержать различные способы определения ограничений и улучшить поддержку в редакторах кода (проверки типов, автозавершение) на основе тестов в нескольких редакторах. -В то же время, я принимал участие в разработке **Starlette**, ещё один из основных компонентов FastAPI. +В то же время, я принимал участие в разработке **Starlette**, ещё один из основных компонентов FastAPI. ## Разработка { #development } diff --git a/docs/ru/docs/how-to/custom-request-and-route.md b/docs/ru/docs/how-to/custom-request-and-route.md index df8a5ee3c..1b8d7f7ed 100644 --- a/docs/ru/docs/how-to/custom-request-and-route.md +++ b/docs/ru/docs/how-to/custom-request-and-route.md @@ -66,7 +66,7 @@ Именно этих двух компонентов — `scope` и `receive` — достаточно, чтобы создать новый экземпляр `Request`. -Чтобы узнать больше о `Request`, см. документацию Starlette о запросах. +Чтобы узнать больше о `Request`, см. документацию Starlette о запросах. /// diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index 1fcc9ea9d..75cd63223 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -123,7 +123,7 @@ FastAPI — это современный, быстрый (высокопрои FastAPI стоит на плечах гигантов: -* Starlette для части, связанной с вебом. +* Starlette для части, связанной с вебом. * Pydantic для части, связанной с данными. ## Установка { #installation } @@ -229,7 +229,7 @@ INFO: Application startup complete.
О команде fastapi dev main.py... -Команда `fastapi dev` читает ваш файл `main.py`, находит в нём приложение **FastAPI** и запускает сервер с помощью Uvicorn. +Команда `fastapi dev` читает ваш файл `main.py`, находит в нём приложение **FastAPI** и запускает сервер с помощью Uvicorn. По умолчанию `fastapi dev` запускается с включённой авто-перезагрузкой для локальной разработки. @@ -470,7 +470,7 @@ FastAPI зависит от Pydantic и Starlette. Используется FastAPI: -* uvicorn — сервер, который загружает и обслуживает ваше приложение. Включает `uvicorn[standard]`, содержащий некоторые зависимости (например, `uvloop`), нужные для высокой производительности. +* uvicorn — сервер, который загружает и обслуживает ваше приложение. Включает `uvicorn[standard]`, содержащий некоторые зависимости (например, `uvloop`), нужные для высокой производительности. * `fastapi-cli[standard]` — чтобы предоставить команду `fastapi`. * Включает `fastapi-cloud-cli`, который позволяет развернуть ваше приложение FastAPI в FastAPI Cloud. diff --git a/docs/ru/docs/tutorial/background-tasks.md b/docs/ru/docs/tutorial/background-tasks.md index 9b6f0c8d3..1ed8522d6 100644 --- a/docs/ru/docs/tutorial/background-tasks.md +++ b/docs/ru/docs/tutorial/background-tasks.md @@ -61,7 +61,7 @@ ## Технические детали { #technical-details } -Класс `BackgroundTasks` приходит напрямую из `starlette.background`. +Класс `BackgroundTasks` приходит напрямую из `starlette.background`. Он импортируется/включается прямо в FastAPI, чтобы вы могли импортировать его из `fastapi` и избежать случайного импорта альтернативного `BackgroundTask` (без `s` на конце) из `starlette.background`. @@ -69,7 +69,7 @@ По‑прежнему можно использовать один `BackgroundTask` в FastAPI, но тогда вам нужно создать объект в своём коде и вернуть Starlette `Response`, включающий его. -Подробнее см. в официальной документации Starlette по фоновым задачам. +Подробнее см. в официальной документации Starlette по фоновым задачам. ## Предостережение { #caveat } diff --git a/docs/ru/docs/tutorial/first-steps.md b/docs/ru/docs/tutorial/first-steps.md index 9cdf76f5d..c82118cbe 100644 --- a/docs/ru/docs/tutorial/first-steps.md +++ b/docs/ru/docs/tutorial/first-steps.md @@ -155,7 +155,7 @@ OpenAPI определяет схему API для вашего API. И эта `FastAPI` — это класс, который напрямую наследуется от `Starlette`. -Вы можете использовать весь функционал Starlette и в `FastAPI`. +Вы можете использовать весь функционал Starlette и в `FastAPI`. /// diff --git a/docs/ru/docs/tutorial/handling-errors.md b/docs/ru/docs/tutorial/handling-errors.md index 33b7babf5..2378c8b04 100644 --- a/docs/ru/docs/tutorial/handling-errors.md +++ b/docs/ru/docs/tutorial/handling-errors.md @@ -81,7 +81,7 @@ ## Установка пользовательских обработчиков исключений { #install-custom-exception-handlers } -Вы можете добавить пользовательские обработчики исключений с помощью то же самое исключение - утилиты от Starlette. +Вы можете добавить пользовательские обработчики исключений с помощью то же самое исключение - утилиты от Starlette. Допустим, у вас есть пользовательское исключение `UnicornException`, которое вы (или используемая вами библиотека) можете `вызвать`. diff --git a/docs/ru/docs/tutorial/middleware.md b/docs/ru/docs/tutorial/middleware.md index ea535a151..5803b398b 100644 --- a/docs/ru/docs/tutorial/middleware.md +++ b/docs/ru/docs/tutorial/middleware.md @@ -39,7 +39,7 @@ Имейте в виду, что можно добавлять свои собственные заголовки при помощи префикса 'X-'. -Если же вы хотите добавить собственные заголовки, которые клиент сможет увидеть в браузере, то вам потребуется добавить их в настройки CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}), используя параметр `expose_headers`, см. документацию Starlette's CORS docs. +Если же вы хотите добавить собственные заголовки, которые клиент сможет увидеть в браузере, то вам потребуется добавить их в настройки CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}), используя параметр `expose_headers`, см. документацию Starlette's CORS docs. /// diff --git a/docs/ru/docs/tutorial/static-files.md b/docs/ru/docs/tutorial/static-files.md index 282c84db1..8455aea0a 100644 --- a/docs/ru/docs/tutorial/static-files.md +++ b/docs/ru/docs/tutorial/static-files.md @@ -38,4 +38,4 @@ OpenAPI и документация из вашего главного прил ## Больше информации { #more-info } -Для получения дополнительной информации о деталях и настройках ознакомьтесь с Документацией Starlette о статических файлах. +Для получения дополнительной информации о деталях и настройках ознакомьтесь с Документацией Starlette о статических файлах. diff --git a/docs/ru/docs/tutorial/testing.md b/docs/ru/docs/tutorial/testing.md index 94e9ae8ae..0224798b1 100644 --- a/docs/ru/docs/tutorial/testing.md +++ b/docs/ru/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # Тестирование { #testing } -Благодаря Starlette, тестировать приложения **FastAPI** легко и приятно. +Благодаря Starlette, тестировать приложения **FastAPI** легко и приятно. Тестирование основано на библиотеке HTTPX, которая в свою очередь основана на библиотеке Requests, так что все действия знакомы и интуитивно понятны. diff --git a/docs/ru/llm-prompt.md b/docs/ru/llm-prompt.md new file mode 100644 index 000000000..6a437bdd1 --- /dev/null +++ b/docs/ru/llm-prompt.md @@ -0,0 +1,94 @@ +Translate to Russian (русский язык). + +Language code: ru. + +--- + +Use a neutral tone (not overly formal or informal). + +Use correct Russian grammar — appropriate cases, suffixes, and endings depending on context. + +For the following technical terms, use these specific translations to ensure consistency and clarity across the documentation: + +* production (meaning production software or environment): продакшн (do not change the ending, for example, translate `in production` as `в продакшн` (not `в продакшене`)) +* completion (meaning code auto-completion): автозавершение +* editor (meaning component of IDE): редактор кода +* adopt (meaning start to use): использовать (or `начать использовать`) +* headers (meaning HTTP-headers): HTTP-заголовки +* cookie sessions: сессии с использованием cookie +* tested (adjective): протестированный +* middleware: middleware (don't translate, but add `промежуточный слой` if clarification is needed) +* path operation: операция пути (optionally clarify as `обработчик пути`) +* path operation function: функция-обработчик пути (or `функция обработки пути`) +* proprietary: проприетарный +* benchmark: бенчмарк (add (`тест производительности`) if clarification is needed or use just `тест производительности`) +* ASGI server: ASGI-сервер +* In a hurry? : Нет времени? +* response status code: статус-код ответа +* HTTP status code: HTTP статус-код +* issue (meaning GitHub issue): Issue (add `тикет\обращение` if clarification is needed) +* PR (meaning GitHub pull request): пулл-реквест (add `запрос на изменение` if clarification is needed) +* run (meaning run the code): запустить (or `прогнать` if it's about testing the program) +* to reach users: донести до пользователей (or `привлечь внимание пользователей` in the promotion context) +* body (meaning HTTP request body): тело запроса +* body (meaning HTTP response body): тело ответа +* body parameter : body-параметр (or `параметр тела запроса`) +* validate: валидировать (or `выполнить валидацию`) +* requirements (meaning dependencies): зависимости +* auto-reload: авто-перезагрузка (or `перезагрузить автоматически` if used as a verb) +* show (meaning show on the screen): отобразить +* parsing (noun): парсинг +* origin (in web development): origin (add `источник` if clarification is needed) +* include: включать (add `в себя` if it's appropriate, or use `содержать` as an alternative) +* virtual environment: виртуальное окружение +* framework: фреймворк +* path paremeter: path-параметр +* path (as in URL path): путь +* form (as in HTML form): форма +* media type: тип содержимого (or `медиа-тип`) +* request: HTTP-запрос +* response: HTTP-ответ +* type hints: аннотации типов +* type annotations: аннотации типов +* context manager: менеджер контекста +* code base: кодовая база +* instantiate: создать экземпляр (avoid "инстанцировать") +* load balancer: балансировщик нагрузки +* load balance: балансировка нагрузки +* worker process: воркер-процесс (or `процесс воркера`) +* worker: воркер +* lifespan: lifespan (do not translate when it's about lifespan events, but translate as `жизненный цикл` or `срок жизни` in other cases) +* mount (verb): монтировать +* mount (noun): точка монтирования / mount (keep in English if it's a FastAPI keyword) +* plugin: плагин +* plug-in: плагин +* full stack: full stack (do not translate) +* full-stack: full-stack (do not translate) +* loop (as in async loop): цикл событий +* Machine Learning: Машинное обучение +* Deep Learning: Глубокое обучение +* callback hell: callback hell (clarify as `ад обратных вызовов`) +* on the fly: на лету +* scratch the surface: поверхностно ознакомиться +* tip: совет (or `подсказка` depending on context) +* Pydantic model: Pydantic-модель (`модель Pydantic` and `Pydantic модель` are also fine) +* declare: объявить +* have the next best performance, after: быть на следующем месте по производительности после +* timing attack: тайминговая атака (clarify `атака по времени` if needed) +* OAuth2 scope: OAuth2 scope (clarify `область` if needed) +* TLS Termination Proxy: прокси-сервер TSL-терминации +* utilize (resources): использовать +* сontent: содержимое (or `контент`) +* raise exception: вызвать исключение (also possible to use `сгенерировать исключение` or `выбросить исключение`) +* password flow: password flow (clarify as `аутентификация по паролю` if needed) +* tutorial: руководство (or `учебник`) +* too long; didn't read: слишком длинно; не читал +* proxy with a stripped path prefix: прокси с функцией удаления префикса пути +* nerd: умник +* sub application: подприложение +* webhook request: вебхук-запрос +* serve (meaning providing access to something): «отдавать» (or `предоставлять доступ к`) +* recap (noun): резюме +* utility function: вспомогательная функция + +Do not add whitespace in `т.д.`, `т.п.`. diff --git a/docs/tr/docs/advanced/testing-websockets.md b/docs/tr/docs/advanced/testing-websockets.md index ddacca449..effe557d1 100644 --- a/docs/tr/docs/advanced/testing-websockets.md +++ b/docs/tr/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ Bu işlem için, `TestClient`'ı bir `with` ifadesinde kullanarak WebSocket'e ba /// note | Not -Daha fazla detay için Starlette'in Websockets'i Test Etmek dokümantasyonunu inceleyin. +Daha fazla detay için Starlette'in Websockets'i Test Etmek dokümantasyonunu inceleyin. /// diff --git a/docs/tr/docs/alternatives.md b/docs/tr/docs/alternatives.md index c98b966b5..9b603ea81 100644 --- a/docs/tr/docs/alternatives.md +++ b/docs/tr/docs/alternatives.md @@ -415,7 +415,7 @@ Bütün veri doğrulama, veri dönüştürme ve JSON Şemasına bağlı otomatik /// -### Starlette +### Starlette Starlette hafif bir ASGI framework'ü ve yüksek performanslı asyncio servisleri oluşturmak için ideal. @@ -460,7 +460,7 @@ Yani, Starlette ile yapabileceğiniz her şeyi, Starlette'in bir nevi güçlendi /// -### Uvicorn +### Uvicorn Uvicorn, uvlook ile httptools üzerine kurulu ışık hzında bir ASGI sunucusudur. diff --git a/docs/tr/docs/features.md b/docs/tr/docs/features.md index 5d40b1086..86085c5e9 100644 --- a/docs/tr/docs/features.md +++ b/docs/tr/docs/features.md @@ -166,7 +166,7 @@ Bütün entegrasyonlar kullanımı kolay olmak üzere (zorunluluklar ile beraber ## Starlette özellikleri -**FastAPI**, Starlette ile tamamiyle uyumlu ve üzerine kurulu. Yani FastAPI üzerine ekleme yapacağınız herhangi bir Starlette kodu da çalışacaktır. +**FastAPI**, Starlette ile tamamiyle uyumlu ve üzerine kurulu. Yani FastAPI üzerine ekleme yapacağınız herhangi bir Starlette kodu da çalışacaktır. `FastAPI` aslında `Starlette`'nin bir sub-class'ı. Eğer Starlette'nin nasıl kullanılacağını biliyor isen, çoğu işlevini aynı şekilde yapıyor. diff --git a/docs/tr/docs/history-design-future.md b/docs/tr/docs/history-design-future.md index 8b2662bc3..cad290828 100644 --- a/docs/tr/docs/history-design-future.md +++ b/docs/tr/docs/history-design-future.md @@ -58,7 +58,7 @@ Hepsi, tüm geliştiriciler için en iyi geliştirme deneyimini sağlayacak şek Sonra, JSON Schema ile tamamen uyumlu olmasını sağlamak, kısıtlama bildirimlerini tanımlamanın farklı yollarını desteklemek ve birkaç editördeki testlere dayanarak editör desteğini (tip kontrolleri, otomatik tamamlama) geliştirmek için katkıda bulundum. -Geliştirme sırasında, diğer ana gereksinim olan **Starlette**'e de katkıda bulundum. +Geliştirme sırasında, diğer ana gereksinim olan **Starlette**'e de katkıda bulundum. ## Geliştirme diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index c7a2b2fbd..516d5959e 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -123,7 +123,7 @@ Eğer API yerine, terminalde kullanılmak üzere bir Starlette. +* Web tarafı için Starlette. * Data tarafı için Pydantic. ## Kurulum @@ -138,7 +138,7 @@ $ pip install fastapi
-Uygulamamızı kullanılabilir hale getirmek için Uvicorn ya da Hypercorn gibi bir ASGI sunucusuna ihtiyacımız olacak. +Uygulamamızı kullanılabilir hale getirmek için Uvicorn ya da Hypercorn gibi bir ASGI sunucusuna ihtiyacımız olacak.
@@ -463,7 +463,7 @@ Starlette tarafında kullanılan: Hem FastAPI hem de Starlette tarafından kullanılan: -* uvicorn - oluşturduğumuz uygulamayı servis edecek web sunucusu görevini üstlenir. +* uvicorn - oluşturduğumuz uygulamayı servis edecek web sunucusu görevini üstlenir. * orjson - `ORJSONResponse` kullanacaksanız gereklidir. * ujson - `UJSONResponse` kullanacaksanız gerekli. diff --git a/docs/tr/docs/tutorial/first-steps.md b/docs/tr/docs/tutorial/first-steps.md index 2d2949b50..9a8ef762d 100644 --- a/docs/tr/docs/tutorial/first-steps.md +++ b/docs/tr/docs/tutorial/first-steps.md @@ -139,7 +139,7 @@ Ayrıca, API'ınızla iletişim kuracak önyüz, mobil veya IoT uygulamaları gi `FastAPI` doğrudan `Starlette`'i miras alan bir sınıftır. -Starlette'in tüm işlevselliğini `FastAPI` ile de kullanabilirsiniz. +Starlette'in tüm işlevselliğini `FastAPI` ile de kullanabilirsiniz. /// diff --git a/docs/tr/docs/tutorial/static-files.md b/docs/tr/docs/tutorial/static-files.md index db30f13bc..4542aca77 100644 --- a/docs/tr/docs/tutorial/static-files.md +++ b/docs/tr/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ Bu parametrelerin hepsi "`static`"den farklı olabilir, bunları kendi uygulaman ## Daha Fazla Bilgi -Daha fazla detay ve seçenek için Starlette'in Statik Dosyalar hakkındaki dokümantasyonunu incelleyin. +Daha fazla detay ve seçenek için Starlette'in Statik Dosyalar hakkındaki dokümantasyonunu incelleyin. diff --git a/docs/uk/docs/alternatives.md b/docs/uk/docs/alternatives.md index 1acbe237a..786df45c5 100644 --- a/docs/uk/docs/alternatives.md +++ b/docs/uk/docs/alternatives.md @@ -415,7 +415,7 @@ Pydantic — це бібліотека для визначення переві /// -### Starlette +### Starlette Starlette — це легкий фреймворк/набір інструментів ASGI, який ідеально підходить для створення високопродуктивних asyncio сервісів. @@ -460,7 +460,7 @@ ASGI — це новий «стандарт», який розробляєтьс /// -### Uvicorn +### Uvicorn Uvicorn — це блискавичний сервер ASGI, побудований на uvloop і httptools. diff --git a/docs/uk/docs/fastapi-cli.md b/docs/uk/docs/fastapi-cli.md index 6bbbbc326..f18b10471 100644 --- a/docs/uk/docs/fastapi-cli.md +++ b/docs/uk/docs/fastapi-cli.md @@ -60,7 +60,7 @@ FastAPI CLI приймає шлях до Вашої Python програми (н Натомість, для запуску у продакшн використовуйте `fastapi run`. 🚀 -Всередині **FastAPI CLI** використовує Uvicorn, високопродуктивний, production-ready, ASGI cервер. 😎 +Всередині **FastAPI CLI** використовує Uvicorn, високопродуктивний, production-ready, ASGI cервер. 😎 ## `fastapi dev` diff --git a/docs/uk/docs/features.md b/docs/uk/docs/features.md index 7d679d8ee..aa0ef7c79 100644 --- a/docs/uk/docs/features.md +++ b/docs/uk/docs/features.md @@ -147,7 +147,7 @@ FastAPI має розумні налаштування **за замовчува ## Можливості Starlette -**FastAPI** повністю сумісний із (та побудований на основі) Starlette. Тому будь-який додатковий код Starlette, який ви маєте, також працюватиме. +**FastAPI** повністю сумісний із (та побудований на основі) Starlette. Тому будь-який додатковий код Starlette, який ви маєте, також працюватиме. **FastAPI** фактично є підкласом **Starlette**. Тому, якщо ви вже знайомі зі Starlette або використовуєте його, більшість функціональності працюватиме так само. diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md index 7e919e257..0811a4c7b 100644 --- a/docs/uk/docs/index.md +++ b/docs/uk/docs/index.md @@ -112,7 +112,7 @@ FastAPI - це сучасний, швидкий (високопродуктив FastAPI стоїть на плечах гігантів: -* Starlette для web частини. +* Starlette для web частини. * Pydantic для частини даних. ## Вставновлення @@ -127,7 +127,7 @@ $ pip install fastapi
-Вам також знадобиться сервер ASGI для продакшину, наприклад Uvicorn або Hypercorn. +Вам також знадобиться сервер ASGI для продакшину, наприклад Uvicorn або Hypercorn.
@@ -452,7 +452,7 @@ Starlette використовує: FastAPI / Starlette використовують: -* uvicorn - для сервера, який завантажує та обслуговує вашу програму. +* uvicorn - для сервера, який завантажує та обслуговує вашу програму. * orjson - Необхідно, якщо Ви хочете використовувати `ORJSONResponse`. * ujson - Необхідно, якщо Ви хочете використовувати `UJSONResponse`. diff --git a/docs/uk/docs/tutorial/background-tasks.md b/docs/uk/docs/tutorial/background-tasks.md index 912ba8c2a..0a9349650 100644 --- a/docs/uk/docs/tutorial/background-tasks.md +++ b/docs/uk/docs/tutorial/background-tasks.md @@ -62,7 +62,7 @@ ## Технічні деталі -Клас `BackgroundTasks` походить безпосередньо з `starlette.background`. +Клас `BackgroundTasks` походить безпосередньо з `starlette.background`. Він імпортується безпосередньо у FastAPI, щоб Ви могли використовувати його з `fastapi` і випадково не імпортували `BackgroundTask` (без s в кінці) з `starlette.background`. @@ -70,7 +70,7 @@ Також можна використовувати `BackgroundTask` окремо в FastAPI, але для цього Вам доведеться створити об'єкт у коді та повернути Starlette `Response`, включаючи його. -Детальніше можна почитати в офіційній документації Starlette про фонові задачі . +Детальніше можна почитати в офіційній документації Starlette про фонові задачі . ## Застереження diff --git a/docs/uk/docs/tutorial/first-steps.md b/docs/uk/docs/tutorial/first-steps.md index e910c4ccc..3f861cb48 100644 --- a/docs/uk/docs/tutorial/first-steps.md +++ b/docs/uk/docs/tutorial/first-steps.md @@ -163,7 +163,7 @@ OpenAPI описує схему для вашого API. І ця схема вк `FastAPI` це клас, який успадковується безпосередньо від `Starlette`. -Ви також можете використовувати всю функціональність Starlette у `FastAPI`. +Ви також можете використовувати всю функціональність Starlette у `FastAPI`. /// diff --git a/docs/uk/docs/tutorial/handling-errors.md b/docs/uk/docs/tutorial/handling-errors.md index 12a356cd0..32de73b2a 100644 --- a/docs/uk/docs/tutorial/handling-errors.md +++ b/docs/uk/docs/tutorial/handling-errors.md @@ -81,7 +81,7 @@ ## Встановлення власних обробників помилок -Ви можете додати власні обробники помилок за допомогою тих самих утиліт обробки помилок зі Starlette. +Ви можете додати власні обробники помилок за допомогою тих самих утиліт обробки помилок зі Starlette. Припустимо, у Вас є власний обʼєкт помилки `UnicornException`, яке Ви (або бібліотека, яку Ви використовуєте) може `згенерувати` (`raise`). diff --git a/docs/uk/docs/tutorial/middleware.md b/docs/uk/docs/tutorial/middleware.md index 807be484a..13ce8573d 100644 --- a/docs/uk/docs/tutorial/middleware.md +++ b/docs/uk/docs/tutorial/middleware.md @@ -39,7 +39,7 @@ Не забувайте, що власні заголовки можна додавати, використовуючи префікс 'X-'. -Але якщо у Вас є власні заголовки, які Ви хочете, щоб браузерний клієнт міг побачити, потрібно додати їх до Вашої конфігурації CORS (див. [CORS (Обмін ресурсами між різними джерелами)](cors.md){.internal-link target=_blank} за допомогою параметра `expose_headers`, описаного в документації Starlette по CORS. +Але якщо у Вас є власні заголовки, які Ви хочете, щоб браузерний клієнт міг побачити, потрібно додати їх до Вашої конфігурації CORS (див. [CORS (Обмін ресурсами між різними джерелами)](cors.md){.internal-link target=_blank} за допомогою параметра `expose_headers`, описаного в документації Starlette по CORS. /// diff --git a/docs/uk/docs/tutorial/static-files.md b/docs/uk/docs/tutorial/static-files.md index a84782d8f..3427f2376 100644 --- a/docs/uk/docs/tutorial/static-files.md +++ b/docs/uk/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ ## Додаткова інформація -Детальніше про налаштування та можливості можна дізнатися в документації Starlette про статичні файли. +Детальніше про налаштування та можливості можна дізнатися в документації Starlette про статичні файли. diff --git a/docs/uk/docs/tutorial/testing.md b/docs/uk/docs/tutorial/testing.md index 25fc370d6..1105c6b0a 100644 --- a/docs/uk/docs/tutorial/testing.md +++ b/docs/uk/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # Тестування -Тестування **FastAPI** додатків є простим та ефективним завдяки бібліотеці Starlette, яка базується на HTTPX. +Тестування **FastAPI** додатків є простим та ефективним завдяки бібліотеці Starlette, яка базується на HTTPX. Оскільки HTTPX розроблений на основі Requests, його API є інтуїтивно зрозумілим для тих, хто вже знайомий з Requests. З його допомогою Ви можете використовувати pytest безпосередньо з **FastAPI**. diff --git a/docs/vi/docs/fastapi-cli.md b/docs/vi/docs/fastapi-cli.md index d9e315ae4..e758f4d3a 100644 --- a/docs/vi/docs/fastapi-cli.md +++ b/docs/vi/docs/fastapi-cli.md @@ -52,7 +52,7 @@ FastAPI CLI nhận đường dẫn đến chương trình Python của bạn (vd Đối với vận hành thực tế (production), bạn sẽ sử dụng `fastapi run` thay thế. 🚀 -Ở bên trong, **FastAPI CLI** sử dụng Uvicorn, một server ASGI có hiệu suất cao, sẵn sàng cho vận hành thực tế (production). 😎 +Ở bên trong, **FastAPI CLI** sử dụng Uvicorn, một server ASGI có hiệu suất cao, sẵn sàng cho vận hành thực tế (production). 😎 ## `fastapi dev` diff --git a/docs/vi/docs/index.md b/docs/vi/docs/index.md index e7df2bf72..a5ac1bfb7 100644 --- a/docs/vi/docs/index.md +++ b/docs/vi/docs/index.md @@ -124,7 +124,7 @@ Nếu bạn đang xây dựng một CLIStarlette cho phần web. +* Starlette cho phần web. * Pydantic cho phần data. ## Cài đặt @@ -139,7 +139,7 @@ $ pip install fastapi
-Bạn cũng sẽ cần một ASGI server cho production như Uvicorn hoặc Hypercorn. +Bạn cũng sẽ cần một ASGI server cho production như Uvicorn hoặc Hypercorn.
@@ -464,7 +464,7 @@ Sử dụng Starlette: Sử dụng bởi FastAPI / Starlette: -* uvicorn - Server để chạy ứng dụng của bạn. +* uvicorn - Server để chạy ứng dụng của bạn. * orjson - Bắt buộc nếu bạn muốn sử dụng `ORJSONResponse`. * ujson - Bắt buộc nếu bạn muốn sử dụng `UJSONResponse`. diff --git a/docs/vi/docs/tutorial/first-steps.md b/docs/vi/docs/tutorial/first-steps.md index 901c8fd59..d1650539c 100644 --- a/docs/vi/docs/tutorial/first-steps.md +++ b/docs/vi/docs/tutorial/first-steps.md @@ -139,7 +139,7 @@ Bạn cũng có thể sử dụng nó để sinh code tự động, với các c `FastAPI` là một class kế thừa trực tiếp `Starlette`. -Bạn cũng có thể sử dụng tất cả Starlette chức năng với `FastAPI`. +Bạn cũng có thể sử dụng tất cả Starlette chức năng với `FastAPI`. /// diff --git a/docs/vi/docs/tutorial/static-files.md b/docs/vi/docs/tutorial/static-files.md index ecf8c2485..1bbec29e7 100644 --- a/docs/vi/docs/tutorial/static-files.md +++ b/docs/vi/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ Tất cả các tham số này có thể khác với `static`, điều chỉnh c ## Thông tin thêm -Để biết thêm chi tiết và tùy chọn, hãy xem Starlette's docs about Static Files. +Để biết thêm chi tiết và tùy chọn, hãy xem Starlette's docs about Static Files. diff --git a/docs/zh-hant/docs/fastapi-cli.md b/docs/zh-hant/docs/fastapi-cli.md index 3c644ce46..b107e7e73 100644 --- a/docs/zh-hant/docs/fastapi-cli.md +++ b/docs/zh-hant/docs/fastapi-cli.md @@ -60,7 +60,7 @@ FastAPI CLI 接收你的 Python 程式路徑(例如 `main.py`),並自動 在生產環境,你應該使用 `fastapi run` 命令。 🚀 -**FastAPI CLI** 內部使用了 Uvicorn,這是一個高效能、適合生產環境的 ASGI 伺服器。 😎 +**FastAPI CLI** 內部使用了 Uvicorn,這是一個高效能、適合生產環境的 ASGI 伺服器。 😎 ## `fastapi dev` diff --git a/docs/zh-hant/docs/features.md b/docs/zh-hant/docs/features.md index 3a1392b51..f44d28a7f 100644 --- a/docs/zh-hant/docs/features.md +++ b/docs/zh-hant/docs/features.md @@ -167,7 +167,7 @@ FastAPI 有一個使用簡單,但是非常強大的Starlette的CORS文档中记录的`expose_headers`参数。 +但是,如果你有自定义头部,你希望浏览器中的客户端能够看到它们,你需要将它们添加到你的CORS配置中(在[CORS(跨源资源共享)](../tutorial/cors.md){.internal-link target=_blank}中阅读更多),使用在Starlette的CORS文档中记录的`expose_headers`参数。 diff --git a/docs/zh/docs/advanced/templates.md b/docs/zh/docs/advanced/templates.md index 8b7019ede..e627eed98 100644 --- a/docs/zh/docs/advanced/templates.md +++ b/docs/zh/docs/advanced/templates.md @@ -122,4 +122,4 @@ Item ID: 42 ## 更多说明 -包括测试模板等更多详情,请参阅 Starlette 官方文档 - 模板。 +包括测试模板等更多详情,请参阅 Starlette 官方文档 - 模板。 diff --git a/docs/zh/docs/advanced/testing-websockets.md b/docs/zh/docs/advanced/testing-websockets.md index 5d713d5f7..b84647a3e 100644 --- a/docs/zh/docs/advanced/testing-websockets.md +++ b/docs/zh/docs/advanced/testing-websockets.md @@ -8,6 +8,6 @@ /// note | 笔记 -更多细节详见 Starlette 官档 - 测试 WebSockets。 +更多细节详见 Starlette 官档 - 测试 WebSockets。 /// diff --git a/docs/zh/docs/advanced/using-request-directly.md b/docs/zh/docs/advanced/using-request-directly.md index db0fcafdf..a9658c034 100644 --- a/docs/zh/docs/advanced/using-request-directly.md +++ b/docs/zh/docs/advanced/using-request-directly.md @@ -15,7 +15,7 @@ ## `Request` 对象的细节 -实际上,**FastAPI** 的底层是 **Starlette**,**FastAPI** 只不过是在 **Starlette** 顶层提供了一些工具,所以能直接使用 Starlette 的 `Request` 对象。 +实际上,**FastAPI** 的底层是 **Starlette**,**FastAPI** 只不过是在 **Starlette** 顶层提供了一些工具,所以能直接使用 Starlette 的 `Request` 对象。 但直接从 `Request` 对象提取数据时(例如,读取请求体),**FastAPI** 不会验证、转换和存档数据(为 API 文档使用 OpenAPI)。 @@ -45,7 +45,7 @@ ## `Request` 文档 -更多细节详见 Starlette 官档 - `Request` 对象。 +更多细节详见 Starlette 官档 - `Request` 对象。 /// note | 技术细节 diff --git a/docs/zh/docs/advanced/websockets.md b/docs/zh/docs/advanced/websockets.md index d91aacc03..005ed9242 100644 --- a/docs/zh/docs/advanced/websockets.md +++ b/docs/zh/docs/advanced/websockets.md @@ -172,5 +172,5 @@ Client #1596980209979 left the chat 要了解更多选项,请查看 Starlette 的文档: -* [WebSocket 类](https://www.starlette.io/websockets/) -* [基于类的 WebSocket 处理](https://www.starlette.io/endpoints/#websocketendpoint)。 +* [WebSocket 类](https://www.starlette.dev/websockets/) +* [基于类的 WebSocket 处理](https://www.starlette.dev/endpoints/#websocketendpoint)。 diff --git a/docs/zh/docs/deployment/manually.md b/docs/zh/docs/deployment/manually.md index 3dc5942e3..2c2784a64 100644 --- a/docs/zh/docs/deployment/manually.md +++ b/docs/zh/docs/deployment/manually.md @@ -52,7 +52,7 @@ FastAPI 使用了一种用于构建 Python Web 框架和服务器的标准,称 除此之外,还有其他一些可选的 ASGI 服务器,例如: -* Uvicorn:高性能 ASGI 服务器。 +* Uvicorn:高性能 ASGI 服务器。 * Hypercorn:与 HTTP/2 和 Trio 等兼容的 ASGI 服务器。 * Daphne:为 Django Channels 构建的 ASGI 服务器。 * Granian:基于 Rust 的 HTTP 服务器,专为 Python 应用设计。 diff --git a/docs/zh/docs/fastapi-cli.md b/docs/zh/docs/fastapi-cli.md index 8a70e1d80..3b67eb664 100644 --- a/docs/zh/docs/fastapi-cli.md +++ b/docs/zh/docs/fastapi-cli.md @@ -52,7 +52,7 @@ FastAPI CLI 接收你的 Python 程序路径,自动检测包含 FastAPI 的变 在生产环境中,你应该使用 `fastapi run` 命令。🚀 -在内部,**FastAPI CLI** 使用了 Uvicorn,这是一个高性能、适用于生产环境的 ASGI 服务器。😎 +在内部,**FastAPI CLI** 使用了 Uvicorn,这是一个高性能、适用于生产环境的 ASGI 服务器。😎 ## `fastapi dev` diff --git a/docs/zh/docs/features.md b/docs/zh/docs/features.md index 24dc3e8ce..eaf8daff7 100644 --- a/docs/zh/docs/features.md +++ b/docs/zh/docs/features.md @@ -165,7 +165,7 @@ FastAPI 有一个使用非常简单,但是非常强大的. +更多细节查看 Starlette's official docs for Background Tasks. ## 告诫 diff --git a/docs/zh/docs/tutorial/first-steps.md b/docs/zh/docs/tutorial/first-steps.md index 80a34116a..2d7c35c8c 100644 --- a/docs/zh/docs/tutorial/first-steps.md +++ b/docs/zh/docs/tutorial/first-steps.md @@ -155,7 +155,7 @@ OpenAPI 为你的 API 定义 API 模式。该模式中包含了你的 API 发送 `FastAPI` 是直接从 `Starlette` 继承的类。 -你可以通过 `FastAPI` 使用所有的 Starlette 的功能。 +你可以通过 `FastAPI` 使用所有的 Starlette 的功能。 /// diff --git a/docs/zh/docs/tutorial/handling-errors.md b/docs/zh/docs/tutorial/handling-errors.md index 0b887c292..ae667b74a 100644 --- a/docs/zh/docs/tutorial/handling-errors.md +++ b/docs/zh/docs/tutorial/handling-errors.md @@ -83,7 +83,7 @@ ## 安装自定义异常处理器 -添加自定义处理器,要使用 [Starlette 的异常工具](https://www.starlette.io/exceptions/)。 +添加自定义处理器,要使用 [Starlette 的异常工具](https://www.starlette.dev/exceptions/)。 假设要触发的自定义异常叫作 `UnicornException`。 diff --git a/docs/zh/docs/tutorial/middleware.md b/docs/zh/docs/tutorial/middleware.md index 258ca7482..5608c4ee1 100644 --- a/docs/zh/docs/tutorial/middleware.md +++ b/docs/zh/docs/tutorial/middleware.md @@ -37,7 +37,7 @@ 请记住可以 用'X-' 前缀添加专有自定义请求头. -但是如果你想让浏览器中的客户端看到你的自定义请求头, 你需要把它们加到 CORS 配置 ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) 的 `expose_headers` 参数中,在 Starlette's CORS docs文档中. +但是如果你想让浏览器中的客户端看到你的自定义请求头, 你需要把它们加到 CORS 配置 ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) 的 `expose_headers` 参数中,在 Starlette's CORS docs文档中. /// diff --git a/docs/zh/docs/tutorial/static-files.md b/docs/zh/docs/tutorial/static-files.md index c19079565..1a0d4504c 100644 --- a/docs/zh/docs/tutorial/static-files.md +++ b/docs/zh/docs/tutorial/static-files.md @@ -37,4 +37,4 @@ ## 更多信息 -更多细节和选择查阅 Starlette's docs about Static Files. +更多细节和选择查阅 Starlette's docs about Static Files. diff --git a/docs/zh/docs/tutorial/testing.md b/docs/zh/docs/tutorial/testing.md index 3e0c48caf..3877adbac 100644 --- a/docs/zh/docs/tutorial/testing.md +++ b/docs/zh/docs/tutorial/testing.md @@ -1,6 +1,6 @@ # 测试 -感谢 Starlette,测试**FastAPI** 应用轻松又愉快。 +感谢 Starlette,测试**FastAPI** 应用轻松又愉快。 它基于 HTTPX, 而HTTPX又是基于Requests设计的,所以很相似且易懂。 diff --git a/docs_src/dependencies/tutorial008e.py b/docs_src/dependencies/tutorial008e.py new file mode 100644 index 000000000..1ed056e91 --- /dev/null +++ b/docs_src/dependencies/tutorial008e.py @@ -0,0 +1,15 @@ +from fastapi import Depends, FastAPI + +app = FastAPI() + + +def get_username(): + try: + yield "Rick" + finally: + print("Cleanup up before response is sent") + + +@app.get("/users/me") +def get_user_me(username: str = Depends(get_username, scope="function")): + return username diff --git a/docs_src/dependencies/tutorial008e_an.py b/docs_src/dependencies/tutorial008e_an.py new file mode 100644 index 000000000..c8a0af2b3 --- /dev/null +++ b/docs_src/dependencies/tutorial008e_an.py @@ -0,0 +1,16 @@ +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +def get_username(): + try: + yield "Rick" + finally: + print("Cleanup up before response is sent") + + +@app.get("/users/me") +def get_user_me(username: Annotated[str, Depends(get_username, scope="function")]): + return username diff --git a/docs_src/dependencies/tutorial008e_an_py39.py b/docs_src/dependencies/tutorial008e_an_py39.py new file mode 100644 index 000000000..80a44c7e2 --- /dev/null +++ b/docs_src/dependencies/tutorial008e_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +def get_username(): + try: + yield "Rick" + finally: + print("Cleanup up before response is sent") + + +@app.get("/users/me") +def get_user_me(username: Annotated[str, Depends(get_username, scope="function")]): + return username diff --git a/docs_src/pydantic_v1_in_v2/tutorial001_an.py b/docs_src/pydantic_v1_in_v2/tutorial001_an.py new file mode 100644 index 000000000..62a4b2c21 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial001_an.py @@ -0,0 +1,9 @@ +from typing import Union + +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + size: float diff --git a/docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py b/docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py new file mode 100644 index 000000000..a8ec729b3 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py @@ -0,0 +1,7 @@ +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + size: float diff --git a/docs_src/pydantic_v1_in_v2/tutorial002_an.py b/docs_src/pydantic_v1_in_v2/tutorial002_an.py new file mode 100644 index 000000000..3c6a06080 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial002_an.py @@ -0,0 +1,18 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + size: float + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Item) -> Item: + return item diff --git a/docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py b/docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py new file mode 100644 index 000000000..4934e7004 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + size: float + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Item) -> Item: + return item diff --git a/docs_src/pydantic_v1_in_v2/tutorial003_an.py b/docs_src/pydantic_v1_in_v2/tutorial003_an.py new file mode 100644 index 000000000..117d6f7a4 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial003_an.py @@ -0,0 +1,25 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel as BaseModelV2 +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + size: float + + +class ItemV2(BaseModelV2): + name: str + description: Union[str, None] = None + size: float + + +app = FastAPI() + + +@app.post("/items/", response_model=ItemV2) +async def create_item(item: Item): + return item diff --git a/docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py b/docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py new file mode 100644 index 000000000..6e3013644 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py @@ -0,0 +1,23 @@ +from fastapi import FastAPI +from pydantic import BaseModel as BaseModelV2 +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + size: float + + +class ItemV2(BaseModelV2): + name: str + description: str | None = None + size: float + + +app = FastAPI() + + +@app.post("/items/", response_model=ItemV2) +async def create_item(item: Item): + return item diff --git a/docs_src/pydantic_v1_in_v2/tutorial004_an.py b/docs_src/pydantic_v1_in_v2/tutorial004_an.py new file mode 100644 index 000000000..cca8a9ea8 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial004_an.py @@ -0,0 +1,20 @@ +from typing import Union + +from fastapi import FastAPI +from fastapi.temp_pydantic_v1_params import Body +from pydantic.v1 import BaseModel +from typing_extensions import Annotated + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + size: float + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Annotated[Item, Body(embed=True)]) -> Item: + return item diff --git a/docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py b/docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py new file mode 100644 index 000000000..c251311e0 --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import FastAPI +from fastapi.temp_pydantic_v1_params import Body +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + size: float + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Annotated[Item, Body(embed=True)]) -> Item: + return item diff --git a/docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py b/docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py new file mode 100644 index 000000000..150ab20ae --- /dev/null +++ b/docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py @@ -0,0 +1,19 @@ +from typing import Annotated, Union + +from fastapi import FastAPI +from fastapi.temp_pydantic_v1_params import Body +from pydantic.v1 import BaseModel + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + size: float + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Annotated[Item, Body(embed=True)]) -> Item: + return item diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 03a5aaad5..f4a952bf5 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.118.0" +__version__ = "0.121.0" from starlette import status as status diff --git a/fastapi/_compat.py b/fastapi/_compat.py deleted file mode 100644 index 26b6638c8..000000000 --- a/fastapi/_compat.py +++ /dev/null @@ -1,665 +0,0 @@ -from collections import deque -from copy import copy -from dataclasses import dataclass, is_dataclass -from enum import Enum -from functools import lru_cache -from typing import ( - Any, - Callable, - Deque, - Dict, - FrozenSet, - List, - Mapping, - Sequence, - Set, - Tuple, - Type, - Union, - cast, -) - -from fastapi.exceptions import RequestErrorModel -from fastapi.types import IncEx, ModelNameMap, UnionType -from pydantic import BaseModel, create_model -from pydantic.version import VERSION as PYDANTIC_VERSION -from starlette.datastructures import UploadFile -from typing_extensions import Annotated, Literal, get_args, get_origin - -PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) -PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 - - -sequence_annotation_to_type = { - Sequence: list, - List: list, - list: list, - Tuple: tuple, - tuple: tuple, - Set: set, - set: set, - FrozenSet: frozenset, - frozenset: frozenset, - Deque: deque, - deque: deque, -} - -sequence_types = tuple(sequence_annotation_to_type.keys()) - -Url: Type[Any] - -if PYDANTIC_V2: - from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError - from pydantic import TypeAdapter - from pydantic import ValidationError as ValidationError - from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined] - GetJsonSchemaHandler as GetJsonSchemaHandler, - ) - from pydantic._internal._typing_extra import eval_type_lenient - from pydantic._internal._utils import lenient_issubclass as lenient_issubclass - from pydantic.fields import FieldInfo - from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema - from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue - from pydantic_core import CoreSchema as CoreSchema - from pydantic_core import PydanticUndefined, PydanticUndefinedType - from pydantic_core import Url as Url - - try: - from pydantic_core.core_schema import ( - with_info_plain_validator_function as with_info_plain_validator_function, - ) - except ImportError: # pragma: no cover - from pydantic_core.core_schema import ( - general_plain_validator_function as with_info_plain_validator_function, # noqa: F401 - ) - - RequiredParam = PydanticUndefined - Undefined = PydanticUndefined - UndefinedType = PydanticUndefinedType - evaluate_forwardref = eval_type_lenient - Validator = Any - - class BaseConfig: - pass - - class ErrorWrapper(Exception): - pass - - @dataclass - class ModelField: - field_info: FieldInfo - name: str - mode: Literal["validation", "serialization"] = "validation" - - @property - def alias(self) -> str: - a = self.field_info.alias - return a if a is not None else self.name - - @property - def required(self) -> bool: - return self.field_info.is_required() - - @property - def default(self) -> Any: - return self.get_default() - - @property - def type_(self) -> Any: - return self.field_info.annotation - - def __post_init__(self) -> None: - self._type_adapter: TypeAdapter[Any] = TypeAdapter( - Annotated[self.field_info.annotation, self.field_info] - ) - - def get_default(self) -> Any: - if self.field_info.is_required(): - return Undefined - return self.field_info.get_default(call_default_factory=True) - - def validate( - self, - value: Any, - values: Dict[str, Any] = {}, # noqa: B006 - *, - loc: Tuple[Union[int, str], ...] = (), - ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: - try: - return ( - self._type_adapter.validate_python(value, from_attributes=True), - None, - ) - except ValidationError as exc: - return None, _regenerate_error_with_loc( - errors=exc.errors(include_url=False), loc_prefix=loc - ) - - def serialize( - self, - value: Any, - *, - mode: Literal["json", "python"] = "json", - include: Union[IncEx, None] = None, - exclude: Union[IncEx, None] = None, - by_alias: bool = True, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - ) -> Any: - # What calls this code passes a value that already called - # self._type_adapter.validate_python(value) - return self._type_adapter.dump_python( - value, - mode=mode, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - - def __hash__(self) -> int: - # Each ModelField is unique for our purposes, to allow making a dict from - # ModelField to its JSON Schema. - return id(self) - - def get_annotation_from_field_info( - annotation: Any, field_info: FieldInfo, field_name: str - ) -> Any: - return annotation - - def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: - return errors # type: ignore[return-value] - - def _model_rebuild(model: Type[BaseModel]) -> None: - model.model_rebuild() - - def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any - ) -> Any: - return model.model_dump(mode=mode, **kwargs) - - def _get_model_config(model: BaseModel) -> Any: - return model.model_config - - def get_schema_from_model_field( - *, - field: ModelField, - schema_generator: GenerateJsonSchema, - model_name_map: ModelNameMap, - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - separate_input_output_schemas: bool = True, - ) -> Dict[str, Any]: - override_mode: Union[Literal["validation"], None] = ( - None if separate_input_output_schemas else "validation" - ) - # This expects that GenerateJsonSchema was already used to generate the definitions - json_schema = field_mapping[(field, override_mode or field.mode)] - if "$ref" not in json_schema: - # TODO remove when deprecating Pydantic v1 - # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 - json_schema["title"] = ( - field.field_info.title or field.alias.title().replace("_", " ") - ) - return json_schema - - def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: - return {} - - def get_definitions( - *, - fields: List[ModelField], - schema_generator: GenerateJsonSchema, - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, - ) -> Tuple[ - Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - Dict[str, Dict[str, Any]], - ]: - override_mode: Union[Literal["validation"], None] = ( - None if separate_input_output_schemas else "validation" - ) - inputs = [ - (field, override_mode or field.mode, field._type_adapter.core_schema) - for field in fields - ] - field_mapping, definitions = schema_generator.generate_definitions( - inputs=inputs - ) - for item_def in cast(Dict[str, Dict[str, Any]], definitions).values(): - if "description" in item_def: - item_description = cast(str, item_def["description"]).split("\f")[0] - item_def["description"] = item_description - return field_mapping, definitions # type: ignore[return-value] - - def is_scalar_field(field: ModelField) -> bool: - from fastapi import params - - return field_annotation_is_scalar( - field.field_info.annotation - ) and not isinstance(field.field_info, params.Body) - - def is_sequence_field(field: ModelField) -> bool: - return field_annotation_is_sequence(field.field_info.annotation) - - def is_scalar_sequence_field(field: ModelField) -> bool: - return field_annotation_is_scalar_sequence(field.field_info.annotation) - - def is_bytes_field(field: ModelField) -> bool: - return is_bytes_or_nonable_bytes_annotation(field.type_) - - def is_bytes_sequence_field(field: ModelField) -> bool: - return is_bytes_sequence_annotation(field.type_) - - def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - cls = type(field_info) - merged_field_info = cls.from_annotation(annotation) - new_field_info = copy(field_info) - new_field_info.metadata = merged_field_info.metadata - new_field_info.annotation = merged_field_info.annotation - return new_field_info - - def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - origin_type = ( - get_origin(field.field_info.annotation) or field.field_info.annotation - ) - assert issubclass(origin_type, sequence_types) # type: ignore[arg-type] - return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return] - - def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: - error = ValidationError.from_exception_data( - "Field required", [{"type": "missing", "loc": loc, "input": {}}] - ).errors(include_url=False)[0] - error["input"] = None - return error # type: ignore[return-value] - - def create_body_model( - *, fields: Sequence[ModelField], model_name: str - ) -> Type[BaseModel]: - field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields} - BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] - return BodyModel - - def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: - return [ - ModelField(field_info=field_info, name=name) - for name, field_info in model.model_fields.items() - ] - -else: - from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX - from pydantic import AnyUrl as Url # noqa: F401 - from pydantic import ( # type: ignore[assignment] - BaseConfig as BaseConfig, # noqa: F401 - ) - from pydantic import ValidationError as ValidationError # noqa: F401 - from pydantic.class_validators import ( # type: ignore[no-redef] - Validator as Validator, # noqa: F401 - ) - from pydantic.error_wrappers import ( # type: ignore[no-redef] - ErrorWrapper as ErrorWrapper, # noqa: F401 - ) - from pydantic.errors import MissingError - from pydantic.fields import ( # type: ignore[attr-defined] - SHAPE_FROZENSET, - SHAPE_LIST, - SHAPE_SEQUENCE, - SHAPE_SET, - SHAPE_SINGLETON, - SHAPE_TUPLE, - SHAPE_TUPLE_ELLIPSIS, - ) - from pydantic.fields import FieldInfo as FieldInfo - from pydantic.fields import ( # type: ignore[no-redef,attr-defined] - ModelField as ModelField, # noqa: F401 - ) - - # Keeping old "Required" functionality from Pydantic V1, without - # shadowing typing.Required. - RequiredParam: Any = Ellipsis # type: ignore[no-redef] - from pydantic.fields import ( # type: ignore[no-redef,attr-defined] - Undefined as Undefined, - ) - from pydantic.fields import ( # type: ignore[no-redef, attr-defined] - UndefinedType as UndefinedType, # noqa: F401 - ) - from pydantic.schema import ( - field_schema, - get_flat_models_from_fields, - get_model_name_map, - model_process_schema, - ) - from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401 - get_annotation_from_field_info as get_annotation_from_field_info, - ) - from pydantic.typing import ( # type: ignore[no-redef] - evaluate_forwardref as evaluate_forwardref, # noqa: F401 - ) - from pydantic.utils import ( # type: ignore[no-redef] - lenient_issubclass as lenient_issubclass, # noqa: F401 - ) - - GetJsonSchemaHandler = Any # type: ignore[assignment,misc] - JsonSchemaValue = Dict[str, Any] # type: ignore[misc] - CoreSchema = Any # type: ignore[assignment,misc] - - sequence_shapes = { - SHAPE_LIST, - SHAPE_SET, - SHAPE_FROZENSET, - SHAPE_TUPLE, - SHAPE_SEQUENCE, - SHAPE_TUPLE_ELLIPSIS, - } - sequence_shape_to_type = { - SHAPE_LIST: list, - SHAPE_SET: set, - SHAPE_TUPLE: tuple, - SHAPE_SEQUENCE: list, - SHAPE_TUPLE_ELLIPSIS: list, - } - - @dataclass - class GenerateJsonSchema: # type: ignore[no-redef] - ref_template: str - - class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef] - pass - - def with_info_plain_validator_function( # type: ignore[misc] - function: Callable[..., Any], - *, - ref: Union[str, None] = None, - metadata: Any = None, - serialization: Any = None, - ) -> Any: - return {} - - def get_model_definitions( - *, - flat_models: Set[Union[Type[BaseModel], Type[Enum]]], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], - ) -> Dict[str, Any]: - definitions: Dict[str, Dict[str, Any]] = {} - for model in flat_models: - m_schema, m_definitions, m_nested_models = model_process_schema( - model, model_name_map=model_name_map, ref_prefix=REF_PREFIX - ) - definitions.update(m_definitions) - model_name = model_name_map[model] - definitions[model_name] = m_schema - for m_schema in definitions.values(): - if "description" in m_schema: - m_schema["description"] = m_schema["description"].split("\f")[0] - return definitions - - def is_pv1_scalar_field(field: ModelField) -> bool: - from fastapi import params - - field_info = field.field_info - if not ( - field.shape == SHAPE_SINGLETON # type: ignore[attr-defined] - and not lenient_issubclass(field.type_, BaseModel) - and not lenient_issubclass(field.type_, dict) - and not field_annotation_is_sequence(field.type_) - and not is_dataclass(field.type_) - and not isinstance(field_info, params.Body) - ): - return False - if field.sub_fields: # type: ignore[attr-defined] - if not all( - is_pv1_scalar_field(f) - for f in field.sub_fields # type: ignore[attr-defined] - ): - return False - return True - - def is_pv1_scalar_sequence_field(field: ModelField) -> bool: - if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined] - field.type_, BaseModel - ): - if field.sub_fields is not None: # type: ignore[attr-defined] - for sub_field in field.sub_fields: # type: ignore[attr-defined] - if not is_pv1_scalar_field(sub_field): - return False - return True - if _annotation_is_sequence(field.type_): - return True - return False - - def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: - use_errors: List[Any] = [] - for error in errors: - if isinstance(error, ErrorWrapper): - new_errors = ValidationError( # type: ignore[call-arg] - errors=[error], model=RequestErrorModel - ).errors() - use_errors.extend(new_errors) - elif isinstance(error, list): - use_errors.extend(_normalize_errors(error)) - else: - use_errors.append(error) - return use_errors - - def _model_rebuild(model: Type[BaseModel]) -> None: - model.update_forward_refs() - - def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any - ) -> Any: - return model.dict(**kwargs) - - def _get_model_config(model: BaseModel) -> Any: - return model.__config__ # type: ignore[attr-defined] - - def get_schema_from_model_field( - *, - field: ModelField, - schema_generator: GenerateJsonSchema, - model_name_map: ModelNameMap, - field_mapping: Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - separate_input_output_schemas: bool = True, - ) -> Dict[str, Any]: - # This expects that GenerateJsonSchema was already used to generate the definitions - return field_schema( # type: ignore[no-any-return] - field, model_name_map=model_name_map, ref_prefix=REF_PREFIX - )[0] - - def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: - models = get_flat_models_from_fields(fields, known_models=set()) - return get_model_name_map(models) # type: ignore[no-any-return] - - def get_definitions( - *, - fields: List[ModelField], - schema_generator: GenerateJsonSchema, - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, - ) -> Tuple[ - Dict[ - Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - Dict[str, Dict[str, Any]], - ]: - models = get_flat_models_from_fields(fields, known_models=set()) - return {}, get_model_definitions( - flat_models=models, model_name_map=model_name_map - ) - - def is_scalar_field(field: ModelField) -> bool: - return is_pv1_scalar_field(field) - - def is_sequence_field(field: ModelField) -> bool: - return field.shape in sequence_shapes or _annotation_is_sequence(field.type_) # type: ignore[attr-defined] - - def is_scalar_sequence_field(field: ModelField) -> bool: - return is_pv1_scalar_sequence_field(field) - - def is_bytes_field(field: ModelField) -> bool: - return lenient_issubclass(field.type_, bytes) - - def is_bytes_sequence_field(field: ModelField) -> bool: - return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined] - - def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - return copy(field_info) - - def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined] - - def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: - missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg] - new_error = ValidationError([missing_field_error], RequestErrorModel) - return new_error.errors()[0] # type: ignore[return-value] - - def create_body_model( - *, fields: Sequence[ModelField], model_name: str - ) -> Type[BaseModel]: - BodyModel = create_model(model_name) - for f in fields: - BodyModel.__fields__[f.name] = f # type: ignore[index] - return BodyModel - - def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: - return list(model.__fields__.values()) # type: ignore[attr-defined] - - -def _regenerate_error_with_loc( - *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...] -) -> List[Dict[str, Any]]: - updated_loc_errors: List[Any] = [ - {**err, "loc": loc_prefix + err.get("loc", ())} - for err in _normalize_errors(errors) - ] - - return updated_loc_errors - - -def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: - if lenient_issubclass(annotation, (str, bytes)): - return False - return lenient_issubclass(annotation, sequence_types) - - -def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - for arg in get_args(annotation): - if field_annotation_is_sequence(arg): - return True - return False - return _annotation_is_sequence(annotation) or _annotation_is_sequence( - get_origin(annotation) - ) - - -def value_is_sequence(value: Any) -> bool: - return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type] - - -def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: - return ( - lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile)) - or _annotation_is_sequence(annotation) - or is_dataclass(annotation) - ) - - -def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - return any(field_annotation_is_complex(arg) for arg in get_args(annotation)) - - return ( - _annotation_is_complex(annotation) - or _annotation_is_complex(origin) - or hasattr(origin, "__pydantic_core_schema__") - or hasattr(origin, "__get_pydantic_core_schema__") - ) - - -def field_annotation_is_scalar(annotation: Any) -> bool: - # handle Ellipsis here to make tuple[int, ...] work nicely - return annotation is Ellipsis or not field_annotation_is_complex(annotation) - - -def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool: - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - at_least_one_scalar_sequence = False - for arg in get_args(annotation): - if field_annotation_is_scalar_sequence(arg): - at_least_one_scalar_sequence = True - continue - elif not field_annotation_is_scalar(arg): - return False - return at_least_one_scalar_sequence - return field_annotation_is_sequence(annotation) and all( - field_annotation_is_scalar(sub_annotation) - for sub_annotation in get_args(annotation) - ) - - -def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool: - if lenient_issubclass(annotation, bytes): - return True - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - for arg in get_args(annotation): - if lenient_issubclass(arg, bytes): - return True - return False - - -def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool: - if lenient_issubclass(annotation, UploadFile): - return True - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - for arg in get_args(annotation): - if lenient_issubclass(arg, UploadFile): - return True - return False - - -def is_bytes_sequence_annotation(annotation: Any) -> bool: - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - at_least_one = False - for arg in get_args(annotation): - if is_bytes_sequence_annotation(arg): - at_least_one = True - continue - return at_least_one - return field_annotation_is_sequence(annotation) and all( - is_bytes_or_nonable_bytes_annotation(sub_annotation) - for sub_annotation in get_args(annotation) - ) - - -def is_uploadfile_sequence_annotation(annotation: Any) -> bool: - origin = get_origin(annotation) - if origin is Union or origin is UnionType: - at_least_one = False - for arg in get_args(annotation): - if is_uploadfile_sequence_annotation(arg): - at_least_one = True - continue - return at_least_one - return field_annotation_is_sequence(annotation) and all( - is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation) - for sub_annotation in get_args(annotation) - ) - - -@lru_cache -def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]: - return get_model_fields(model) diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py new file mode 100644 index 000000000..0aadd68de --- /dev/null +++ b/fastapi/_compat/__init__.py @@ -0,0 +1,50 @@ +from .main import BaseConfig as BaseConfig +from .main import PydanticSchemaGenerationError as PydanticSchemaGenerationError +from .main import RequiredParam as RequiredParam +from .main import Undefined as Undefined +from .main import UndefinedType as UndefinedType +from .main import Url as Url +from .main import Validator as Validator +from .main import _get_model_config as _get_model_config +from .main import _is_error_wrapper as _is_error_wrapper +from .main import _is_model_class as _is_model_class +from .main import _is_model_field as _is_model_field +from .main import _is_undefined as _is_undefined +from .main import _model_dump as _model_dump +from .main import _model_rebuild as _model_rebuild +from .main import copy_field_info as copy_field_info +from .main import create_body_model as create_body_model +from .main import evaluate_forwardref as evaluate_forwardref +from .main import get_annotation_from_field_info as get_annotation_from_field_info +from .main import get_cached_model_fields as get_cached_model_fields +from .main import get_compat_model_name_map as get_compat_model_name_map +from .main import get_definitions as get_definitions +from .main import get_missing_field_error as get_missing_field_error +from .main import get_schema_from_model_field as get_schema_from_model_field +from .main import is_bytes_field as is_bytes_field +from .main import is_bytes_sequence_field as is_bytes_sequence_field +from .main import is_scalar_field as is_scalar_field +from .main import is_scalar_sequence_field as is_scalar_sequence_field +from .main import is_sequence_field as is_sequence_field +from .main import serialize_sequence_value as serialize_sequence_value +from .main import ( + with_info_plain_validator_function as with_info_plain_validator_function, +) +from .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_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation, +) +from .shared import ( + is_uploadfile_sequence_annotation as is_uploadfile_sequence_annotation, +) +from .shared import lenient_issubclass as lenient_issubclass +from .shared import sequence_types as sequence_types +from .shared import value_is_sequence as value_is_sequence diff --git a/fastapi/_compat/main.py b/fastapi/_compat/main.py new file mode 100644 index 000000000..e5275950e --- /dev/null +++ b/fastapi/_compat/main.py @@ -0,0 +1,362 @@ +import sys +from functools import lru_cache +from typing import ( + Any, + Dict, + List, + Sequence, + Tuple, + Type, +) + +from fastapi._compat import may_v1 +from fastapi._compat.shared import PYDANTIC_V2, lenient_issubclass +from fastapi.types import ModelNameMap +from pydantic import BaseModel +from typing_extensions import Literal + +from .model_field import ModelField + +if PYDANTIC_V2: + from .v2 import BaseConfig as BaseConfig + from .v2 import FieldInfo as FieldInfo + from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError + from .v2 import RequiredParam as RequiredParam + from .v2 import Undefined as Undefined + from .v2 import UndefinedType as UndefinedType + from .v2 import Url as Url + from .v2 import Validator as Validator + from .v2 import evaluate_forwardref as evaluate_forwardref + from .v2 import get_missing_field_error as get_missing_field_error + from .v2 import ( + with_info_plain_validator_function as with_info_plain_validator_function, + ) +else: + from .v1 import BaseConfig as BaseConfig # type: ignore[assignment] + from .v1 import FieldInfo as FieldInfo + from .v1 import ( # type: ignore[assignment] + PydanticSchemaGenerationError as PydanticSchemaGenerationError, + ) + from .v1 import RequiredParam as RequiredParam + from .v1 import Undefined as Undefined + from .v1 import UndefinedType as UndefinedType + from .v1 import Url as Url # type: ignore[assignment] + from .v1 import Validator as Validator + from .v1 import evaluate_forwardref as evaluate_forwardref + from .v1 import get_missing_field_error as get_missing_field_error + from .v1 import ( # type: ignore[assignment] + with_info_plain_validator_function as with_info_plain_validator_function, + ) + + +@lru_cache +def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]: + if lenient_issubclass(model, may_v1.BaseModel): + from fastapi._compat import v1 + + return v1.get_model_fields(model) + else: + from . import v2 + + return v2.get_model_fields(model) # type: ignore[return-value] + + +def _is_undefined(value: object) -> bool: + if isinstance(value, may_v1.UndefinedType): + return True + elif PYDANTIC_V2: + from . import v2 + + return isinstance(value, v2.UndefinedType) + return False + + +def _get_model_config(model: BaseModel) -> Any: + if isinstance(model, may_v1.BaseModel): + from fastapi._compat import v1 + + return v1._get_model_config(model) + elif PYDANTIC_V2: + from . import v2 + + return v2._get_model_config(model) + + +def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any +) -> Any: + if isinstance(model, may_v1.BaseModel): + from fastapi._compat import v1 + + return v1._model_dump(model, mode=mode, **kwargs) + elif PYDANTIC_V2: + from . import v2 + + return v2._model_dump(model, mode=mode, **kwargs) + + +def _is_error_wrapper(exc: Exception) -> bool: + if isinstance(exc, may_v1.ErrorWrapper): + return True + elif PYDANTIC_V2: + from . import v2 + + return isinstance(exc, v2.ErrorWrapper) + return False + + +def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + if isinstance(field_info, may_v1.FieldInfo): + from fastapi._compat import v1 + + return v1.copy_field_info(field_info=field_info, annotation=annotation) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.copy_field_info(field_info=field_info, annotation=annotation) + + +def create_body_model( + *, fields: Sequence[ModelField], model_name: str +) -> Type[BaseModel]: + if fields and isinstance(fields[0], may_v1.ModelField): + from fastapi._compat import v1 + + return v1.create_body_model(fields=fields, model_name=model_name) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type] + + +def get_annotation_from_field_info( + annotation: Any, field_info: FieldInfo, field_name: str +) -> Any: + if isinstance(field_info, 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 + ) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.get_annotation_from_field_info( + annotation=annotation, field_info=field_info, field_name=field_name + ) + + +def is_bytes_field(field: ModelField) -> bool: + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + + return v1.is_bytes_field(field) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.is_bytes_field(field) # type: ignore[arg-type] + + +def is_bytes_sequence_field(field: ModelField) -> bool: + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + + return v1.is_bytes_sequence_field(field) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.is_bytes_sequence_field(field) # type: ignore[arg-type] + + +def is_scalar_field(field: ModelField) -> bool: + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + + return v1.is_scalar_field(field) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.is_scalar_field(field) # type: ignore[arg-type] + + +def is_scalar_sequence_field(field: ModelField) -> bool: + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + + return v1.is_scalar_sequence_field(field) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.is_scalar_sequence_field(field) # type: ignore[arg-type] + + +def is_sequence_field(field: ModelField) -> bool: + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + + return v1.is_sequence_field(field) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.is_sequence_field(field) # type: ignore[arg-type] + + +def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + if isinstance(field, may_v1.ModelField): + from fastapi._compat import v1 + + return v1.serialize_sequence_value(field=field, value=value) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type] + + +def _model_rebuild(model: Type[BaseModel]) -> None: + if lenient_issubclass(model, may_v1.BaseModel): + from fastapi._compat import v1 + + v1._model_rebuild(model) + elif PYDANTIC_V2: + from . import v2 + + v2._model_rebuild(model) + + +def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: + v1_model_fields = [ + field for field in fields if isinstance(field, may_v1.ModelField) + ] + if v1_model_fields: + from fastapi._compat import v1 + + v1_flat_models = v1.get_flat_models_from_fields( + v1_model_fields, known_models=set() + ) + all_flat_models = v1_flat_models + else: + all_flat_models = set() + if PYDANTIC_V2: + from . import v2 + + v2_model_fields = [ + field for field in fields if isinstance(field, v2.ModelField) + ] + v2_flat_models = v2.get_flat_models_from_fields( + v2_model_fields, known_models=set() + ) + all_flat_models = all_flat_models.union(v2_flat_models) + + model_name_map = v2.get_model_name_map(all_flat_models) + return model_name_map + from fastapi._compat import v1 + + model_name_map = v1.get_model_name_map(all_flat_models) + return model_name_map + + +def get_definitions( + *, + fields: List[ModelField], + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, +) -> Tuple[ + Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], + 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, + model_name_map=model_name_map, + separate_input_output_schemas=separate_input_output_schemas, + ) + if not PYDANTIC_V2: + return v1_field_maps, v1_definitions + else: + from . import v2 + + v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] + v2_field_maps, v2_definitions = v2.get_definitions( + fields=v2_fields, + model_name_map=model_name_map, + separate_input_output_schemas=separate_input_output_schemas, + ) + all_definitions = {**v1_definitions, **v2_definitions} + all_field_maps = {**v1_field_maps, **v2_field_maps} + return all_field_maps, all_definitions + + # Pydantic v1 is not supported since Python 3.14 + else: + from . import v2 + + 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, + ) + else: + assert PYDANTIC_V2 + from . import v2 + + return v2.get_schema_from_model_field( + field=field, # type: ignore[arg-type] + model_name_map=model_name_map, + field_mapping=field_mapping, # type: ignore[arg-type] + separate_input_output_schemas=separate_input_output_schemas, + ) + + +def _is_model_field(value: Any) -> bool: + if isinstance(value, may_v1.ModelField): + return True + elif PYDANTIC_V2: + from . import v2 + + return isinstance(value, v2.ModelField) + return False + + +def _is_model_class(value: Any) -> bool: + if lenient_issubclass(value, may_v1.BaseModel): + return True + elif PYDANTIC_V2: + from . import v2 + + return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined] + return False diff --git a/fastapi/_compat/may_v1.py b/fastapi/_compat/may_v1.py new file mode 100644 index 000000000..beea4d167 --- /dev/null +++ b/fastapi/_compat/may_v1.py @@ -0,0 +1,123 @@ +import sys +from typing import Any, Dict, List, Literal, Sequence, Tuple, Type, Union + +from fastapi.types import ModelNameMap + +if sys.version_info >= (3, 14): + + class AnyUrl: + pass + + class BaseConfig: + pass + + class BaseModel: + pass + + class Color: + pass + + class CoreSchema: + pass + + class ErrorWrapper: + pass + + class FieldInfo: + pass + + class GetJsonSchemaHandler: + pass + + class JsonSchemaValue: + pass + + class ModelField: + pass + + class NameEmail: + pass + + class RequiredParam: + pass + + class SecretBytes: + pass + + class SecretStr: + pass + + class Undefined: + pass + + class UndefinedType: + pass + + class Url: + pass + + from .v2 import ValidationError, create_model + + def get_definitions( + *, + fields: List[ModelField], + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, + ) -> Tuple[ + Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + Dict[str, Dict[str, Any]], + ]: + return {}, {} # pragma: no cover + + +else: + from .v1 import AnyUrl as AnyUrl + from .v1 import BaseConfig as BaseConfig + from .v1 import BaseModel as BaseModel + from .v1 import Color as Color + from .v1 import CoreSchema as CoreSchema + from .v1 import ErrorWrapper as ErrorWrapper + from .v1 import FieldInfo as FieldInfo + from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler + from .v1 import JsonSchemaValue as JsonSchemaValue + from .v1 import ModelField as ModelField + from .v1 import NameEmail as NameEmail + from .v1 import RequiredParam as RequiredParam + from .v1 import SecretBytes as SecretBytes + from .v1 import SecretStr as SecretStr + from .v1 import Undefined as Undefined + from .v1 import UndefinedType as UndefinedType + from .v1 import Url as Url + from .v1 import ValidationError, create_model + from .v1 import get_definitions as get_definitions + + +RequestErrorModel: Type[BaseModel] = create_model("Request") + + +def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: + use_errors: List[Any] = [] + for error in errors: + if isinstance(error, ErrorWrapper): + new_errors = ValidationError( # type: ignore[call-arg] + errors=[error], model=RequestErrorModel + ).errors() + use_errors.extend(new_errors) + elif isinstance(error, list): + use_errors.extend(_normalize_errors(error)) + else: + use_errors.append(error) + return use_errors + + +def _regenerate_error_with_loc( + *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...] +) -> List[Dict[str, Any]]: + updated_loc_errors: List[Any] = [ + {**err, "loc": loc_prefix + err.get("loc", ())} + for err in _normalize_errors(errors) + ] + + return updated_loc_errors diff --git a/fastapi/_compat/model_field.py b/fastapi/_compat/model_field.py new file mode 100644 index 000000000..fa2008c5e --- /dev/null +++ b/fastapi/_compat/model_field.py @@ -0,0 +1,53 @@ +from typing import ( + Any, + Dict, + List, + Tuple, + Union, +) + +from fastapi.types import IncEx +from pydantic.fields import FieldInfo +from typing_extensions import Literal, Protocol + + +class ModelField(Protocol): + field_info: "FieldInfo" + name: str + mode: Literal["validation", "serialization"] = "validation" + _version: Literal["v1", "v2"] = "v1" + + @property + def alias(self) -> str: ... + + @property + def required(self) -> bool: ... + + @property + def default(self) -> Any: ... + + @property + def type_(self) -> Any: ... + + def get_default(self) -> Any: ... + + def validate( + self, + value: Any, + values: Dict[str, Any] = {}, # noqa: B006 + *, + loc: Tuple[Union[int, str], ...] = (), + ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: ... + + def serialize( + self, + value: Any, + *, + mode: Literal["json", "python"] = "json", + include: Union[IncEx, None] = None, + exclude: Union[IncEx, None] = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> Any: ... diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py new file mode 100644 index 000000000..cabf48228 --- /dev/null +++ b/fastapi/_compat/shared.py @@ -0,0 +1,211 @@ +import sys +import types +import typing +from collections import deque +from dataclasses import is_dataclass +from typing import ( + Any, + Deque, + FrozenSet, + List, + Mapping, + Sequence, + Set, + Tuple, + Type, + Union, +) + +from fastapi._compat import may_v1 +from fastapi.types import UnionType +from pydantic import BaseModel +from pydantic.version import VERSION as PYDANTIC_VERSION +from starlette.datastructures import UploadFile +from typing_extensions import Annotated, get_args, get_origin + +# Copy from Pydantic v2, compatible with v1 +if sys.version_info < (3, 9): + # Pydantic no longer supports Python 3.8, this might be incorrect, but the code + # this is used for is also never reached in this codebase, as it's a copy of + # Pydantic's lenient_issubclass, just for compatibility with v1 + # TODO: remove when dropping support for Python 3.8 + WithArgsTypes: Tuple[Any, ...] = () +elif sys.version_info < (3, 10): + WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias) # type: ignore[attr-defined] +else: + WithArgsTypes: tuple[Any, ...] = ( + typing._GenericAlias, # type: ignore[attr-defined] + types.GenericAlias, + types.UnionType, + ) # pyright: ignore[reportAttributeAccessIssue] + +PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) +PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 + + +sequence_annotation_to_type = { + Sequence: list, + List: list, + list: list, + Tuple: tuple, + tuple: tuple, + Set: set, + set: set, + FrozenSet: frozenset, + frozenset: frozenset, + Deque: deque, + deque: deque, +} + +sequence_types = tuple(sequence_annotation_to_type.keys()) + +Url: Type[Any] + + +# Copy of Pydantic v2, compatible with v1 +def lenient_issubclass( + cls: Any, class_or_tuple: Union[Type[Any], Tuple[Type[Any], ...], None] +) -> bool: + try: + return isinstance(cls, type) and issubclass(cls, class_or_tuple) # type: ignore[arg-type] + except TypeError: # pragma: no cover + if isinstance(cls, WithArgsTypes): + return False + raise # pragma: no cover + + +def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + if lenient_issubclass(annotation, (str, bytes)): + return False + return lenient_issubclass(annotation, sequence_types) # type: ignore[arg-type] + + +def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if field_annotation_is_sequence(arg): + return True + return False + return _annotation_is_sequence(annotation) or _annotation_is_sequence( + get_origin(annotation) + ) + + +def value_is_sequence(value: Any) -> bool: + return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type] + + +def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: + return ( + lenient_issubclass( + annotation, (BaseModel, may_v1.BaseModel, Mapping, UploadFile) + ) + or _annotation_is_sequence(annotation) + or is_dataclass(annotation) + ) + + +def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + return any(field_annotation_is_complex(arg) for arg in get_args(annotation)) + + if origin is Annotated: + return field_annotation_is_complex(get_args(annotation)[0]) + + return ( + _annotation_is_complex(annotation) + or _annotation_is_complex(origin) + or hasattr(origin, "__pydantic_core_schema__") + or hasattr(origin, "__get_pydantic_core_schema__") + ) + + +def field_annotation_is_scalar(annotation: Any) -> bool: + # handle Ellipsis here to make tuple[int, ...] work nicely + return annotation is Ellipsis or not field_annotation_is_complex(annotation) + + +def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one_scalar_sequence = False + for arg in get_args(annotation): + if field_annotation_is_scalar_sequence(arg): + at_least_one_scalar_sequence = True + continue + elif not field_annotation_is_scalar(arg): + return False + return at_least_one_scalar_sequence + return field_annotation_is_sequence(annotation) and all( + field_annotation_is_scalar(sub_annotation) + for sub_annotation in get_args(annotation) + ) + + +def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool: + if lenient_issubclass(annotation, bytes): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, bytes): + return True + return False + + +def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool: + if lenient_issubclass(annotation, UploadFile): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, UploadFile): + return True + return False + + +def is_bytes_sequence_annotation(annotation: Any) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one = False + for arg in get_args(annotation): + if is_bytes_sequence_annotation(arg): + at_least_one = True + continue + return at_least_one + return field_annotation_is_sequence(annotation) and all( + is_bytes_or_nonable_bytes_annotation(sub_annotation) + for sub_annotation in get_args(annotation) + ) + + +def is_uploadfile_sequence_annotation(annotation: Any) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one = False + for arg in get_args(annotation): + if is_uploadfile_sequence_annotation(arg): + at_least_one = True + continue + return at_least_one + return field_annotation_is_sequence(annotation) and all( + is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation) + for sub_annotation in get_args(annotation) + ) + + +def annotation_is_pydantic_v1(annotation: Any) -> bool: + if lenient_issubclass(annotation, may_v1.BaseModel): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, may_v1.BaseModel): + return True + if field_annotation_is_sequence(annotation): + for sub_annotation in get_args(annotation): + if annotation_is_pydantic_v1(sub_annotation): + return True + return False diff --git a/fastapi/_compat/v1.py b/fastapi/_compat/v1.py new file mode 100644 index 000000000..e17ce8bea --- /dev/null +++ b/fastapi/_compat/v1.py @@ -0,0 +1,312 @@ +from copy import copy +from dataclasses import dataclass, is_dataclass +from enum import Enum +from typing import ( + Any, + Callable, + Dict, + List, + Sequence, + Set, + Tuple, + Type, + Union, +) + +from fastapi._compat import shared +from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX +from fastapi.types import ModelNameMap +from pydantic.version import VERSION as PYDANTIC_VERSION +from typing_extensions import Literal + +PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) +PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 +# Keeping old "Required" functionality from Pydantic V1, without +# shadowing typing.Required. +RequiredParam: Any = Ellipsis + +if not PYDANTIC_V2: + from pydantic import BaseConfig as BaseConfig + from pydantic import BaseModel as BaseModel + from pydantic import ValidationError as ValidationError + from pydantic import create_model as create_model + from pydantic.class_validators import Validator as Validator + from pydantic.color import Color as Color + from pydantic.error_wrappers import ErrorWrapper as ErrorWrapper + from pydantic.errors import MissingError + from pydantic.fields import ( # type: ignore[attr-defined] + SHAPE_FROZENSET, + SHAPE_LIST, + SHAPE_SEQUENCE, + SHAPE_SET, + SHAPE_SINGLETON, + SHAPE_TUPLE, + SHAPE_TUPLE_ELLIPSIS, + ) + from pydantic.fields import FieldInfo as FieldInfo + from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined] + from pydantic.fields import Undefined as Undefined # type: ignore[attr-defined] + from pydantic.fields import ( # type: ignore[attr-defined] + UndefinedType as UndefinedType, + ) + from pydantic.networks import AnyUrl as AnyUrl + from pydantic.networks import NameEmail as NameEmail + from pydantic.schema import TypeModelSet as TypeModelSet + from pydantic.schema import ( + field_schema, + model_process_schema, + ) + from pydantic.schema import ( + get_annotation_from_field_info as get_annotation_from_field_info, + ) + from pydantic.schema import get_flat_models_from_field as get_flat_models_from_field + from pydantic.schema import ( + get_flat_models_from_fields as get_flat_models_from_fields, + ) + from pydantic.schema import get_model_name_map as get_model_name_map + from pydantic.types import SecretBytes as SecretBytes + from pydantic.types import SecretStr as SecretStr + from pydantic.typing import evaluate_forwardref as evaluate_forwardref + from pydantic.utils import lenient_issubclass as lenient_issubclass + + +else: + from pydantic.v1 import BaseConfig as BaseConfig # type: ignore[assignment] + from pydantic.v1 import BaseModel as BaseModel # type: ignore[assignment] + from pydantic.v1 import ( # type: ignore[assignment] + ValidationError as ValidationError, + ) + from pydantic.v1 import create_model as create_model # type: ignore[no-redef] + from pydantic.v1.class_validators import Validator as Validator + from pydantic.v1.color import Color as Color # type: ignore[assignment] + from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper + from pydantic.v1.errors import MissingError + from pydantic.v1.fields import ( + SHAPE_FROZENSET, + SHAPE_LIST, + SHAPE_SEQUENCE, + SHAPE_SET, + SHAPE_SINGLETON, + SHAPE_TUPLE, + SHAPE_TUPLE_ELLIPSIS, + ) + from pydantic.v1.fields import FieldInfo as FieldInfo # type: ignore[assignment] + from pydantic.v1.fields import ModelField as ModelField + from pydantic.v1.fields import Undefined as Undefined + from pydantic.v1.fields import UndefinedType as UndefinedType + from pydantic.v1.networks import AnyUrl as AnyUrl + from pydantic.v1.networks import ( # type: ignore[assignment] + NameEmail as NameEmail, + ) + from pydantic.v1.schema import TypeModelSet as TypeModelSet + from pydantic.v1.schema import ( + field_schema, + 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 ( # type: ignore[assignment] + SecretBytes as SecretBytes, + ) + from pydantic.v1.types import ( # type: ignore[assignment] + SecretStr as SecretStr, + ) + from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref + from pydantic.v1.utils import lenient_issubclass as lenient_issubclass + + +GetJsonSchemaHandler = Any +JsonSchemaValue = Dict[str, Any] +CoreSchema = Any +Url = AnyUrl + +sequence_shapes = { + SHAPE_LIST, + SHAPE_SET, + SHAPE_FROZENSET, + SHAPE_TUPLE, + SHAPE_SEQUENCE, + SHAPE_TUPLE_ELLIPSIS, +} +sequence_shape_to_type = { + SHAPE_LIST: list, + SHAPE_SET: set, + SHAPE_TUPLE: tuple, + SHAPE_SEQUENCE: list, + SHAPE_TUPLE_ELLIPSIS: list, +} + + +@dataclass +class GenerateJsonSchema: + ref_template: str + + +class PydanticSchemaGenerationError(Exception): + pass + + +RequestErrorModel: Type[BaseModel] = create_model("Request") + + +def with_info_plain_validator_function( + function: Callable[..., Any], + *, + ref: Union[str, None] = None, + metadata: Any = None, + serialization: Any = None, +) -> Any: + return {} + + +def get_model_definitions( + *, + flat_models: Set[Union[Type[BaseModel], Type[Enum]]], + model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], +) -> Dict[str, Any]: + definitions: Dict[str, Dict[str, Any]] = {} + for model in flat_models: + m_schema, m_definitions, m_nested_models = model_process_schema( + model, model_name_map=model_name_map, ref_prefix=REF_PREFIX + ) + definitions.update(m_definitions) + model_name = model_name_map[model] + definitions[model_name] = m_schema + for m_schema in definitions.values(): + if "description" in m_schema: + m_schema["description"] = m_schema["description"].split("\f")[0] + return definitions + + +def is_pv1_scalar_field(field: ModelField) -> bool: + from fastapi import params + + field_info = field.field_info + if not ( + field.shape == SHAPE_SINGLETON + and not lenient_issubclass(field.type_, BaseModel) + and not lenient_issubclass(field.type_, dict) + and not shared.field_annotation_is_sequence(field.type_) + and not is_dataclass(field.type_) + and not isinstance(field_info, params.Body) + ): + return False + if field.sub_fields: + if not all(is_pv1_scalar_field(f) for f in field.sub_fields): + return False + return True + + +def is_pv1_scalar_sequence_field(field: ModelField) -> bool: + if (field.shape in sequence_shapes) and not lenient_issubclass( + field.type_, BaseModel + ): + if field.sub_fields is not None: + for sub_field in field.sub_fields: + if not is_pv1_scalar_field(sub_field): + return False + return True + if shared._annotation_is_sequence(field.type_): + return True + return False + + +def _model_rebuild(model: Type[BaseModel]) -> None: + model.update_forward_refs() + + +def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any +) -> Any: + return model.dict(**kwargs) + + +def _get_model_config(model: BaseModel) -> Any: + return model.__config__ # type: ignore[attr-defined] + + +def get_schema_from_model_field( + *, + field: ModelField, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + separate_input_output_schemas: bool = True, +) -> Dict[str, Any]: + return field_schema( # type: ignore[no-any-return] + field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + )[0] + + +# def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: +# models = get_flat_models_from_fields(fields, known_models=set()) +# return get_model_name_map(models) # type: ignore[no-any-return] + + +def get_definitions( + *, + fields: List[ModelField], + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, +) -> Tuple[ + Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], + Dict[str, Dict[str, Any]], +]: + models = get_flat_models_from_fields(fields, known_models=set()) + return {}, get_model_definitions(flat_models=models, model_name_map=model_name_map) + + +def is_scalar_field(field: ModelField) -> bool: + return is_pv1_scalar_field(field) + + +def is_sequence_field(field: ModelField) -> bool: + return field.shape in sequence_shapes or shared._annotation_is_sequence(field.type_) + + +def is_scalar_sequence_field(field: ModelField) -> bool: + return is_pv1_scalar_sequence_field(field) + + +def is_bytes_field(field: ModelField) -> bool: + return lenient_issubclass(field.type_, bytes) # type: ignore[no-any-return] + + +def is_bytes_sequence_field(field: ModelField) -> bool: + return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) + + +def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + return copy(field_info) + + +def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return] + + +def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: + missing_field_error = ErrorWrapper(MissingError(), loc=loc) + new_error = ValidationError([missing_field_error], RequestErrorModel) + return new_error.errors()[0] # type: ignore[return-value] + + +def create_body_model( + *, fields: Sequence[ModelField], model_name: str +) -> Type[BaseModel]: + BodyModel = create_model(model_name) + for f in fields: + BodyModel.__fields__[f.name] = f # type: ignore[index] + return BodyModel + + +def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return list(model.__fields__.values()) # type: ignore[attr-defined] diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py new file mode 100644 index 000000000..6a87b9ae9 --- /dev/null +++ b/fastapi/_compat/v2.py @@ -0,0 +1,479 @@ +import re +import warnings +from copy import copy, deepcopy +from dataclasses import dataclass +from enum import Enum +from typing import ( + Any, + Dict, + List, + Sequence, + Set, + Tuple, + Type, + Union, + cast, +) + +from fastapi._compat import may_v1, shared +from fastapi.openapi.constants import REF_TEMPLATE +from fastapi.types import IncEx, ModelNameMap +from pydantic import BaseModel, TypeAdapter, create_model +from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError +from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation +from pydantic import ValidationError as ValidationError +from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined] + GetJsonSchemaHandler as GetJsonSchemaHandler, +) +from pydantic._internal._typing_extra import eval_type_lenient +from pydantic._internal._utils import lenient_issubclass as lenient_issubclass +from pydantic.fields import FieldInfo as FieldInfo +from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema +from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue +from pydantic_core import CoreSchema as CoreSchema +from pydantic_core import PydanticUndefined, PydanticUndefinedType +from pydantic_core import Url as Url +from typing_extensions import Annotated, Literal, get_args, get_origin + +try: + from pydantic_core.core_schema import ( + with_info_plain_validator_function as with_info_plain_validator_function, + ) +except ImportError: # pragma: no cover + from pydantic_core.core_schema import ( + general_plain_validator_function as with_info_plain_validator_function, # noqa: F401 + ) + +RequiredParam = PydanticUndefined +Undefined = PydanticUndefined +UndefinedType = PydanticUndefinedType +evaluate_forwardref = eval_type_lenient +Validator = Any + + +class BaseConfig: + pass + + +class ErrorWrapper(Exception): + pass + + +@dataclass +class ModelField: + field_info: FieldInfo + name: str + mode: Literal["validation", "serialization"] = "validation" + + @property + def alias(self) -> str: + a = self.field_info.alias + return a if a is not None else self.name + + @property + def required(self) -> bool: + return self.field_info.is_required() + + @property + def default(self) -> Any: + return self.get_default() + + @property + def type_(self) -> Any: + return self.field_info.annotation + + def __post_init__(self) -> None: + with warnings.catch_warnings(): + # Pydantic >= 2.12.0 warns about field specific metadata that is unused + # (e.g. `TypeAdapter(Annotated[int, Field(alias='b')])`). In some cases, we + # end up building the type adapter from a model field annotation so we + # need to ignore the warning: + if shared.PYDANTIC_VERSION_MINOR_TUPLE >= (2, 12): + from pydantic.warnings import UnsupportedFieldAttributeWarning + + warnings.simplefilter( + "ignore", category=UnsupportedFieldAttributeWarning + ) + self._type_adapter: TypeAdapter[Any] = TypeAdapter( + Annotated[self.field_info.annotation, self.field_info] + ) + + def get_default(self) -> Any: + if self.field_info.is_required(): + return Undefined + return self.field_info.get_default(call_default_factory=True) + + def validate( + self, + value: Any, + values: Dict[str, Any] = {}, # noqa: B006 + *, + loc: Tuple[Union[int, str], ...] = (), + ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: + try: + return ( + self._type_adapter.validate_python(value, from_attributes=True), + None, + ) + except ValidationError as exc: + return None, may_v1._regenerate_error_with_loc( + errors=exc.errors(include_url=False), loc_prefix=loc + ) + + def serialize( + self, + value: Any, + *, + mode: Literal["json", "python"] = "json", + include: Union[IncEx, None] = None, + exclude: Union[IncEx, None] = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> Any: + # What calls this code passes a value that already called + # self._type_adapter.validate_python(value) + return self._type_adapter.dump_python( + value, + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + def __hash__(self) -> int: + # Each ModelField is unique for our purposes, to allow making a dict from + # ModelField to its JSON Schema. + return id(self) + + +def get_annotation_from_field_info( + annotation: Any, field_info: FieldInfo, field_name: str +) -> Any: + return annotation + + +def _model_rebuild(model: Type[BaseModel]) -> None: + model.model_rebuild() + + +def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any +) -> Any: + return model.model_dump(mode=mode, **kwargs) + + +def _get_model_config(model: BaseModel) -> Any: + return model.model_config + + +def get_schema_from_model_field( + *, + field: ModelField, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + separate_input_output_schemas: bool = True, +) -> Dict[str, Any]: + override_mode: Union[Literal["validation"], None] = ( + None if separate_input_output_schemas else "validation" + ) + # This expects that GenerateJsonSchema was already used to generate the definitions + json_schema = field_mapping[(field, override_mode or field.mode)] + if "$ref" not in json_schema: + # TODO remove when deprecating Pydantic v1 + # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 + json_schema["title"] = field.field_info.title or field.alias.title().replace( + "_", " " + ) + return json_schema + + +def get_definitions( + *, + fields: Sequence[ModelField], + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, +) -> Tuple[ + Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], + Dict[str, Dict[str, Any]], +]: + schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) + override_mode: Union[Literal["validation"], None] = ( + None if separate_input_output_schemas else "validation" + ) + validation_fields = [field for field in fields if field.mode == "validation"] + serialization_fields = [field for field in fields if field.mode == "serialization"] + flat_validation_models = get_flat_models_from_fields( + validation_fields, known_models=set() + ) + flat_serialization_models = get_flat_models_from_fields( + serialization_fields, known_models=set() + ) + flat_validation_model_fields = [ + ModelField( + field_info=FieldInfo(annotation=model), + name=model.__name__, + mode="validation", + ) + for model in flat_validation_models + ] + flat_serialization_model_fields = [ + ModelField( + field_info=FieldInfo(annotation=model), + name=model.__name__, + mode="serialization", + ) + for model in flat_serialization_models + ] + flat_model_fields = flat_validation_model_fields + flat_serialization_model_fields + input_types = {f.type_ for f in fields} + unique_flat_model_fields = { + f for f in flat_model_fields if f.type_ not in input_types + } + + inputs = [ + (field, override_mode or field.mode, field._type_adapter.core_schema) + for field in list(fields) + list(unique_flat_model_fields) + ] + field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs) + for item_def in cast(Dict[str, Dict[str, Any]], definitions).values(): + if "description" in item_def: + item_description = cast(str, item_def["description"]).split("\f")[0] + item_def["description"] = item_description + new_mapping, new_definitions = _remap_definitions_and_field_mappings( + model_name_map=model_name_map, + definitions=definitions, # type: ignore[arg-type] + field_mapping=field_mapping, + ) + return new_mapping, new_definitions + + +def _replace_refs( + *, + schema: Dict[str, Any], + old_name_to_new_name_map: Dict[str, str], +) -> Dict[str, Any]: + new_schema = deepcopy(schema) + for key, value in new_schema.items(): + if key == "$ref": + ref_name = schema["$ref"].split("/")[-1] + if ref_name in old_name_to_new_name_map: + new_name = old_name_to_new_name_map[ref_name] + new_schema["$ref"] = REF_TEMPLATE.format(model=new_name) + else: + new_schema["$ref"] = schema["$ref"] + continue + if isinstance(value, dict): + new_schema[key] = _replace_refs( + schema=value, + old_name_to_new_name_map=old_name_to_new_name_map, + ) + elif isinstance(value, list): + new_value = [] + for item in value: + if isinstance(item, dict): + new_item = _replace_refs( + schema=item, + old_name_to_new_name_map=old_name_to_new_name_map, + ) + new_value.append(new_item) + + else: + new_value.append(item) + new_schema[key] = new_value + return new_schema + + +def _remap_definitions_and_field_mappings( + *, + model_name_map: ModelNameMap, + definitions: Dict[str, Any], + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], +) -> Tuple[ + Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], + Dict[str, Any], +]: + old_name_to_new_name_map = {} + for field_key, schema in field_mapping.items(): + model = field_key[0].type_ + if model not in model_name_map: + continue + new_name = model_name_map[model] + old_name = schema["$ref"].split("/")[-1] + if old_name in {f"{new_name}-Input", f"{new_name}-Output"}: + continue + old_name_to_new_name_map[old_name] = new_name + + new_field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ] = {} + for field_key, schema in field_mapping.items(): + new_schema = _replace_refs( + schema=schema, + old_name_to_new_name_map=old_name_to_new_name_map, + ) + new_field_mapping[field_key] = new_schema + + new_definitions = {} + for key, value in definitions.items(): + if key in old_name_to_new_name_map: + new_key = old_name_to_new_name_map[key] + else: + new_key = key + new_value = _replace_refs( + schema=value, + old_name_to_new_name_map=old_name_to_new_name_map, + ) + new_definitions[new_key] = new_value + return new_field_mapping, new_definitions + + +def is_scalar_field(field: ModelField) -> bool: + from fastapi import params + + return shared.field_annotation_is_scalar( + field.field_info.annotation + ) and not isinstance(field.field_info, params.Body) + + +def is_sequence_field(field: ModelField) -> bool: + return shared.field_annotation_is_sequence(field.field_info.annotation) + + +def is_scalar_sequence_field(field: ModelField) -> bool: + return shared.field_annotation_is_scalar_sequence(field.field_info.annotation) + + +def is_bytes_field(field: ModelField) -> bool: + return shared.is_bytes_or_nonable_bytes_annotation(field.type_) + + +def is_bytes_sequence_field(field: ModelField) -> bool: + return shared.is_bytes_sequence_annotation(field.type_) + + +def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + cls = type(field_info) + merged_field_info = cls.from_annotation(annotation) + new_field_info = copy(field_info) + new_field_info.metadata = merged_field_info.metadata + new_field_info.annotation = merged_field_info.annotation + return new_field_info + + +def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation + assert issubclass(origin_type, shared.sequence_types) # type: ignore[arg-type] + return shared.sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return] + + +def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: + error = ValidationError.from_exception_data( + "Field required", [{"type": "missing", "loc": loc, "input": {}}] + ).errors(include_url=False)[0] + error["input"] = None + return error # type: ignore[return-value] + + +def create_body_model( + *, fields: Sequence[ModelField], model_name: str +) -> Type[BaseModel]: + field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields} + BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] + return BodyModel + + +def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return [ + ModelField(field_info=field_info, name=name) + for name, field_info in model.model_fields.items() + ] + + +# Duplicate of several schema functions from Pydantic v1 to make them compatible with +# Pydantic v2 and allow mixing the models + +TypeModelOrEnum = Union[Type["BaseModel"], Type[Enum]] +TypeModelSet = Set[TypeModelOrEnum] + + +def normalize_name(name: str) -> str: + return re.sub(r"[^a-zA-Z0-9.\-_]", "_", name) + + +def get_model_name_map(unique_models: TypeModelSet) -> Dict[TypeModelOrEnum, str]: + name_model_map = {} + conflicting_names: Set[str] = set() + for model in unique_models: + model_name = normalize_name(model.__name__) + if model_name in conflicting_names: + model_name = get_long_model_name(model) + name_model_map[model_name] = model + elif model_name in name_model_map: + conflicting_names.add(model_name) + conflicting_model = name_model_map.pop(model_name) + name_model_map[get_long_model_name(conflicting_model)] = conflicting_model + name_model_map[get_long_model_name(model)] = model + else: + name_model_map[model_name] = model + return {v: k for k, v in name_model_map.items()} + + +def get_flat_models_from_model( + model: Type["BaseModel"], known_models: Union[TypeModelSet, None] = None +) -> TypeModelSet: + known_models = known_models or set() + fields = get_model_fields(model) + get_flat_models_from_fields(fields, known_models=known_models) + return known_models + + +def get_flat_models_from_annotation( + annotation: Any, known_models: TypeModelSet +) -> TypeModelSet: + origin = get_origin(annotation) + if origin is not None: + for arg in get_args(annotation): + if lenient_issubclass(arg, (BaseModel, Enum)) and arg not in known_models: + known_models.add(arg) + if lenient_issubclass(arg, BaseModel): + get_flat_models_from_model(arg, known_models=known_models) + else: + get_flat_models_from_annotation(arg, known_models=known_models) + return known_models + + +def get_flat_models_from_field( + field: ModelField, known_models: TypeModelSet +) -> TypeModelSet: + field_type = field.type_ + if lenient_issubclass(field_type, BaseModel): + if field_type in known_models: + return known_models + known_models.add(field_type) + get_flat_models_from_model(field_type, known_models=known_models) + elif lenient_issubclass(field_type, Enum): + known_models.add(field_type) + else: + get_flat_models_from_annotation(field_type, known_models=known_models) + return known_models + + +def get_flat_models_from_fields( + fields: Sequence[ModelField], known_models: TypeModelSet +) -> TypeModelSet: + for field in fields: + get_flat_models_from_field(field, known_models=known_models) + return known_models + + +def get_long_model_name(model: TypeModelOrEnum) -> str: + return f"{model.__module__}__{model.__qualname__}".replace(".", "__") diff --git a/fastapi/applications.py b/fastapi/applications.py index 5333a281c..79e0c7fc7 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -13,6 +13,7 @@ from typing import ( Union, ) +from annotated_doc import Doc from fastapi import routing from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.exception_handlers import ( @@ -43,7 +44,7 @@ from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse, Response from starlette.routing import BaseRoute from starlette.types import ASGIApp, ExceptionHandler, Lifespan, Receive, Scope, Send -from typing_extensions import Annotated, Doc, deprecated +from typing_extensions import Annotated, deprecated AppType = TypeVar("AppType", bound="FastAPI") @@ -75,7 +76,7 @@ class FastAPI(Starlette): errors. Read more in the - [Starlette docs for Applications](https://www.starlette.io/applications/#instantiating-the-application). + [Starlette docs for Applications](https://www.starlette.dev/applications/#instantiating-the-application). """ ), ] = False, @@ -938,7 +939,7 @@ class FastAPI(Starlette): This is simply inherited from Starlette. Read more about it in the - [Starlette docs for Applications](https://www.starlette.io/applications/#storing-state-on-the-app-instance). + [Starlette docs for Applications](https://www.starlette.dev/applications/#storing-state-on-the-app-instance). """ ), ] = State() diff --git a/fastapi/background.py b/fastapi/background.py index 203578a41..6d4a30d44 100644 --- a/fastapi/background.py +++ b/fastapi/background.py @@ -1,7 +1,8 @@ from typing import Any, Callable +from annotated_doc import Doc from starlette.background import BackgroundTasks as StarletteBackgroundTasks -from typing_extensions import Annotated, Doc, ParamSpec +from typing_extensions import Annotated, ParamSpec P = ParamSpec("P") diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index cf8406b0f..8ad9aa11a 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -10,12 +10,11 @@ from typing import ( cast, ) +from annotated_doc import Doc from fastapi._compat import ( - PYDANTIC_V2, CoreSchema, GetJsonSchemaHandler, JsonSchemaValue, - with_info_plain_validator_function, ) from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401 @@ -24,7 +23,7 @@ from starlette.datastructures import Headers as Headers # noqa: F401 from starlette.datastructures import QueryParams as QueryParams # noqa: F401 from starlette.datastructures import State as State # noqa: F401 from starlette.datastructures import UploadFile as StarletteUploadFile -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated class UploadFile(StarletteUploadFile): @@ -154,11 +153,10 @@ class UploadFile(StarletteUploadFile): raise ValueError(f"Expected UploadFile, received: {type(__input_value)}") return cast(UploadFile, __input_value) - if not PYDANTIC_V2: - - @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - field_schema.update({"type": "string", "format": "binary"}) + # TODO: remove when deprecating Pydantic v1 + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update({"type": "string", "format": "binary"}) @classmethod def __get_pydantic_json_schema__( @@ -170,6 +168,8 @@ class UploadFile(StarletteUploadFile): def __get_pydantic_core_schema__( cls, source: Type[Any], handler: Callable[[Any], CoreSchema] ) -> CoreSchema: + from ._compat.v2 import with_info_plain_validator_function + return with_info_plain_validator_function(cls._validate) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 418c11725..d6359c0f5 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -1,8 +1,18 @@ +import inspect +import sys from dataclasses import dataclass, field -from typing import Any, Callable, List, Optional, Sequence, Tuple +from functools import cached_property +from typing import Any, Callable, List, Optional, Sequence, Union from fastapi._compat import ModelField from fastapi.security.base import SecurityBase +from fastapi.types import DependencyCacheKey +from typing_extensions import Literal + +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction +else: # pragma: no cover + from asyncio import iscoroutinefunction @dataclass @@ -31,7 +41,43 @@ class Dependant: security_scopes: Optional[List[str]] = None use_cache: bool = True path: Optional[str] = None - cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False) + scope: Union[Literal["function", "request"], None] = None - def __post_init__(self) -> None: - self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or [])))) + @cached_property + def cache_key(self) -> DependencyCacheKey: + return ( + self.call, + tuple(sorted(set(self.security_scopes or []))), + self.computed_scope or "", + ) + + @cached_property + def is_gen_callable(self) -> bool: + if inspect.isgeneratorfunction(self.call): + return True + dunder_call = getattr(self.call, "__call__", None) # noqa: B004 + return inspect.isgeneratorfunction(dunder_call) + + @cached_property + def is_async_gen_callable(self) -> bool: + if inspect.isasyncgenfunction(self.call): + return True + dunder_call = getattr(self.call, "__call__", None) # noqa: B004 + return inspect.isasyncgenfunction(dunder_call) + + @cached_property + def is_coroutine_callable(self) -> bool: + if inspect.isroutine(self.call): + return iscoroutinefunction(self.call) + if inspect.isclass(self.call): + return False + dunder_call = getattr(self.call, "__call__", None) # noqa: B004 + return iscoroutinefunction(dunder_call) + + @cached_property + def computed_scope(self) -> Union[str, None]: + if self.scope: + return self.scope + if self.is_gen_callable or self.is_async_gen_callable: + return "request" + return None diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index e49380cb3..c5c6b69bb 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,5 +1,4 @@ import inspect -import sys from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy from dataclasses import dataclass @@ -23,11 +22,11 @@ import anyio from fastapi import params from fastapi._compat import ( PYDANTIC_V2, - ErrorWrapper, ModelField, RequiredParam, Undefined, - _regenerate_error_with_loc, + _is_error_wrapper, + _is_model_class, copy_field_info, create_body_model, evaluate_forwardref, @@ -43,20 +42,24 @@ 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, SecurityRequirement +from fastapi.exceptions import DependencyScopeError from fastapi.logger import logger from fastapi.security.base import SecurityBase from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect +from fastapi.types import DependencyCacheKey from fastapi.utils import create_model_field, get_path_param_names from pydantic import BaseModel from pydantic.fields import FieldInfo @@ -72,12 +75,9 @@ from starlette.datastructures import ( from starlette.requests import HTTPConnection, Request from starlette.responses import Response from starlette.websockets import WebSocket -from typing_extensions import Annotated, get_args, get_origin +from typing_extensions import Annotated, Literal, get_args, get_origin -if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction -else: # pragma: no cover - from asyncio import iscoroutinefunction +from .. import temp_pydantic_v1_params multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' @@ -121,70 +121,23 @@ def ensure_multipart_is_installed() -> None: raise RuntimeError(multipart_not_installed_error) from None -def get_param_sub_dependant( - *, - param_name: str, - depends: params.Depends, - path: str, - security_scopes: Optional[List[str]] = None, -) -> Dependant: - assert depends.dependency - return get_sub_dependant( - depends=depends, - dependency=depends.dependency, - path=path, - name=param_name, - security_scopes=security_scopes, - ) - - def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant: assert callable(depends.dependency), ( "A parameter-less dependency must have a callable dependency" ) - return get_sub_dependant(depends=depends, dependency=depends.dependency, path=path) - - -def get_sub_dependant( - *, - depends: params.Depends, - dependency: Callable[..., Any], - path: str, - name: Optional[str] = None, - security_scopes: Optional[List[str]] = None, -) -> Dependant: - security_requirement = None - security_scopes = security_scopes or [] - if isinstance(depends, params.Security): - dependency_scopes = depends.scopes - security_scopes.extend(dependency_scopes) - if isinstance(dependency, SecurityBase): - use_scopes: List[str] = [] - if isinstance(dependency, (OAuth2, OpenIdConnect)): - use_scopes = security_scopes - security_requirement = SecurityRequirement( - security_scheme=dependency, scopes=use_scopes - ) - sub_dependant = get_dependant( - path=path, - call=dependency, - name=name, - security_scopes=security_scopes, - use_cache=depends.use_cache, + use_security_scopes: List[str] = [] + if isinstance(depends, params.Security) and depends.scopes: + use_security_scopes.extend(depends.scopes) + return get_dependant( + path=path, call=depends.dependency, security_scopes=use_security_scopes ) - if security_requirement: - sub_dependant.security_requirements.append(security_requirement) - return sub_dependant - - -CacheKey = Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] def get_flat_dependant( dependant: Dependant, *, skip_repeats: bool = False, - visited: Optional[List[CacheKey]] = None, + visited: Optional[List[DependencyCacheKey]] = None, ) -> Dependant: if visited is None: visited = [] @@ -219,7 +172,7 @@ def _get_flat_fields_from_params(fields: List[ModelField]) -> List[ModelField]: if not fields: return fields first_field = fields[0] - if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel): + if len(fields) == 1 and _is_model_class(first_field.type_): fields_to_extract = get_cached_model_fields(first_field.type_) return fields_to_extract return fields @@ -277,17 +230,27 @@ def get_dependant( name: Optional[str] = None, security_scopes: Optional[List[str]] = None, use_cache: bool = True, + scope: Union[Literal["function", "request"], None] = None, ) -> Dependant: - path_param_names = get_path_param_names(path) - endpoint_signature = get_typed_signature(call) - signature_params = endpoint_signature.parameters dependant = Dependant( call=call, name=name, path=path, security_scopes=security_scopes, use_cache=use_cache, + scope=scope, ) + path_param_names = get_path_param_names(path) + endpoint_signature = get_typed_signature(call) + signature_params = endpoint_signature.parameters + if isinstance(call, SecurityBase): + use_scopes: List[str] = [] + if isinstance(call, (OAuth2, OpenIdConnect)): + use_scopes = security_scopes or use_scopes + security_requirement = SecurityRequirement( + security_scheme=call, scopes=use_scopes + ) + dependant.security_requirements.append(security_requirement) for param_name, param in signature_params.items(): is_path_param = param_name in path_param_names param_details = analyze_param( @@ -297,11 +260,28 @@ def get_dependant( is_path_param=is_path_param, ) if param_details.depends is not None: - sub_dependant = get_param_sub_dependant( - param_name=param_name, - depends=param_details.depends, + assert param_details.depends.dependency + if ( + (dependant.is_gen_callable or dependant.is_async_gen_callable) + and dependant.computed_scope == "request" + and param_details.depends.scope == "function" + ): + assert dependant.call + raise DependencyScopeError( + f'The dependency "{dependant.call.__name__}" has a scope of ' + '"request", it cannot depend on dependencies with scope "function".' + ) + use_security_scopes = security_scopes or [] + if isinstance(param_details.depends, params.Security): + if param_details.depends.scopes: + use_security_scopes.extend(param_details.depends.scopes) + sub_dependant = get_dependant( path=path, - security_scopes=security_scopes, + call=param_details.depends.dependency, + name=param_name, + security_scopes=use_security_scopes, + use_cache=param_details.depends.use_cache, + scope=param_details.depends.scope, ) dependant.dependencies.append(sub_dependant) continue @@ -315,7 +295,9 @@ def get_dependant( ) continue assert param_details.field is not None - if isinstance(param_details.field.field_info, params.Body): + if isinstance( + param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body) + ): dependant.body_params.append(param_details.field) else: add_param_to_fields(field=param_details.field, dependant=dependant) @@ -374,28 +356,38 @@ def analyze_param( fastapi_annotations = [ arg for arg in annotated_args[1:] - if isinstance(arg, (FieldInfo, params.Depends)) + if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends)) ] fastapi_specific_annotations = [ arg for arg in fastapi_annotations - if isinstance(arg, (params.Param, params.Body, params.Depends)) + if isinstance( + arg, + ( + params.Param, + temp_pydantic_v1_params.Param, + params.Body, + temp_pydantic_v1_params.Body, + params.Depends, + ), + ) ] if fastapi_specific_annotations: - fastapi_annotation: Union[FieldInfo, params.Depends, None] = ( - fastapi_specific_annotations[-1] - ) + fastapi_annotation: Union[ + FieldInfo, may_v1.FieldInfo, params.Depends, None + ] = fastapi_specific_annotations[-1] else: fastapi_annotation = None # Set default for Annotated FieldInfo - if isinstance(fastapi_annotation, FieldInfo): + if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)): # Copy `field_info` because we mutate `field_info.default` below. field_info = copy_field_info( field_info=fastapi_annotation, annotation=use_annotation ) - assert ( - field_info.default is Undefined or field_info.default is RequiredParam - ), ( + assert field_info.default in { + Undefined, + may_v1.Undefined, + } or field_info.default in {RequiredParam, may_v1.RequiredParam}, ( f"`{field_info.__class__.__name__}` default value cannot be set in" f" `Annotated` for {param_name!r}. Set the default value with `=` instead." ) @@ -419,14 +411,15 @@ def analyze_param( ) depends = value # Get FieldInfo from default value - elif isinstance(value, FieldInfo): + elif isinstance(value, (FieldInfo, may_v1.FieldInfo)): assert field_info is None, ( "Cannot specify FastAPI annotations in `Annotated` and default value" f" together for {param_name!r}" ) field_info = value if PYDANTIC_V2: - field_info.annotation = type_annotation + if isinstance(field_info, FieldInfo): + field_info.annotation = type_annotation # Get Depends from type annotation if depends is not None and depends.dependency is None: @@ -463,7 +456,14 @@ def analyze_param( ) or is_uploadfile_sequence_annotation(type_annotation): field_info = params.File(annotation=use_annotation, default=default_value) elif not field_annotation_is_scalar(annotation=type_annotation): - field_info = params.Body(annotation=use_annotation, default=default_value) + if annotation_is_pydantic_v1(use_annotation): + field_info = temp_pydantic_v1_params.Body( + annotation=use_annotation, default=default_value + ) + else: + field_info = params.Body( + annotation=use_annotation, default=default_value + ) else: field_info = params.Query(annotation=use_annotation, default=default_value) @@ -472,12 +472,14 @@ def analyze_param( if field_info is not None: # Handle field_info.in_ if is_path_param: - assert isinstance(field_info, params.Path), ( + assert isinstance( + field_info, (params.Path, temp_pydantic_v1_params.Path) + ), ( f"Cannot use `{field_info.__class__.__name__}` for path param" f" {param_name!r}" ) elif ( - isinstance(field_info, params.Param) + isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)) and getattr(field_info, "in_", None) is None ): field_info.in_ = params.ParamTypes.query @@ -486,7 +488,7 @@ def analyze_param( field_info, param_name, ) - if isinstance(field_info, params.Form): + if isinstance(field_info, (params.Form, temp_pydantic_v1_params.Form)): ensure_multipart_is_installed() if not field_info.alias and getattr(field_info, "convert_underscores", None): alias = param_name.replace("_", "-") @@ -498,19 +500,20 @@ def analyze_param( type_=use_annotation_from_field_info, default=field_info.default, alias=alias, - required=field_info.default in (RequiredParam, Undefined), + required=field_info.default + in (RequiredParam, may_v1.RequiredParam, Undefined), field_info=field_info, ) if is_path_param: assert is_scalar_field(field=field), ( "Path params must be of one of the supported types" ) - elif isinstance(field_info, params.Query): + elif isinstance(field_info, (params.Query, temp_pydantic_v1_params.Query)): assert ( is_scalar_field(field) or is_scalar_sequence_field(field) or ( - lenient_issubclass(field.type_, BaseModel) + _is_model_class(field.type_) # For Pydantic v1 and getattr(field, "shape", 1) == 1 ) @@ -535,36 +538,14 @@ def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None: dependant.cookie_params.append(field) -def is_coroutine_callable(call: Callable[..., Any]) -> bool: - if inspect.isroutine(call): - return iscoroutinefunction(call) - if inspect.isclass(call): - return False - dunder_call = getattr(call, "__call__", None) # noqa: B004 - return iscoroutinefunction(dunder_call) - - -def is_async_gen_callable(call: Callable[..., Any]) -> bool: - if inspect.isasyncgenfunction(call): - return True - dunder_call = getattr(call, "__call__", None) # noqa: B004 - return inspect.isasyncgenfunction(dunder_call) - - -def is_gen_callable(call: Callable[..., Any]) -> bool: - if inspect.isgeneratorfunction(call): - return True - dunder_call = getattr(call, "__call__", None) # noqa: B004 - return inspect.isgeneratorfunction(dunder_call) - - -async def solve_generator( - *, call: Callable[..., Any], stack: AsyncExitStack, sub_values: Dict[str, Any] +async def _solve_generator( + *, dependant: Dependant, stack: AsyncExitStack, sub_values: Dict[str, Any] ) -> Any: - if is_gen_callable(call): - cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values)) - elif is_async_gen_callable(call): - cm = asynccontextmanager(call)(**sub_values) + assert dependant.call + if dependant.is_gen_callable: + cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values)) + elif dependant.is_async_gen_callable: + cm = asynccontextmanager(dependant.call)(**sub_values) return await stack.enter_async_context(cm) @@ -574,7 +555,7 @@ class SolvedDependency: errors: List[Any] background_tasks: Optional[StarletteBackgroundTasks] response: Response - dependency_cache: Dict[Tuple[Callable[..., Any], Tuple[str]], Any] + dependency_cache: Dict[DependencyCacheKey, Any] async def solve_dependencies( @@ -585,10 +566,20 @@ async def solve_dependencies( background_tasks: Optional[StarletteBackgroundTasks] = None, response: Optional[Response] = None, dependency_overrides_provider: Optional[Any] = None, - dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, + dependency_cache: Optional[Dict[DependencyCacheKey, Any]] = None, + # TODO: remove this parameter later, no longer used, not removing it yet as some + # people might be monkey patching this function (although that's not supported) async_exit_stack: AsyncExitStack, embed_body_fields: bool, ) -> SolvedDependency: + request_astack = request.scope.get("fastapi_inner_astack") + assert isinstance(request_astack, AsyncExitStack), ( + "fastapi_inner_astack not found in request scope" + ) + function_astack = request.scope.get("fastapi_function_astack") + assert isinstance(function_astack, AsyncExitStack), ( + "fastapi_function_astack not found in request scope" + ) values: Dict[str, Any] = {} errors: List[Any] = [] if response is None: @@ -597,12 +588,8 @@ async def solve_dependencies( response.status_code = None # type: ignore if dependency_cache is None: dependency_cache = {} - sub_dependant: Dependant for sub_dependant in dependant.dependencies: sub_dependant.call = cast(Callable[..., Any], sub_dependant.call) - sub_dependant.cache_key = cast( - Tuple[Callable[..., Any], Tuple[str]], sub_dependant.cache_key - ) call = sub_dependant.call use_sub_dependant = sub_dependant if ( @@ -619,6 +606,7 @@ async def solve_dependencies( call=call, name=sub_dependant.name, security_scopes=sub_dependant.security_scopes, + scope=sub_dependant.scope, ) solved_result = await solve_dependencies( @@ -638,11 +626,18 @@ async def solve_dependencies( continue if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache: solved = dependency_cache[sub_dependant.cache_key] - elif is_gen_callable(call) or is_async_gen_callable(call): - solved = await solve_generator( - call=call, stack=async_exit_stack, sub_values=solved_result.values + elif ( + use_sub_dependant.is_gen_callable or use_sub_dependant.is_async_gen_callable + ): + use_astack = request_astack + if sub_dependant.scope == "function": + use_astack = function_astack + solved = await _solve_generator( + dependant=use_sub_dependant, + stack=use_astack, + sub_values=solved_result.values, ) - elif is_coroutine_callable(call): + elif use_sub_dependant.is_coroutine_callable: solved = await call(**solved_result.values) else: solved = await run_in_threadpool(call, **solved_result.values) @@ -712,10 +707,10 @@ def _validate_value_with_model_field( else: return deepcopy(field.default), [] v_, errors_ = field.validate(value, values, loc=loc) - if isinstance(errors_, ErrorWrapper): + if _is_error_wrapper(errors_): # type: ignore[arg-type] return None, [errors_] elif isinstance(errors_, list): - new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) + new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=()) return None, new_errors else: return v_, [] @@ -732,7 +727,7 @@ def _get_multidict_value( if ( value is None or ( - isinstance(field.field_info, params.Form) + isinstance(field.field_info, (params.Form, temp_pydantic_v1_params.Form)) and isinstance(value, str) # For type checks and value == "" ) @@ -798,7 +793,7 @@ def request_params_to_args( if single_not_embedded_field: field_info = first_field.field_info - assert isinstance(field_info, params.Param), ( + assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( "Params must be subclasses of Param" ) loc: Tuple[str, ...] = (field_info.in_.value,) @@ -810,7 +805,7 @@ def request_params_to_args( for field in fields: value = _get_multidict_value(field, received_params) field_info = field.field_info - assert isinstance(field_info, params.Param), ( + assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( "Params must be subclasses of Param" ) loc = (field_info.in_.value, field.alias) @@ -837,7 +832,7 @@ def is_union_of_base_models(field_type: Any) -> bool: union_args = get_args(field_type) for arg in union_args: - if not lenient_issubclass(arg, BaseModel): + if not _is_model_class(arg): return False return True @@ -859,8 +854,8 @@ def _should_embed_body_fields(fields: List[ModelField]) -> bool: # If it's a Form (or File) field, it has to be a BaseModel (or a union of BaseModels) to be top level # otherwise it has to be embedded, so that the key value pair can be extracted if ( - isinstance(first_field.field_info, params.Form) - and not lenient_issubclass(first_field.type_, BaseModel) + isinstance(first_field.field_info, (params.Form, temp_pydantic_v1_params.Form)) + and not _is_model_class(first_field.type_) and not is_union_of_base_models(first_field.type_) ): return True @@ -877,14 +872,14 @@ async def _extract_form_body( value = _get_multidict_value(field, received_body) field_info = field.field_info if ( - isinstance(field_info, params.File) + isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) and is_bytes_field(field) and isinstance(value, UploadFile) ): value = await value.read() elif ( is_bytes_sequence_field(field) - and isinstance(field_info, params.File) + and isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) and value_is_sequence(value) ): # For types @@ -925,7 +920,7 @@ async def request_body_to_args( if ( single_not_embedded_field - and lenient_issubclass(first_field.type_, BaseModel) + and _is_model_class(first_field.type_) and isinstance(received_body, FormData) ): fields_to_extract = get_cached_model_fields(first_field.type_) @@ -990,15 +985,28 @@ def get_body_field( BodyFieldInfo_kwargs["default"] = None if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params): BodyFieldInfo: Type[params.Body] = params.File + elif any( + isinstance(f.field_info, temp_pydantic_v1_params.File) + for f in flat_dependant.body_params + ): + BodyFieldInfo: Type[temp_pydantic_v1_params.Body] = temp_pydantic_v1_params.File # type: ignore[no-redef] elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params): BodyFieldInfo = params.Form + elif any( + isinstance(f.field_info, temp_pydantic_v1_params.Form) + for f in flat_dependant.body_params + ): + BodyFieldInfo = temp_pydantic_v1_params.Form # type: ignore[assignment] else: - BodyFieldInfo = params.Body + if annotation_is_pydantic_v1(BodyModel): + BodyFieldInfo = temp_pydantic_v1_params.Body # type: ignore[assignment] + else: + BodyFieldInfo = params.Body body_param_media_types = [ f.field_info.media_type for f in flat_dependant.body_params - if isinstance(f.field_info, params.Body) + if isinstance(f.field_info, (params.Body, temp_pydantic_v1_params.Body)) ] if len(set(body_param_media_types)) == 1: BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0] diff --git a/fastapi/encoders.py b/fastapi/encoders.py index b037f8bb5..6fc6228e1 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -17,14 +17,16 @@ from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union from uuid import UUID +from annotated_doc import Doc +from fastapi._compat import may_v1 from fastapi.types import IncEx from pydantic import BaseModel from pydantic.color import Color from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated -from ._compat import PYDANTIC_V2, UndefinedType, Url, _model_dump +from ._compat import Url, _is_undefined, _model_dump # Taken from Pydantic v1 as is @@ -58,6 +60,7 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]: ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { bytes: lambda o: o.decode(), Color: str, + may_v1.Color: str, datetime.date: isoformat, datetime.datetime: isoformat, datetime.time: isoformat, @@ -74,14 +77,19 @@ 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, } @@ -213,10 +221,10 @@ def jsonable_encoder( include = set(include) if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) - if isinstance(obj, BaseModel): + if isinstance(obj, (BaseModel, may_v1.BaseModel)): # TODO: remove when deprecating Pydantic v1 encoders: Dict[Any, Any] = {} - if not PYDANTIC_V2: + if isinstance(obj, may_v1.BaseModel): encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined] if custom_encoder: encoders = {**encoders, **custom_encoder} @@ -260,7 +268,7 @@ def jsonable_encoder( return str(obj) if isinstance(obj, (str, int, float, type(None))): return obj - if isinstance(obj, UndefinedType): + if _is_undefined(obj): return None if isinstance(obj, dict): encoded_dict = {} diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 44d4ada86..0620428be 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -1,9 +1,10 @@ from typing import Any, Dict, Optional, Sequence, Type, Union +from annotated_doc import Doc from pydantic import BaseModel, create_model from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import WebSocketException as StarletteWebSocketException -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated class HTTPException(StarletteHTTPException): @@ -146,6 +147,13 @@ class FastAPIError(RuntimeError): """ +class DependencyScopeError(FastAPIError): + """ + A dependency declared that it depends on another dependency with an invalid + (narrower) scope. + """ + + class ValidationException(Exception): def __init__(self, errors: Sequence[Any]) -> None: self._errors = errors diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index f181b43c1..74b23a370 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -1,9 +1,10 @@ import json from typing import Any, Dict, Optional +from annotated_doc import Doc from fastapi.encoders import jsonable_encoder from starlette.responses import HTMLResponse -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated swagger_ui_default_parameters: Annotated[ Dict[str, Any], diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 21105cf65..dbc93d289 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -5,7 +5,6 @@ from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, from fastapi import routing from fastapi._compat import ( - GenerateJsonSchema, JsonSchemaValue, ModelField, Undefined, @@ -22,7 +21,7 @@ from fastapi.dependencies.utils import ( get_flat_params, ) from fastapi.encoders import jsonable_encoder -from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE +from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX from fastapi.openapi.models import OpenAPI from fastapi.params import Body, ParamTypes from fastapi.responses import Response @@ -37,6 +36,8 @@ from starlette.responses import JSONResponse from starlette.routing import BaseRoute from typing_extensions import Literal +from .._compat import _is_model_field + validation_error_definition = { "title": "ValidationError", "type": "object", @@ -94,7 +95,6 @@ def get_openapi_security_definitions( def _get_openapi_operation_parameters( *, dependant: Dependant, - schema_generator: GenerateJsonSchema, model_name_map: ModelNameMap, field_mapping: Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue @@ -128,7 +128,6 @@ def _get_openapi_operation_parameters( continue param_schema = get_schema_from_model_field( field=param, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -169,7 +168,6 @@ def _get_openapi_operation_parameters( def get_openapi_operation_request_body( *, body_field: Optional[ModelField], - schema_generator: GenerateJsonSchema, model_name_map: ModelNameMap, field_mapping: Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue @@ -178,10 +176,9 @@ def get_openapi_operation_request_body( ) -> Optional[Dict[str, Any]]: if not body_field: return None - assert isinstance(body_field, ModelField) + assert _is_model_field(body_field) body_schema = get_schema_from_model_field( field=body_field, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -254,7 +251,6 @@ def get_openapi_path( *, route: routing.APIRoute, operation_ids: Set[str], - schema_generator: GenerateJsonSchema, model_name_map: ModelNameMap, field_mapping: Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue @@ -287,7 +283,6 @@ def get_openapi_path( security_schemes.update(security_definitions) operation_parameters = _get_openapi_operation_parameters( dependant=route.dependant, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -309,7 +304,6 @@ def get_openapi_path( if method in METHODS_WITH_BODY: request_body_oai = get_openapi_operation_request_body( body_field=route.body_field, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -327,7 +321,6 @@ def get_openapi_path( ) = get_openapi_path( route=callback, operation_ids=operation_ids, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -358,7 +351,6 @@ def get_openapi_path( if route.response_field: response_schema = get_schema_from_model_field( field=route.response_field, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -392,7 +384,6 @@ def get_openapi_path( if field: additional_field_schema = get_schema_from_model_field( field=field, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -454,7 +445,7 @@ def get_fields_from_routes( route, routing.APIRoute ): if route.body_field: - assert isinstance(route.body_field, ModelField), ( + assert _is_model_field(route.body_field), ( "A request body must be a Pydantic Field" ) body_fields_from_routes.append(route.body_field) @@ -510,10 +501,8 @@ def get_openapi( operation_ids: Set[str] = set() all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or [])) model_name_map = get_compat_model_name_map(all_fields) - schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) field_mapping, definitions = get_definitions( fields=all_fields, - schema_generator=schema_generator, model_name_map=model_name_map, separate_input_output_schemas=separate_input_output_schemas, ) @@ -522,7 +511,6 @@ def get_openapi( result = get_openapi_path( route=route, operation_ids=operation_ids, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -542,7 +530,6 @@ def get_openapi( result = get_openapi_path( route=webhook, operation_ids=operation_ids, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index b3621626c..e32f75593 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1,9 +1,10 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Union +from annotated_doc import Doc from fastapi import params from fastapi._compat import Undefined from fastapi.openapi.models import Example -from typing_extensions import Annotated, Doc, deprecated +from typing_extensions import Annotated, Literal, deprecated _Unset: Any = Undefined @@ -2244,6 +2245,26 @@ def Depends( # noqa: N802 """ ), ] = True, + scope: Annotated[ + Union[Literal["function", "request"], None], + Doc( + """ + Mainly for dependencies with `yield`, define when the dependency function + should start (the code before `yield`) and when it should end (the code + after `yield`). + + * `"function"`: start the dependency before the *path operation function* + that handles the request, end the dependency after the *path operation + function* ends, but **before** the response is sent back to the client. + So, the dependency function will be executed **around** the *path operation + **function***. + * `"request"`: start the dependency before the *path operation function* + that handles the request (similar to when using `"function"`), but end + **after** the response is sent back to the client. So, the dependency + function will be executed **around** the **request** and response cycle. + """ + ), + ] = None, ) -> Any: """ Declare a FastAPI dependency. @@ -2274,7 +2295,7 @@ def Depends( # noqa: N802 return commons ``` """ - return params.Depends(dependency=dependency, use_cache=use_cache) + return params.Depends(dependency=dependency, use_cache=use_cache, scope=scope) def Security( # noqa: N802 diff --git a/fastapi/params.py b/fastapi/params.py index 8f5601dd3..6a58d5808 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -1,10 +1,11 @@ import warnings +from dataclasses import dataclass from enum import Enum from typing import Any, Callable, Dict, List, Optional, Sequence, Union from fastapi.openapi.models import Example from pydantic.fields import FieldInfo -from typing_extensions import Annotated, deprecated +from typing_extensions import Annotated, Literal, deprecated from ._compat import ( PYDANTIC_V2, @@ -22,7 +23,7 @@ class ParamTypes(Enum): cookie = "cookie" -class Param(FieldInfo): +class Param(FieldInfo): # type: ignore[misc] in_: ParamTypes def __init__( @@ -136,7 +137,7 @@ class Param(FieldInfo): return f"{self.__class__.__name__}({self.default})" -class Path(Param): +class Path(Param): # type: ignore[misc] in_ = ParamTypes.path def __init__( @@ -222,7 +223,7 @@ class Path(Param): ) -class Query(Param): +class Query(Param): # type: ignore[misc] in_ = ParamTypes.query def __init__( @@ -306,7 +307,7 @@ class Query(Param): ) -class Header(Param): +class Header(Param): # type: ignore[misc] in_ = ParamTypes.header def __init__( @@ -392,7 +393,7 @@ class Header(Param): ) -class Cookie(Param): +class Cookie(Param): # type: ignore[misc] in_ = ParamTypes.cookie def __init__( @@ -476,7 +477,7 @@ class Cookie(Param): ) -class Body(FieldInfo): +class Body(FieldInfo): # type: ignore[misc] def __init__( self, default: Any = Undefined, @@ -593,7 +594,7 @@ class Body(FieldInfo): return f"{self.__class__.__name__}({self.default})" -class Form(Body): +class Form(Body): # type: ignore[misc] def __init__( self, default: Any = Undefined, @@ -677,7 +678,7 @@ class Form(Body): ) -class File(Form): +class File(Form): # type: ignore[misc] def __init__( self, default: Any = Undefined, @@ -761,26 +762,13 @@ class File(Form): ) +@dataclass class Depends: - def __init__( - self, dependency: Optional[Callable[..., Any]] = None, *, use_cache: bool = True - ): - self.dependency = dependency - self.use_cache = use_cache - - def __repr__(self) -> str: - attr = getattr(self.dependency, "__name__", type(self.dependency).__name__) - cache = "" if self.use_cache else ", use_cache=False" - return f"{self.__class__.__name__}({attr}{cache})" + dependency: Optional[Callable[..., Any]] = None + use_cache: bool = True + scope: Union[Literal["function", "request"], None] = None +@dataclass class Security(Depends): - def __init__( - self, - dependency: Optional[Callable[..., Any]] = None, - *, - scopes: Optional[Sequence[str]] = None, - use_cache: bool = True, - ): - super().__init__(dependency=dependency, use_cache=use_cache) - self.scopes = scopes or [] + scopes: Optional[Sequence[str]] = None diff --git a/fastapi/routing.py b/fastapi/routing.py index 2b83cca28..bf2742032 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -25,7 +25,8 @@ from typing import ( Union, ) -from fastapi import params +from annotated_doc import Doc +from fastapi import params, temp_pydantic_v1_params from fastapi._compat import ( ModelField, Undefined, @@ -77,7 +78,7 @@ from starlette.routing import ( from starlette.routing import Mount as Mount # noqa from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send from starlette.websockets import WebSocket -from typing_extensions import Annotated, Doc, deprecated +from typing_extensions import Annotated, deprecated if sys.version_info >= (3, 13): # pragma: no cover from inspect import iscoroutinefunction @@ -104,10 +105,11 @@ def request_response( async def app(scope: Scope, receive: Receive, send: Send) -> None: # Starts customization response_awaited = False - async with AsyncExitStack() as stack: - scope["fastapi_inner_astack"] = stack - # Same as in Starlette - response = await f(request) + async with AsyncExitStack() as request_stack: + scope["fastapi_inner_astack"] = request_stack + async with AsyncExitStack() as function_stack: + scope["fastapi_function_astack"] = function_stack + response = await f(request) await response(scope, receive, send) # Continues customization response_awaited = True @@ -140,11 +142,11 @@ def websocket_session( session = WebSocket(scope, receive=receive, send=send) async def app(scope: Scope, receive: Receive, send: Send) -> None: - # Starts customization - async with AsyncExitStack() as stack: - scope["fastapi_inner_astack"] = stack - # Same as in Starlette - await func(session) + async with AsyncExitStack() as request_stack: + scope["fastapi_inner_astack"] = request_stack + async with AsyncExitStack() as function_stack: + scope["fastapi_function_astack"] = function_stack + await func(session) # Same as in Starlette await wrap_app_handling_exceptions(app, session)(scope, receive, send) @@ -308,7 +310,9 @@ def get_request_handler( ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" is_coroutine = iscoroutinefunction(dependant.call) - is_body_form = body_field and isinstance(body_field.field_info, params.Form) + is_body_form = body_field and isinstance( + body_field.field_info, (params.Form, temp_pydantic_v1_params.Form) + ) if isinstance(response_class, DefaultPlaceholder): actual_response_class: Type[Response] = response_class.value else: @@ -477,7 +481,9 @@ class APIWebSocketRoute(routing.WebSocketRoute): self.name = get_name(endpoint) if name is None else name self.dependencies = list(dependencies or []) self.path_regex, self.path_format, self.param_convertors = compile_path(path) - self.dependant = get_dependant(path=self.path_format, call=self.endpoint) + self.dependant = get_dependant( + path=self.path_format, call=self.endpoint, scope="function" + ) for depends in self.dependencies[::-1]: self.dependant.dependencies.insert( 0, @@ -638,7 +644,9 @@ class APIRoute(routing.Route): @cached_property def dependant(self) -> Dependant: - dependant = get_dependant(path=self.path_format, call=self.endpoint) + dependant = get_dependant( + path=self.path_format, call=self.endpoint, scope="function" + ) for depends in self.dependencies[::-1]: dependant.dependencies.insert( 0, diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 6d6dd01d9..496c815a7 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -1,11 +1,12 @@ from typing import Optional +from annotated_doc import Doc from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.status import HTTP_403_FORBIDDEN -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated class APIKeyBase(SecurityBase): diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 9ab2df3c9..3a5985650 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -2,6 +2,7 @@ import binascii from base64 import b64decode from typing import Optional +from annotated_doc import Doc from fastapi.exceptions import HTTPException from fastapi.openapi.models import HTTPBase as HTTPBaseModel from fastapi.openapi.models import HTTPBearer as HTTPBearerModel @@ -10,7 +11,7 @@ from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated class HTTPBasicCredentials(BaseModel): diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index fdedbc2da..f8d97d762 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -1,5 +1,6 @@ from typing import Any, Dict, List, Optional, Union, cast +from annotated_doc import Doc from fastapi.exceptions import HTTPException from fastapi.openapi.models import OAuth2 as OAuth2Model from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel @@ -10,7 +11,7 @@ from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN # TODO: import from typing when deprecating Python 3.9 -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated class OAuth2PasswordRequestForm: diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index c8cceb911..5e99798e6 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -1,11 +1,12 @@ from typing import Optional +from annotated_doc import Doc from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.status import HTTP_403_FORBIDDEN -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated class OpenIdConnect(SecurityBase): diff --git a/fastapi/temp_pydantic_v1_params.py b/fastapi/temp_pydantic_v1_params.py new file mode 100644 index 000000000..e41d71230 --- /dev/null +++ b/fastapi/temp_pydantic_v1_params.py @@ -0,0 +1,724 @@ +import warnings +from typing import Any, Callable, Dict, List, Optional, Union + +from fastapi.openapi.models import Example +from fastapi.params import ParamTypes +from typing_extensions import Annotated, deprecated + +from ._compat.may_v1 import FieldInfo, Undefined +from ._compat.shared import PYDANTIC_VERSION_MINOR_TUPLE + +_Unset: Any = Undefined + + +class Param(FieldInfo): # type: ignore[misc] + in_: ParamTypes + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + if example is not _Unset: + warnings.warn( + "`example` has been deprecated, please use `examples` instead", + category=DeprecationWarning, + stacklevel=4, + ) + self.example = example + self.include_in_schema = include_in_schema + self.openapi_examples = openapi_examples + kwargs = dict( + default=default, + default_factory=default_factory, + alias=alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + discriminator=discriminator, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + **extra, + ) + if examples is not None: + kwargs["examples"] = examples + if regex is not None: + warnings.warn( + "`regex` has been deprecated, please use `pattern` instead", + category=DeprecationWarning, + stacklevel=4, + ) + current_json_schema_extra = json_schema_extra or extra + if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7): + self.deprecated = deprecated + else: + kwargs["deprecated"] = deprecated + kwargs["regex"] = pattern or regex + kwargs.update(**current_json_schema_extra) + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} + + super().__init__(**use_kwargs) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.default})" + + +class Path(Param): # type: ignore[misc] + in_ = ParamTypes.path + + def __init__( + self, + default: Any = ..., + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + assert default is ..., "Path parameters cannot have a default value" + self.in_ = self.in_ + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + example=example, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +class Query(Param): # type: ignore[misc] + in_ = ParamTypes.query + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + example=example, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +class Header(Param): # type: ignore[misc] + in_ = ParamTypes.header + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + convert_underscores: bool = True, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + self.convert_underscores = convert_underscores + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + example=example, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +class Cookie(Param): # type: ignore[misc] + in_ = ParamTypes.cookie + + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + example=example, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +class Body(FieldInfo): # type: ignore[misc] + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + embed: Union[bool, None] = None, + media_type: str = "application/json", + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + self.embed = embed + self.media_type = media_type + if example is not _Unset: + warnings.warn( + "`example` has been deprecated, please use `examples` instead", + category=DeprecationWarning, + stacklevel=4, + ) + self.example = example + self.include_in_schema = include_in_schema + self.openapi_examples = openapi_examples + kwargs = dict( + default=default, + default_factory=default_factory, + alias=alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + discriminator=discriminator, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + **extra, + ) + if examples is not None: + kwargs["examples"] = examples + if regex is not None: + warnings.warn( + "`regex` has been deprecated, please use `pattern` instead", + category=DeprecationWarning, + stacklevel=4, + ) + current_json_schema_extra = json_schema_extra or extra + if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7): + self.deprecated = deprecated + else: + kwargs["deprecated"] = deprecated + kwargs["regex"] = pattern or regex + kwargs.update(**current_json_schema_extra) + + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} + + super().__init__(**use_kwargs) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.default})" + + +class Form(Body): # type: ignore[misc] + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + media_type: str = "application/x-www-form-urlencoded", + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + media_type=media_type, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + example=example, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) + + +class File(Form): # type: ignore[misc] + def __init__( + self, + default: Any = Undefined, + *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, + media_type: str = "multipart/form-data", + alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, + title: Optional[str] = None, + description: Optional[str] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Union[deprecated, str, bool, None] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, + **extra: Any, + ): + super().__init__( + default=default, + default_factory=default_factory, + annotation=annotation, + media_type=media_type, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + description=description, + gt=gt, + ge=ge, + lt=lt, + le=le, + min_length=min_length, + max_length=max_length, + pattern=pattern, + regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, + example=example, + examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, + **extra, + ) diff --git a/fastapi/types.py b/fastapi/types.py index 3205654c7..3f4e81a7c 100644 --- a/fastapi/types.py +++ b/fastapi/types.py @@ -1,6 +1,6 @@ import types from enum import Enum -from typing import Any, Callable, Dict, Set, Type, TypeVar, Union +from typing import Any, Callable, Dict, Optional, Set, Tuple, Type, TypeVar, Union from pydantic import BaseModel @@ -8,3 +8,4 @@ DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]) UnionType = getattr(types, "UnionType", Union) ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str] IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]] +DependencyCacheKey = Tuple[Optional[Callable[..., Any]], Tuple[str, ...], str] diff --git a/fastapi/utils.py b/fastapi/utils.py index 98725ff19..2e79ee6b1 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -23,10 +23,12 @@ from fastapi._compat import ( Undefined, UndefinedType, Validator, + annotation_is_pydantic_v1, lenient_issubclass, + may_v1, ) from fastapi.datastructures import DefaultPlaceholder, DefaultType -from pydantic import BaseModel, create_model +from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Literal @@ -60,50 +62,74 @@ def get_path_param_names(path: str) -> Set[str]: return set(re.findall("{(.*?)}", path)) +_invalid_args_message = ( + "Invalid args for response field! Hint: " + "check that {type_} is a valid Pydantic field type. " + "If you are using a return type annotation that is not a valid Pydantic " + "field (e.g. Union[Response, dict, None]) you can disable generating the " + "response model from the type annotation with the path operation decorator " + "parameter response_model=None. Read more: " + "https://fastapi.tiangolo.com/tutorial/response-model/" +) + + def create_model_field( name: str, type_: Any, class_validators: Optional[Dict[str, Validator]] = None, default: Optional[Any] = Undefined, required: Union[bool, UndefinedType] = Undefined, - model_config: Type[BaseConfig] = BaseConfig, + model_config: Union[Type[BaseConfig], None] = None, field_info: Optional[FieldInfo] = None, alias: Optional[str] = None, mode: Literal["validation", "serialization"] = "validation", + version: Literal["1", "auto"] = "auto", ) -> ModelField: class_validators = class_validators or {} - if PYDANTIC_V2: + + v1_model_config = 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[no-any-return] + except RuntimeError: + raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None + elif PYDANTIC_V2: + from ._compat import v2 + field_info = field_info or FieldInfo( annotation=type_, default=default, alias=alias ) - else: - field_info = field_info or FieldInfo() - kwargs = {"name": name, "field_info": field_info} - if PYDANTIC_V2: - kwargs.update({"mode": mode}) - else: - kwargs.update( - { - "type_": type_, - "class_validators": class_validators, - "default": default, - "required": required, - "model_config": model_config, - "alias": alias, - } - ) + kwargs = {"mode": mode, "name": name, "field_info": field_info} + try: + return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type] + except PydanticSchemaGenerationError: + raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None + # Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be + # a Pydantic v1 type, like a constrained int + from fastapi._compat import v1 + try: - return ModelField(**kwargs) # type: ignore[arg-type] - except (RuntimeError, PydanticSchemaGenerationError): - raise fastapi.exceptions.FastAPIError( - "Invalid args for response field! Hint: " - f"check that {type_} is a valid Pydantic field type. " - "If you are using a return type annotation that is not a valid Pydantic " - "field (e.g. Union[Response, dict, None]) you can disable generating the " - "response model from the type annotation with the path operation decorator " - "parameter response_model=None. Read more: " - "https://fastapi.tiangolo.com/tutorial/response-model/" - ) from None + return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] + except RuntimeError: + raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None def create_cloned_field( @@ -112,7 +138,13 @@ def create_cloned_field( cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None, ) -> ModelField: if PYDANTIC_V2: - return field + from ._compat import v2 + + if isinstance(field, v2.ModelField): + return field + + 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: @@ -122,17 +154,18 @@ def create_cloned_field( if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"): original_type = original_type.__pydantic_model__ use_type = original_type - if lenient_issubclass(original_type, BaseModel): - original_type = cast(Type[BaseModel], original_type) + if lenient_issubclass(original_type, v1.BaseModel): + original_type = cast(Type[v1.BaseModel], original_type) use_type = cloned_types.get(original_type) if use_type is None: - use_type = create_model(original_type.__name__, __base__=original_type) + use_type = v1.create_model(original_type.__name__, __base__=original_type) cloned_types[original_type] = use_type for f in original_type.__fields__.values(): use_type.__fields__[f.name] = create_cloned_field( - f, cloned_types=cloned_types + f, + cloned_types=cloned_types, ) - new_field = create_model_field(name=field.name, type_=use_type) + new_field = create_model_field(name=field.name, type_=use_type, version="1") new_field.has_alias = field.has_alias # type: ignore[attr-defined] new_field.alias = field.alias # type: ignore[misc] new_field.class_validators = field.class_validators # type: ignore[attr-defined] diff --git a/pyproject.toml b/pyproject.toml index 41ef1eb76..7d2be0074 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,8 @@ name = "fastapi" dynamic = ["version"] description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" readme = "README.md" +license = "MIT" +license-files = ["LICENSE"] requires-python = ">=3.8" authors = [ { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, @@ -31,7 +33,6 @@ classifiers = [ "Framework :: Pydantic :: 1", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -39,13 +40,15 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.40.0,<0.49.0", + "starlette>=0.40.0,<0.50.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.8.0", + "annotated-doc>=0.0.2", ] [project.urls] diff --git a/requirements-docs.txt b/requirements-docs.txt index 0013f9f79..696eb2a33 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -11,9 +11,9 @@ jieba==0.42.1 pillow==11.3.0 # For image processing by Material for MkDocs cairosvg==2.8.2 -mkdocstrings[python]==0.26.1 -griffe-typingdoc==0.2.9 +mkdocstrings[python]==0.30.1 +griffe-typingdoc==0.3.0 # For griffe, it formats with black black==25.1.0 -mkdocs-macros-plugin==1.4.0 +mkdocs-macros-plugin==1.4.1 markdown-include-variants==0.0.5 diff --git a/requirements-tests.txt b/requirements-tests.txt index 79aac7e7e..c5de4157e 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -4,7 +4,7 @@ pytest >=7.1.3,<9.0.0 coverage[toml] >= 6.5.0,< 8.0 mypy ==1.14.1 dirty-equals ==0.9.0 -sqlmodel==0.0.25 +sqlmodel==0.0.27 flask >=1.1.2,<4.0.0 anyio[trio] >=3.2.1,<5.0.0 PyJWT==2.9.0 diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100755 index 000000000..e07b51ec5 --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e +set -x + +coverage combine +coverage report +coverage html diff --git a/scripts/test-cov-html.sh b/scripts/test-cov-html.sh index 517ac6422..f87f906dc 100755 --- a/scripts/test-cov-html.sh +++ b/scripts/test-cov-html.sh @@ -4,6 +4,4 @@ set -e set -x bash scripts/test.sh ${@} -coverage combine -coverage report -coverage html +bash scripts/coverage.sh diff --git a/tests/test_compat.py b/tests/test_compat.py index 43c686489..0184c9a2e 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -2,53 +2,48 @@ from typing import Any, Dict, List, Union from fastapi import FastAPI, UploadFile from fastapi._compat import ( - ModelField, Undefined, _get_model_config, get_cached_model_fields, - get_model_fields, - is_bytes_sequence_annotation, is_scalar_field, is_uploadfile_sequence_annotation, + may_v1, ) +from fastapi._compat.shared import is_bytes_sequence_annotation from fastapi.testclient import TestClient -from pydantic import BaseConfig, BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict from pydantic.fields import FieldInfo -from .utils import needs_pydanticv1, needs_pydanticv2 +from .utils import needs_py_lt_314, needs_pydanticv2 @needs_pydanticv2 def test_model_field_default_required(): + from fastapi._compat import v2 + # For coverage field_info = FieldInfo(annotation=str) - field = ModelField(name="foo", field_info=field_info) + field = v2.ModelField(name="foo", field_info=field_info) assert field.default is Undefined -@needs_pydanticv1 -def test_upload_file_dummy_with_info_plain_validator_function(): +@needs_py_lt_314 +def test_v1_plain_validator_function(): + from fastapi._compat import v1 + # For coverage - assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {} + def func(v): # pragma: no cover + return v + + result = v1.with_info_plain_validator_function(func) + assert result == {} -@needs_pydanticv1 -def test_union_scalar_list(): +def test_is_model_field(): # For coverage - # TODO: there might not be a current valid code path that uses this, it would - # potentially enable query parameters defined as both a scalar and a list - # but that would require more refactors, also not sure it's really useful - from fastapi._compat import is_pv1_scalar_field + from fastapi._compat import _is_model_field - field_info = FieldInfo() - field = ModelField( - name="foo", - field_info=field_info, - type_=Union[str, List[int]], - class_validators={}, - model_config=BaseConfig, - ) - assert not is_pv1_scalar_field(field) + assert not _is_model_field(str) @needs_pydanticv2 @@ -141,21 +136,27 @@ def test_is_uploadfile_sequence_annotation(): assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) +@needs_py_lt_314 def test_is_pv1_scalar_field(): + from fastapi._compat import v1 + # For coverage - class Model(BaseModel): + class Model(v1.BaseModel): foo: Union[str, Dict[str, Any]] - fields = get_model_fields(Model) + fields = v1.get_model_fields(Model) assert not is_scalar_field(fields[0]) +@needs_py_lt_314 def test_get_model_fields_cached(): - class Model(BaseModel): + from fastapi._compat import v1 + + class Model(may_v1.BaseModel): foo: str - non_cached_fields = get_model_fields(Model) - non_cached_fields2 = get_model_fields(Model) + non_cached_fields = v1.get_model_fields(Model) + non_cached_fields2 = v1.get_model_fields(Model) cached_fields = get_cached_model_fields(Model) cached_fields2 = get_cached_model_fields(Model) for f1, f2 in zip(cached_fields, cached_fields2): diff --git a/tests/test_compat_params_v1.py b/tests/test_compat_params_v1.py new file mode 100644 index 000000000..7064761cb --- /dev/null +++ b/tests/test_compat_params_v1.py @@ -0,0 +1,1122 @@ +import sys +from typing import List, Optional + +import pytest + +from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi._compat.v1 import BaseModel +from fastapi.temp_pydantic_v1_params import ( + Body, + Cookie, + File, + Form, + Header, + Path, + Query, +) +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from typing_extensions import Annotated + + +class Item(BaseModel): + name: str + price: float + description: Optional[str] = None + + +app = FastAPI() + + +@app.get("/items/{item_id}") +def get_item_with_path( + item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=1000)], +): + return {"item_id": item_id} + + +@app.get("/items/") +def get_items_with_query( + q: Annotated[ + Optional[str], Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$") + ] = None, + skip: Annotated[int, Query(ge=0)] = 0, + limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10, +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/users/") +def get_user_with_header( + x_custom: Annotated[Optional[str], Header()] = None, + x_token: Annotated[Optional[str], Header(convert_underscores=True)] = None, +): + return {"x_custom": x_custom, "x_token": x_token} + + +@app.get("/cookies/") +def get_cookies( + session_id: Annotated[Optional[str], Cookie()] = None, + tracking_id: Annotated[Optional[str], Cookie(min_length=10)] = None, +): + return {"session_id": session_id, "tracking_id": tracking_id} + + +@app.post("/items/") +def create_item( + item: Annotated[ + Item, + Body(examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}]), + ], +): + return {"item": item} + + +@app.post("/items-embed/") +def create_item_embed( + item: Annotated[Item, Body(embed=True)], +): + return {"item": item} + + +@app.put("/items/{item_id}") +def update_item( + item_id: Annotated[int, Path(ge=1)], + item: Annotated[Item, Body()], + importance: Annotated[int, Body(gt=0, le=10)], +): + return {"item": item, "importance": importance} + + +@app.post("/form-data/") +def submit_form( + username: Annotated[str, Form(min_length=3, max_length=50)], + password: Annotated[str, Form(min_length=8)], + email: Annotated[Optional[str], Form()] = None, +): + return {"username": username, "password": password, "email": email} + + +@app.post("/upload/") +def upload_file( + file: Annotated[bytes, File()], + description: Annotated[Optional[str], Form()] = None, +): + return {"file_size": len(file), "description": description} + + +@app.post("/upload-multiple/") +def upload_multiple_files( + files: Annotated[List[bytes], File()], + note: Annotated[str, Form()] = "", +): + return { + "file_count": len(files), + "total_size": sum(len(f) for f in files), + "note": note, + } + + +client = TestClient(app) + + +# Path parameter tests +def test_path_param_valid(): + response = client.get("/items/50") + assert response.status_code == 200 + assert response.json() == {"item_id": 50} + + +def test_path_param_too_large(): + response = client.get("/items/1001") + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["path", "item_id"] + + +def test_path_param_too_small(): + response = client.get("/items/0") + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["path", "item_id"] + + +# Query parameter tests +def test_query_params_valid(): + response = client.get("/items/?q=test search&skip=5&limit=20") + assert response.status_code == 200 + assert response.json() == {"q": "test search", "skip": 5, "limit": 20} + + +def test_query_params_defaults(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"q": None, "skip": 0, "limit": 10} + + +def test_query_param_too_short(): + response = client.get("/items/?q=ab") + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["query", "q"] + + +def test_query_param_invalid_pattern(): + response = client.get("/items/?q=test@#$") + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["query", "q"] + + +def test_query_param_limit_too_large(): + response = client.get("/items/?limit=101") + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["query", "limit"] + + +# Header parameter tests +def test_header_params(): + response = client.get( + "/users/", + headers={"X-Custom": "Plumbus", "X-Token": "secret-token"}, + ) + assert response.status_code == 200 + assert response.json() == { + "x_custom": "Plumbus", + "x_token": "secret-token", + } + + +def test_header_underscore_conversion(): + response = client.get( + "/users/", + headers={"x-token": "secret-token-with-dash"}, + ) + assert response.status_code == 200 + assert response.json()["x_token"] == "secret-token-with-dash" + + +def test_header_params_none(): + response = client.get("/users/") + assert response.status_code == 200 + assert response.json() == {"x_custom": None, "x_token": None} + + +# Cookie parameter tests +def test_cookie_params(): + with TestClient(app) as client: + client.cookies.set("session_id", "abc123") + client.cookies.set("tracking_id", "1234567890abcdef") + response = client.get("/cookies/") + assert response.status_code == 200 + assert response.json() == { + "session_id": "abc123", + "tracking_id": "1234567890abcdef", + } + + +def test_cookie_tracking_id_too_short(): + with TestClient(app) as client: + client.cookies.set("tracking_id", "short") + response = client.get("/cookies/") + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["cookie", "tracking_id"], + "msg": "ensure this value has at least 10 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 10}, + } + ] + } + ) + + +def test_cookie_params_none(): + response = client.get("/cookies/") + assert response.status_code == 200 + assert response.json() == {"session_id": None, "tracking_id": None} + + +# Body parameter tests +def test_body_param(): + response = client.post( + "/items/", + json={"name": "Test Item", "price": 29.99, "description": "A test item"}, + ) + assert response.status_code == 200 + assert response.json() == { + "item": { + "name": "Test Item", + "price": 29.99, + "description": "A test item", + } + } + + +def test_body_param_minimal(): + response = client.post( + "/items/", + json={"name": "Minimal", "price": 9.99}, + ) + assert response.status_code == 200 + assert response.json() == { + "item": {"name": "Minimal", "price": 9.99, "description": None} + } + + +def test_body_param_missing_required(): + response = client.post( + "/items/", + json={"name": "Incomplete"}, + ) + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["body", "price"] + + +def test_body_embed(): + response = client.post( + "/items-embed/", + json={"item": {"name": "Embedded", "price": 15.0}}, + ) + assert response.status_code == 200 + assert response.json() == { + "item": {"name": "Embedded", "price": 15.0, "description": None} + } + + +def test_body_embed_wrong_structure(): + response = client.post( + "/items-embed/", + json={"name": "Not Embedded", "price": 15.0}, + ) + assert response.status_code == 422 + + +# Multiple body parameters test +def test_multiple_body_params(): + response = client.put( + "/items/5", + json={ + "item": {"name": "Updated Item", "price": 49.99}, + "importance": 8, + }, + ) + assert response.status_code == 200 + assert response.json() == snapshot( + { + "item": {"name": "Updated Item", "price": 49.99, "description": None}, + "importance": 8, + } + ) + + +def test_multiple_body_params_importance_too_large(): + response = client.put( + "/items/5", + json={ + "item": {"name": "Item", "price": 10.0}, + "importance": 11, + }, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "importance"], + "msg": "ensure this value is less than or equal to 10", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 10}, + } + ] + } + ) + + +def test_multiple_body_params_importance_too_small(): + response = client.put( + "/items/5", + json={ + "item": {"name": "Item", "price": 10.0}, + "importance": 0, + }, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "importance"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 0}, + } + ] + } + ) + + +# Form parameter tests +def test_form_data_valid(): + response = client.post( + "/form-data/", + data={ + "username": "testuser", + "password": "password123", + "email": "test@example.com", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "testuser", + "password": "password123", + "email": "test@example.com", + } + + +def test_form_data_optional_field(): + response = client.post( + "/form-data/", + data={"username": "testuser", "password": "password123"}, + ) + assert response.status_code == 200 + assert response.json() == { + "username": "testuser", + "password": "password123", + "email": None, + } + + +def test_form_data_username_too_short(): + response = client.post( + "/form-data/", + data={"username": "ab", "password": "password123"}, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "ensure this value has at least 3 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_form_data_password_too_short(): + response = client.post( + "/form-data/", + data={"username": "testuser", "password": "short"}, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "ensure this value has at least 8 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 8}, + } + ] + } + ) + + +# File upload tests +def test_upload_file(): + response = client.post( + "/upload/", + files={"file": ("test.txt", b"Hello, World!", "text/plain")}, + data={"description": "A test file"}, + ) + assert response.status_code == 200 + assert response.json() == { + "file_size": 13, + "description": "A test file", + } + + +def test_upload_file_without_description(): + response = client.post( + "/upload/", + files={"file": ("test.txt", b"Hello!", "text/plain")}, + ) + assert response.status_code == 200 + assert response.json() == { + "file_size": 6, + "description": None, + } + + +def test_upload_multiple_files(): + response = client.post( + "/upload-multiple/", + files=[ + ("files", ("file1.txt", b"Content 1", "text/plain")), + ("files", ("file2.txt", b"Content 2", "text/plain")), + ("files", ("file3.txt", b"Content 3", "text/plain")), + ], + data={"note": "Multiple files uploaded"}, + ) + assert response.status_code == 200 + assert response.json() == { + "file_count": 3, + "total_size": 27, + "note": "Multiple files uploaded", + } + + +def test_upload_multiple_files_empty_note(): + response = client.post( + "/upload-multiple/", + files=[ + ("files", ("file1.txt", b"Test", "text/plain")), + ], + ) + assert response.status_code == 200 + assert response.json()["file_count"] == 1 + assert response.json()["note"] == "" + + +# __repr__ tests +def test_query_repr(): + query_param = Query(default=None, min_length=3) + assert repr(query_param) == "Query(None)" + + +def test_body_repr(): + body_param = Body(default=None) + assert repr(body_param) == "Body(None)" + + +# Deprecation warning tests for regex parameter +def test_query_regex_deprecation_warning(): + with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"): + Query(regex="^test$") + + +def test_body_regex_deprecation_warning(): + with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"): + Body(regex="^test$") + + +# Deprecation warning tests for example parameter +def test_query_example_deprecation_warning(): + with pytest.warns(DeprecationWarning, match="`example` has been deprecated"): + Query(example="test example") + + +def test_body_example_deprecation_warning(): + with pytest.warns(DeprecationWarning, match="`example` has been deprecated"): + Body(example={"test": "example"}) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Get Item With Path", + "operationId": "get_item_with_path_items__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": { + "title": "The ID of the item", + "minimum": 1, + "maximum": 1000, + "type": "integer", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": { + "title": "Item Id", + "minimum": 1, + "type": "integer", + }, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": pydantic_snapshot( + v1=snapshot( + { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + ), + v2=snapshot( + { + "title": "Body", + "allOf": [ + { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + ], + } + ), + ), + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/items/": { + "get": { + "summary": "Get Items With Query", + "operationId": "get_items_with_query_items__get", + "parameters": [ + { + "name": "q", + "in": "query", + "required": False, + "schema": { + "title": "Q", + "maxLength": 50, + "minLength": 3, + "pattern": "^[a-zA-Z0-9 ]+$", + "type": "string", + }, + }, + { + "name": "skip", + "in": "query", + "required": False, + "schema": { + "title": "Skip", + "default": 0, + "minimum": 0, + "type": "integer", + }, + }, + { + "name": "limit", + "in": "query", + "required": False, + "schema": { + "title": "Limit", + "default": 10, + "minimum": 1, + "maximum": 100, + "examples": [5], + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "title": "Item", + "examples": [ + { + "name": "Foo", + "price": 35.4, + "description": "The Foo item", + } + ], + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/users/": { + "get": { + "summary": "Get User With Header", + "operationId": "get_user_with_header_users__get", + "parameters": [ + { + "name": "x-custom", + "in": "header", + "required": False, + "schema": {"title": "X-Custom", "type": "string"}, + }, + { + "name": "x-token", + "in": "header", + "required": False, + "schema": {"title": "X-Token", "type": "string"}, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookies/": { + "get": { + "summary": "Get Cookies", + "operationId": "get_cookies_cookies__get", + "parameters": [ + { + "name": "session_id", + "in": "cookie", + "required": False, + "schema": {"title": "Session Id", "type": "string"}, + }, + { + "name": "tracking_id", + "in": "cookie", + "required": False, + "schema": { + "title": "Tracking Id", + "minLength": 10, + "type": "string", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items-embed/": { + "post": { + "summary": "Create Item Embed", + "operationId": "create_item_embed_items_embed__post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v1=snapshot( + { + "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post" + } + ), + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post" + } + ], + "title": "Body", + } + ), + ), + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/form-data/": { + "post": { + "summary": "Submit Form", + "operationId": "submit_form_form_data__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": pydantic_snapshot( + v1=snapshot( + { + "$ref": "#/components/schemas/Body_submit_form_form_data__post" + } + ), + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_submit_form_form_data__post" + } + ], + "title": "Body", + } + ), + ), + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/upload/": { + "post": { + "summary": "Upload File", + "operationId": "upload_file_upload__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": pydantic_snapshot( + v1=snapshot( + { + "$ref": "#/components/schemas/Body_upload_file_upload__post" + } + ), + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_upload_file_upload__post" + } + ], + "title": "Body", + } + ), + ), + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/upload-multiple/": { + "post": { + "summary": "Upload Multiple Files", + "operationId": "upload_multiple_files_upload_multiple__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": pydantic_snapshot( + v1=snapshot( + { + "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post" + } + ), + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post" + } + ], + "title": "Body", + } + ), + ), + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_item_embed_items_embed__post": { + "properties": pydantic_snapshot( + v1=snapshot( + {"item": {"$ref": "#/components/schemas/Item"}} + ), + v2=snapshot( + { + "item": { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + } + ), + ), + "type": "object", + "required": ["item"], + "title": "Body_create_item_embed_items_embed__post", + }, + "Body_submit_form_form_data__post": { + "properties": { + "username": { + "type": "string", + "maxLength": 50, + "minLength": 3, + "title": "Username", + }, + "password": { + "type": "string", + "minLength": 8, + "title": "Password", + }, + "email": {"type": "string", "title": "Email"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "Body_submit_form_form_data__post", + }, + "Body_update_item_items__item_id__put": { + "properties": { + "item": pydantic_snapshot( + v1=snapshot({"$ref": "#/components/schemas/Item"}), + v2=snapshot( + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + ), + ), + "importance": { + "type": "integer", + "maximum": 10.0, + "exclusiveMinimum": 0.0, + "title": "Importance", + }, + }, + "type": "object", + "required": ["item", "importance"], + "title": "Body_update_item_items__item_id__put", + }, + "Body_upload_file_upload__post": { + "properties": { + "file": { + "type": "string", + "format": "binary", + "title": "File", + }, + "description": {"type": "string", "title": "Description"}, + }, + "type": "object", + "required": ["file"], + "title": "Body_upload_file_upload__post", + }, + "Body_upload_multiple_files_upload_multiple__post": { + "properties": { + "files": { + "items": {"type": "string", "format": "binary"}, + "type": "array", + "title": "Files", + }, + "note": {"type": "string", "title": "Note", "default": ""}, + }, + "type": "object", + "required": ["files"], + "title": "Body_upload_multiple_files_upload_multiple__post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "price": {"type": "number", "title": "Price"}, + "description": {"type": "string", "title": "Description"}, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_dependency_paramless.py b/tests/test_dependency_paramless.py new file mode 100644 index 000000000..9c3cc3878 --- /dev/null +++ b/tests/test_dependency_paramless.py @@ -0,0 +1,78 @@ +from typing import Union + +from fastapi import FastAPI, HTTPException, Security +from fastapi.security import ( + OAuth2PasswordBearer, + SecurityScopes, +) +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +def process_auth( + credentials: Annotated[Union[str, None], Security(oauth2_scheme)], + security_scopes: SecurityScopes, +): + # This is an incorrect way of using it, this is not checking if the scopes are + # provided by the token, only if the endpoint is requesting them, but the test + # here is just to check if FastAPI is indeed registering and passing the scopes + # correctly when using Security with parameterless dependencies. + if "a" not in security_scopes.scopes or "b" not in security_scopes.scopes: + raise HTTPException(detail="a or b not in scopes", status_code=401) + return {"token": credentials, "scopes": security_scopes.scopes} + + +@app.get("/get-credentials") +def get_credentials( + credentials: Annotated[dict, Security(process_auth, scopes=["a", "b"])], +): + return credentials + + +@app.get( + "/parameterless-with-scopes", + dependencies=[Security(process_auth, scopes=["a", "b"])], +) +def get_parameterless_with_scopes(): + return {"status": "ok"} + + +@app.get( + "/parameterless-without-scopes", + dependencies=[Security(process_auth)], +) +def get_parameterless_without_scopes(): + return {"status": "ok"} + + +client = TestClient(app) + + +def test_get_credentials(): + response = client.get("/get-credentials", headers={"authorization": "Bearer token"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "token", "scopes": ["a", "b"]} + + +def test_parameterless_with_scopes(): + response = client.get( + "/parameterless-with-scopes", headers={"authorization": "Bearer token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +def test_parameterless_without_scopes(): + response = client.get( + "/parameterless-without-scopes", headers={"authorization": "Bearer token"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "a or b not in scopes"} + + +def test_call_get_parameterless_without_scopes_for_coverage(): + assert get_parameterless_without_scopes() == {"status": "ok"} diff --git a/tests/test_dependency_yield_scope.py b/tests/test_dependency_yield_scope.py new file mode 100644 index 000000000..a5227dd7a --- /dev/null +++ b/tests/test_dependency_yield_scope.py @@ -0,0 +1,184 @@ +import json +from typing import Any, Tuple + +import pytest +from fastapi import Depends, FastAPI +from fastapi.exceptions import FastAPIError +from fastapi.responses import StreamingResponse +from fastapi.testclient import TestClient +from typing_extensions import Annotated + + +class Session: + def __init__(self) -> None: + self.open = True + + +def dep_session() -> Any: + s = Session() + yield s + s.open = False + + +SessionFuncDep = Annotated[Session, Depends(dep_session, scope="function")] +SessionRequestDep = Annotated[Session, Depends(dep_session, scope="request")] +SessionDefaultDep = Annotated[Session, Depends(dep_session)] + + +class NamedSession: + def __init__(self, name: str = "default") -> None: + self.name = name + self.open = True + + +def get_named_session(session: SessionRequestDep, session_b: SessionDefaultDep) -> Any: + assert session is session_b + named_session = NamedSession(name="named") + yield named_session, session_b + named_session.open = False + + +NamedSessionsDep = Annotated[Tuple[NamedSession, Session], Depends(get_named_session)] + + +def get_named_func_session(session: SessionFuncDep) -> Any: + named_session = NamedSession(name="named") + yield named_session, session + named_session.open = False + + +def get_named_regular_func_session(session: SessionFuncDep) -> Any: + named_session = NamedSession(name="named") + return named_session, session + + +BrokenSessionsDep = Annotated[ + Tuple[NamedSession, Session], Depends(get_named_func_session) +] +NamedSessionsFuncDep = Annotated[ + Tuple[NamedSession, Session], Depends(get_named_func_session, scope="function") +] + +RegularSessionsDep = Annotated[ + Tuple[NamedSession, Session], Depends(get_named_regular_func_session) +] + +app = FastAPI() + + +@app.get("/function-scope") +def function_scope(session: SessionFuncDep) -> Any: + def iter_data(): + yield json.dumps({"is_open": session.open}) + + return StreamingResponse(iter_data()) + + +@app.get("/request-scope") +def request_scope(session: SessionRequestDep) -> Any: + def iter_data(): + yield json.dumps({"is_open": session.open}) + + return StreamingResponse(iter_data()) + + +@app.get("/two-scopes") +def get_stream_session( + function_session: SessionFuncDep, request_session: SessionRequestDep +) -> Any: + def iter_data(): + yield json.dumps( + {"func_is_open": function_session.open, "req_is_open": request_session.open} + ) + + return StreamingResponse(iter_data()) + + +@app.get("/sub") +def get_sub(sessions: NamedSessionsDep) -> Any: + def iter_data(): + yield json.dumps( + {"named_session_open": sessions[0].open, "session_open": sessions[1].open} + ) + + return StreamingResponse(iter_data()) + + +@app.get("/named-function-scope") +def get_named_function_scope(sessions: NamedSessionsFuncDep) -> Any: + def iter_data(): + yield json.dumps( + {"named_session_open": sessions[0].open, "session_open": sessions[1].open} + ) + + return StreamingResponse(iter_data()) + + +@app.get("/regular-function-scope") +def get_regular_function_scope(sessions: RegularSessionsDep) -> Any: + def iter_data(): + yield json.dumps( + {"named_session_open": sessions[0].open, "session_open": sessions[1].open} + ) + + return StreamingResponse(iter_data()) + + +client = TestClient(app) + + +def test_function_scope() -> None: + response = client.get("/function-scope") + assert response.status_code == 200 + data = response.json() + assert data["is_open"] is False + + +def test_request_scope() -> None: + response = client.get("/request-scope") + assert response.status_code == 200 + data = response.json() + assert data["is_open"] is True + + +def test_two_scopes() -> None: + response = client.get("/two-scopes") + assert response.status_code == 200 + data = response.json() + assert data["func_is_open"] is False + assert data["req_is_open"] is True + + +def test_sub() -> None: + response = client.get("/sub") + assert response.status_code == 200 + data = response.json() + assert data["named_session_open"] is True + assert data["session_open"] is True + + +def test_broken_scope() -> None: + with pytest.raises( + FastAPIError, + match='The dependency "get_named_func_session" has a scope of "request", it cannot depend on dependencies with scope "function"', + ): + + @app.get("/broken-scope") + def get_broken(sessions: BrokenSessionsDep) -> Any: # pragma: no cover + pass + + +def test_named_function_scope() -> None: + response = client.get("/named-function-scope") + assert response.status_code == 200 + data = response.json() + assert data["named_session_open"] is False + assert data["session_open"] is False + + +def test_regular_function_scope() -> None: + response = client.get("/regular-function-scope") + assert response.status_code == 200 + data = response.json() + assert data["named_session_open"] is True + assert data["session_open"] is False diff --git a/tests/test_dependency_yield_scope_websockets.py b/tests/test_dependency_yield_scope_websockets.py new file mode 100644 index 000000000..52a30ae7a --- /dev/null +++ b/tests/test_dependency_yield_scope_websockets.py @@ -0,0 +1,201 @@ +from contextvars import ContextVar +from typing import Any, Dict, Tuple + +import pytest +from fastapi import Depends, FastAPI, WebSocket +from fastapi.exceptions import FastAPIError +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +global_context: ContextVar[Dict[str, Any]] = ContextVar("global_context", default={}) # noqa: B039 + + +class Session: + def __init__(self) -> None: + self.open = True + + +async def dep_session() -> Any: + s = Session() + yield s + s.open = False + global_state = global_context.get() + global_state["session_closed"] = True + + +SessionFuncDep = Annotated[Session, Depends(dep_session, scope="function")] +SessionRequestDep = Annotated[Session, Depends(dep_session, scope="request")] +SessionDefaultDep = Annotated[Session, Depends(dep_session)] + + +class NamedSession: + def __init__(self, name: str = "default") -> None: + self.name = name + self.open = True + + +def get_named_session(session: SessionRequestDep, session_b: SessionDefaultDep) -> Any: + assert session is session_b + named_session = NamedSession(name="named") + yield named_session, session_b + named_session.open = False + global_state = global_context.get() + global_state["named_session_closed"] = True + + +NamedSessionsDep = Annotated[Tuple[NamedSession, Session], Depends(get_named_session)] + + +def get_named_func_session(session: SessionFuncDep) -> Any: + named_session = NamedSession(name="named") + yield named_session, session + named_session.open = False + global_state = global_context.get() + global_state["named_func_session_closed"] = True + + +def get_named_regular_func_session(session: SessionFuncDep) -> Any: + named_session = NamedSession(name="named") + return named_session, session + + +BrokenSessionsDep = Annotated[ + Tuple[NamedSession, Session], Depends(get_named_func_session) +] +NamedSessionsFuncDep = Annotated[ + Tuple[NamedSession, Session], Depends(get_named_func_session, scope="function") +] + +RegularSessionsDep = Annotated[ + Tuple[NamedSession, Session], Depends(get_named_regular_func_session) +] + +app = FastAPI() + + +@app.websocket("/function-scope") +async def function_scope(websocket: WebSocket, session: SessionFuncDep) -> Any: + await websocket.accept() + await websocket.send_json({"is_open": session.open}) + + +@app.websocket("/request-scope") +async def request_scope(websocket: WebSocket, session: SessionRequestDep) -> Any: + await websocket.accept() + await websocket.send_json({"is_open": session.open}) + + +@app.websocket("/two-scopes") +async def get_stream_session( + websocket: WebSocket, + function_session: SessionFuncDep, + request_session: SessionRequestDep, +) -> Any: + await websocket.accept() + await websocket.send_json( + {"func_is_open": function_session.open, "req_is_open": request_session.open} + ) + + +@app.websocket("/sub") +async def get_sub(websocket: WebSocket, sessions: NamedSessionsDep) -> Any: + await websocket.accept() + await websocket.send_json( + {"named_session_open": sessions[0].open, "session_open": sessions[1].open} + ) + + +@app.websocket("/named-function-scope") +async def get_named_function_scope( + websocket: WebSocket, sessions: NamedSessionsFuncDep +) -> Any: + await websocket.accept() + await websocket.send_json( + {"named_session_open": sessions[0].open, "session_open": sessions[1].open} + ) + + +@app.websocket("/regular-function-scope") +async def get_regular_function_scope( + websocket: WebSocket, sessions: RegularSessionsDep +) -> Any: + await websocket.accept() + await websocket.send_json( + {"named_session_open": sessions[0].open, "session_open": sessions[1].open} + ) + + +client = TestClient(app) + + +def test_function_scope() -> None: + global_context.set({}) + global_state = global_context.get() + with client.websocket_connect("/function-scope") as websocket: + data = websocket.receive_json() + assert data["is_open"] is True + assert global_state["session_closed"] is True + + +def test_request_scope() -> None: + global_context.set({}) + global_state = global_context.get() + with client.websocket_connect("/request-scope") as websocket: + data = websocket.receive_json() + assert data["is_open"] is True + assert global_state["session_closed"] is True + + +def test_two_scopes() -> None: + global_context.set({}) + global_state = global_context.get() + with client.websocket_connect("/two-scopes") as websocket: + data = websocket.receive_json() + assert data["func_is_open"] is True + assert data["req_is_open"] is True + assert global_state["session_closed"] is True + + +def test_sub() -> None: + global_context.set({}) + global_state = global_context.get() + with client.websocket_connect("/sub") as websocket: + data = websocket.receive_json() + assert data["named_session_open"] is True + assert data["session_open"] is True + assert global_state["session_closed"] is True + assert global_state["named_session_closed"] is True + + +def test_broken_scope() -> None: + with pytest.raises( + FastAPIError, + match='The dependency "get_named_func_session" has a scope of "request", it cannot depend on dependencies with scope "function"', + ): + + @app.websocket("/broken-scope") + async def get_broken( + websocket: WebSocket, sessions: BrokenSessionsDep + ) -> Any: # pragma: no cover + pass + + +def test_named_function_scope() -> None: + global_context.set({}) + global_state = global_context.get() + with client.websocket_connect("/named-function-scope") as websocket: + data = websocket.receive_json() + assert data["named_session_open"] is True + assert data["session_open"] is True + assert global_state["session_closed"] is True + assert global_state["named_func_session_closed"] is True + + +def test_regular_function_scope() -> None: + global_context.set({}) + global_state = global_context.get() + with client.websocket_connect("/regular-function-scope") as websocket: + data = websocket.receive_json() + assert data["named_session_open"] is True + assert data["session_open"] is True + assert global_state["session_closed"] is True diff --git a/tests/test_get_model_definitions_formfeed_escape.py b/tests/test_get_model_definitions_formfeed_escape.py index f77195dc5..6601585ef 100644 --- a/tests/test_get_model_definitions_formfeed_escape.py +++ b/tests/test_get_model_definitions_formfeed_escape.py @@ -164,16 +164,16 @@ def test_model_description_escaped_with_formfeed(sort_reversed: bool): Test `get_model_definitions` with models passed in different order. """ + from fastapi._compat import v1 + all_fields = fastapi.openapi.utils.get_fields_from_routes(app.routes) - flat_models = fastapi._compat.get_flat_models_from_fields( - all_fields, known_models=set() - ) + flat_models = v1.get_flat_models_from_fields(all_fields, known_models=set()) model_name_map = pydantic.schema.get_model_name_map(flat_models) expected_address_description = "This is a public description of an Address\n" - models = fastapi._compat.get_model_definitions( + models = v1.get_model_definitions( flat_models=SortedTypeSet(flat_models, sort_reversed=sort_reversed), model_name_map=model_name_map, ) diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 0102f0f1a..33304827a 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -185,7 +185,15 @@ def test_openapi_schema(): "title": "Age", "anyOf": [ {"exclusiveMinimum": 0.0, "type": "number"}, - {"type": "string"}, + IsOneOf( + # pydantic < 2.12.0 + {"type": "string"}, + # pydantic >= 2.12.0 + { + "type": "string", + "pattern": r"^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$", + }, + ), ], } ) diff --git a/tests/test_no_schema_split.py b/tests/test_no_schema_split.py new file mode 100644 index 000000000..b0b5958c1 --- /dev/null +++ b/tests/test_no_schema_split.py @@ -0,0 +1,203 @@ +# Test with parts from, and to verify the report in: +# https://github.com/fastapi/fastapi/discussions/14177 +# Made an issue in: +# https://github.com/fastapi/fastapi/issues/14247 +from enum import Enum +from typing import List + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel, Field + +from tests.utils import pydantic_snapshot + + +class MessageEventType(str, Enum): + alpha = "alpha" + beta = "beta" + + +class MessageEvent(BaseModel): + event_type: MessageEventType = Field(default=MessageEventType.alpha) + output: str + + +class MessageOutput(BaseModel): + body: str = "" + events: List[MessageEvent] = [] + + +class Message(BaseModel): + input: str + output: MessageOutput + + +app = FastAPI(title="Minimal FastAPI App", version="1.0.0") + + +@app.post("/messages", response_model=Message) +async def create_message(input_message: str) -> Message: + return Message( + input=input_message, + output=MessageOutput(body=f"Processed: {input_message}"), + ) + + +client = TestClient(app) + + +def test_create_message(): + response = client.post("/messages", params={"input_message": "Hello"}) + assert response.status_code == 200, response.text + assert response.json() == { + "input": "Hello", + "output": {"body": "Processed: Hello", "events": []}, + } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "Minimal FastAPI App", "version": "1.0.0"}, + "paths": { + "/messages": { + "post": { + "summary": "Create Message", + "operationId": "create_message_messages_post", + "parameters": [ + { + "name": "input_message", + "in": "query", + "required": True, + "schema": {"type": "string", "title": "Input Message"}, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Message": { + "properties": { + "input": {"type": "string", "title": "Input"}, + "output": {"$ref": "#/components/schemas/MessageOutput"}, + }, + "type": "object", + "required": ["input", "output"], + "title": "Message", + }, + "MessageEvent": { + "properties": { + "event_type": pydantic_snapshot( + v2=snapshot( + { + "$ref": "#/components/schemas/MessageEventType", + "default": "alpha", + } + ), + v1=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/MessageEventType" + } + ], + "default": "alpha", + } + ), + ), + "output": {"type": "string", "title": "Output"}, + }, + "type": "object", + "required": ["output"], + "title": "MessageEvent", + }, + "MessageEventType": pydantic_snapshot( + v2=snapshot( + { + "type": "string", + "enum": ["alpha", "beta"], + "title": "MessageEventType", + } + ), + v1=snapshot( + { + "type": "string", + "enum": ["alpha", "beta"], + "title": "MessageEventType", + "description": "An enumeration.", + } + ), + ), + "MessageOutput": { + "properties": { + "body": {"type": "string", "title": "Body", "default": ""}, + "events": { + "items": {"$ref": "#/components/schemas/MessageEvent"}, + "type": "array", + "title": "Events", + "default": [], + }, + }, + "type": "object", + "title": "MessageOutput", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py index f7e045259..fa73620ea 100644 --- a/tests/test_openapi_separate_input_output_schemas.py +++ b/tests/test_openapi_separate_input_output_schemas.py @@ -2,6 +2,7 @@ from typing import List, Optional from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel from .utils import PYDANTIC_V2, needs_pydanticv2 @@ -135,217 +136,223 @@ def test_openapi_schema(): client = get_app_client() response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item-Output" + }, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item-Input" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item-Output" + } + } + }, + }, + "402": { + "description": "Payment Required", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item-Output" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/items-list/": { + "post": { + "summary": "Create Item List", + "operationId": "create_item_list_items_list__post", + "requestBody": { "content": { "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/Item-Output" + "$ref": "#/components/schemas/Item-Input" }, "type": "array", - "title": "Response Read Items Items Get", + "title": "Item", } } }, - } - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item-Input"} - } + "required": True, }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - "402": { - "description": "Payment Required", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, + } }, }, - "/items-list/": { - "post": { - "summary": "Create Item List", - "operationId": "create_item_list_items_list__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item-Input" - }, - "type": "array", - "title": "Item", - } + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", } }, - "required": True, + "type": "object", + "title": "HTTPValidationError", }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + "Item-Input": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "sub": { + "anyOf": [ + {"$ref": "#/components/schemas/SubItem-Input"}, + {"type": "null"}, + ] }, }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "Item-Output": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "sub": { + "anyOf": [ + {"$ref": "#/components/schemas/SubItem-Output"}, + {"type": "null"}, + ] + }, + }, + "type": "object", + "required": ["name", "description", "sub"], + "title": "Item", + }, + "SubItem-Input": { + "properties": { + "subname": {"type": "string", "title": "Subname"}, + "sub_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Sub Description", + }, + "tags": { + "items": {"type": "string"}, + "type": "array", + "title": "Tags", + "default": [], + }, + }, + "type": "object", + "required": ["subname"], + "title": "SubItem", + }, + "SubItem-Output": { + "properties": { + "subname": {"type": "string", "title": "Subname"}, + "sub_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Sub Description", + }, + "tags": { + "items": {"type": "string"}, + "type": "array", + "title": "Tags", + "default": [], + }, + }, + "type": "object", + "required": ["subname", "sub_description", "tags"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", }, } }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item-Input": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "sub": { - "anyOf": [ - {"$ref": "#/components/schemas/SubItem-Input"}, - {"type": "null"}, - ] - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "Item-Output": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "sub": { - "anyOf": [ - {"$ref": "#/components/schemas/SubItem-Output"}, - {"type": "null"}, - ] - }, - }, - "type": "object", - "required": ["name", "description", "sub"], - "title": "Item", - }, - "SubItem-Input": { - "properties": { - "subname": {"type": "string", "title": "Subname"}, - "sub_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Sub Description", - }, - "tags": { - "items": {"type": "string"}, - "type": "array", - "title": "Tags", - "default": [], - }, - }, - "type": "object", - "required": ["subname"], - "title": "SubItem", - }, - "SubItem-Output": { - "properties": { - "subname": {"type": "string", "title": "Subname"}, - "sub_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Sub Description", - }, - "tags": { - "items": {"type": "string"}, - "type": "array", - "title": "Tags", - "default": [], - }, - }, - "type": "object", - "required": ["subname", "sub_description", "tags"], - "title": "SubItem", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } + } + ) @needs_pydanticv2 diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py index bfc7bed09..baa172497 100644 --- a/tests/test_params_repr.py +++ b/tests/test_params_repr.py @@ -1,7 +1,7 @@ from typing import Any, List from dirty_equals import IsOneOf -from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query +from fastapi.params import Body, Cookie, Header, Param, Path, Query test_data: List[Any] = ["teststr", None, ..., 1, []] @@ -141,12 +141,3 @@ def test_body_repr_number(): def test_body_repr_list(): assert repr(Body([])) == "Body([])" - - -def test_depends_repr(): - assert repr(Depends()) == "Depends(NoneType)" - assert repr(Depends(get_user)) == "Depends(get_user)" - assert repr(Depends(use_cache=False)) == "Depends(NoneType, use_cache=False)" - assert ( - repr(Depends(get_user, use_cache=False)) == "Depends(get_user, use_cache=False)" - ) diff --git a/tests/test_pydantic_v1_v2_01.py b/tests/test_pydantic_v1_v2_01.py new file mode 100644 index 000000000..769e5fab6 --- /dev/null +++ b/tests/test_pydantic_v1_v2_01.py @@ -0,0 +1,475 @@ +import sys +from typing import Any, List, Union + +from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi._compat.v1 import BaseModel +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + + +class SubItem(BaseModel): + name: str + + +class Item(BaseModel): + title: str + size: int + description: Union[str, None] = None + sub: SubItem + multi: List[SubItem] = [] + + +app = FastAPI() + + +@app.post("/simple-model") +def handle_simple_model(data: SubItem) -> SubItem: + return data + + +@app.post("/simple-model-filter", response_model=SubItem) +def handle_simple_model_filter(data: SubItem) -> Any: + extended_data = data.dict() + extended_data.update({"secret_price": 42}) + return extended_data + + +@app.post("/item") +def handle_item(data: Item) -> Item: + return data + + +@app.post("/item-filter", response_model=Item) +def handle_item_filter(data: Item) -> Any: + extended_data = data.dict() + extended_data.update({"secret_data": "classified", "internal_id": 12345}) + extended_data["sub"].update({"internal_id": 67890}) + return extended_data + + +client = TestClient(app) + + +def test_old_simple_model(): + response = client.post( + "/simple-model", + json={"name": "Foo"}, + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo"} + + +def test_old_simple_model_validation_error(): + response = client.post( + "/simple-model", + json={"wrong_name": "Foo"}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "name"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_old_simple_model_filter(): + response = client.post( + "/simple-model-filter", + json={"name": "Foo"}, + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo"} + + +def test_item_model(): + response = client.post( + "/item", + json={ + "title": "Test Item", + "size": 100, + "description": "This is a test item", + "sub": {"name": "SubItem1"}, + "multi": [{"name": "Multi1"}, {"name": "Multi2"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "Test Item", + "size": 100, + "description": "This is a test item", + "sub": {"name": "SubItem1"}, + "multi": [{"name": "Multi1"}, {"name": "Multi2"}], + } + + +def test_item_model_minimal(): + response = client.post( + "/item", + json={"title": "Minimal Item", "size": 50, "sub": {"name": "SubMin"}}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "Minimal Item", + "size": 50, + "description": None, + "sub": {"name": "SubMin"}, + "multi": [], + } + + +def test_item_model_validation_errors(): + response = client.post( + "/item", + json={"title": "Missing fields"}, + ) + assert response.status_code == 422, response.text + error_detail = response.json()["detail"] + assert len(error_detail) == 2 + assert { + "loc": ["body", "size"], + "msg": "field required", + "type": "value_error.missing", + } in error_detail + assert { + "loc": ["body", "sub"], + "msg": "field required", + "type": "value_error.missing", + } in error_detail + + +def test_item_model_nested_validation_error(): + response = client.post( + "/item", + json={"title": "Test Item", "size": 100, "sub": {"wrong_field": "test"}}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "sub", "name"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_item_model_invalid_type(): + response = client.post( + "/item", + json={"title": "Test Item", "size": "not_a_number", "sub": {"name": "SubItem"}}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "size"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_item_filter(): + response = client.post( + "/item-filter", + json={ + "title": "Filtered Item", + "size": 200, + "description": "Test filtering", + "sub": {"name": "SubFiltered"}, + "multi": [], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == { + "title": "Filtered Item", + "size": 200, + "description": "Test filtering", + "sub": {"name": "SubFiltered"}, + "multi": [], + } + assert "secret_data" not in result + assert "internal_id" not in result + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/simple-model": { + "post": { + "summary": "Handle Simple Model", + "operationId": "handle_simple_model_simple_model_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/SubItem" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/SubItem"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubItem" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/simple-model-filter": { + "post": { + "summary": "Handle Simple Model Filter", + "operationId": "handle_simple_model_filter_simple_model_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/SubItem" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/SubItem"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubItem" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item": { + "post": { + "summary": "Handle Item", + "operationId": "handle_item_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item-filter": { + "post": { + "summary": "Handle Item Filter", + "operationId": "handle_item_filter_item_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "title": {"type": "string", "title": "Title"}, + "size": {"type": "integer", "title": "Size"}, + "description": {"type": "string", "title": "Description"}, + "sub": {"$ref": "#/components/schemas/SubItem"}, + "multi": { + "items": {"$ref": "#/components/schemas/SubItem"}, + "type": "array", + "title": "Multi", + "default": [], + }, + }, + "type": "object", + "required": ["title", "size", "sub"], + "title": "Item", + }, + "SubItem": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_pydantic_v1_v2_list.py b/tests/test_pydantic_v1_v2_list.py new file mode 100644 index 000000000..64f3dd344 --- /dev/null +++ b/tests/test_pydantic_v1_v2_list.py @@ -0,0 +1,701 @@ +import sys +from typing import Any, List, Union + +from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi._compat.v1 import BaseModel +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + + +class SubItem(BaseModel): + name: str + + +class Item(BaseModel): + title: str + size: int + description: Union[str, None] = None + sub: SubItem + multi: List[SubItem] = [] + + +app = FastAPI() + + +@app.post("/item") +def handle_item(data: Item) -> List[Item]: + return [data, data] + + +@app.post("/item-filter", response_model=List[Item]) +def handle_item_filter(data: Item) -> Any: + extended_data = data.dict() + extended_data.update({"secret_data": "classified", "internal_id": 12345}) + extended_data["sub"].update({"internal_id": 67890}) + return [extended_data, extended_data] + + +@app.post("/item-list") +def handle_item_list(data: List[Item]) -> Item: + if data: + return data[0] + return Item(title="", size=0, sub=SubItem(name="")) + + +@app.post("/item-list-filter", response_model=Item) +def handle_item_list_filter(data: List[Item]) -> Any: + if data: + extended_data = data[0].dict() + extended_data.update({"secret_data": "classified", "internal_id": 12345}) + extended_data["sub"].update({"internal_id": 67890}) + return extended_data + return Item(title="", size=0, sub=SubItem(name="")) + + +@app.post("/item-list-to-list") +def handle_item_list_to_list(data: List[Item]) -> List[Item]: + return data + + +@app.post("/item-list-to-list-filter", response_model=List[Item]) +def handle_item_list_to_list_filter(data: List[Item]) -> Any: + if data: + extended_data = data[0].dict() + extended_data.update({"secret_data": "classified", "internal_id": 12345}) + extended_data["sub"].update({"internal_id": 67890}) + return [extended_data, extended_data] + return [] + + +client = TestClient(app) + + +def test_item_to_list(): + response = client.post( + "/item", + json={ + "title": "Test Item", + "size": 100, + "description": "This is a test item", + "sub": {"name": "SubItem1"}, + "multi": [{"name": "Multi1"}, {"name": "Multi2"}], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert isinstance(result, list) + assert len(result) == 2 + for item in result: + assert item == { + "title": "Test Item", + "size": 100, + "description": "This is a test item", + "sub": {"name": "SubItem1"}, + "multi": [{"name": "Multi1"}, {"name": "Multi2"}], + } + + +def test_item_to_list_filter(): + response = client.post( + "/item-filter", + json={ + "title": "Filtered Item", + "size": 200, + "description": "Test filtering", + "sub": {"name": "SubFiltered"}, + "multi": [], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert isinstance(result, list) + assert len(result) == 2 + for item in result: + assert item == { + "title": "Filtered Item", + "size": 200, + "description": "Test filtering", + "sub": {"name": "SubFiltered"}, + "multi": [], + } + # Verify secret fields are filtered out + assert "secret_data" not in item + assert "internal_id" not in item + assert "internal_id" not in item["sub"] + + +def test_list_to_item(): + response = client.post( + "/item-list", + json=[ + {"title": "First Item", "size": 50, "sub": {"name": "First Sub"}}, + {"title": "Second Item", "size": 75, "sub": {"name": "Second Sub"}}, + ], + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "First Item", + "size": 50, + "description": None, + "sub": {"name": "First Sub"}, + "multi": [], + } + + +def test_list_to_item_empty(): + response = client.post( + "/item-list", + json=[], + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "", + "size": 0, + "description": None, + "sub": {"name": ""}, + "multi": [], + } + + +def test_list_to_item_filter(): + response = client.post( + "/item-list-filter", + json=[ + { + "title": "First Item", + "size": 100, + "sub": {"name": "First Sub"}, + "multi": [{"name": "Multi1"}], + }, + {"title": "Second Item", "size": 200, "sub": {"name": "Second Sub"}}, + ], + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == { + "title": "First Item", + "size": 100, + "description": None, + "sub": {"name": "First Sub"}, + "multi": [{"name": "Multi1"}], + } + # Verify secret fields are filtered out + assert "secret_data" not in result + assert "internal_id" not in result + + +def test_list_to_item_filter_no_data(): + response = client.post("/item-list-filter", json=[]) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "", + "size": 0, + "description": None, + "sub": {"name": ""}, + "multi": [], + } + + +def test_list_to_list(): + input_items = [ + {"title": "Item 1", "size": 10, "sub": {"name": "Sub1"}}, + { + "title": "Item 2", + "size": 20, + "description": "Second item", + "sub": {"name": "Sub2"}, + "multi": [{"name": "M1"}, {"name": "M2"}], + }, + {"title": "Item 3", "size": 30, "sub": {"name": "Sub3"}}, + ] + response = client.post( + "/item-list-to-list", + json=input_items, + ) + assert response.status_code == 200, response.text + result = response.json() + assert isinstance(result, list) + assert len(result) == 3 + assert result[0] == { + "title": "Item 1", + "size": 10, + "description": None, + "sub": {"name": "Sub1"}, + "multi": [], + } + assert result[1] == { + "title": "Item 2", + "size": 20, + "description": "Second item", + "sub": {"name": "Sub2"}, + "multi": [{"name": "M1"}, {"name": "M2"}], + } + assert result[2] == { + "title": "Item 3", + "size": 30, + "description": None, + "sub": {"name": "Sub3"}, + "multi": [], + } + + +def test_list_to_list_filter(): + response = client.post( + "/item-list-to-list-filter", + json=[{"title": "Item 1", "size": 100, "sub": {"name": "Sub1"}}], + ) + assert response.status_code == 200, response.text + result = response.json() + assert isinstance(result, list) + assert len(result) == 2 + for item in result: + assert item == { + "title": "Item 1", + "size": 100, + "description": None, + "sub": {"name": "Sub1"}, + "multi": [], + } + # Verify secret fields are filtered out + assert "secret_data" not in item + assert "internal_id" not in item + + +def test_list_to_list_filter_no_data(): + response = client.post( + "/item-list-to-list-filter", + json=[], + ) + assert response.status_code == 200, response.text + assert response.json() == [] + + +def test_list_validation_error(): + response = client.post( + "/item-list", + json=[ + {"title": "Valid Item", "size": 100, "sub": {"name": "Sub1"}}, + { + "title": "Invalid Item" + # Missing required fields: size and sub + }, + ], + ) + assert response.status_code == 422, response.text + error_detail = response.json()["detail"] + assert len(error_detail) == 2 + assert { + "loc": ["body", 1, "size"], + "msg": "field required", + "type": "value_error.missing", + } in error_detail + assert { + "loc": ["body", 1, "sub"], + "msg": "field required", + "type": "value_error.missing", + } in error_detail + + +def test_list_nested_validation_error(): + response = client.post( + "/item-list", + json=[ + {"title": "Item with bad sub", "size": 100, "sub": {"wrong_field": "value"}} + ], + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", 0, "sub", "name"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_list_type_validation_error(): + response = client.post( + "/item-list", + json=[{"title": "Item", "size": "not_a_number", "sub": {"name": "Sub"}}], + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", 0, "size"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_invalid_list_structure(): + response = client.post( + "/item-list", + json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid list", + "type": "type_error.list", + } + ] + } + ) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/item": { + "post": { + "summary": "Handle Item", + "operationId": "handle_item_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle Item Item Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item-filter": { + "post": { + "summary": "Handle Item Filter", + "operationId": "handle_item_filter_item_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle Item Filter Item Filter Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item-list": { + "post": { + "summary": "Handle Item List", + "operationId": "handle_item_list_item_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item-list-filter": { + "post": { + "summary": "Handle Item List Filter", + "operationId": "handle_item_list_filter_item_list_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item-list-to-list": { + "post": { + "summary": "Handle Item List To List", + "operationId": "handle_item_list_to_list_item_list_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle Item List To List Item List To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/item-list-to-list-filter": { + "post": { + "summary": "Handle Item List To List Filter", + "operationId": "handle_item_list_to_list_filter_item_list_to_list_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle Item List To List Filter Item List To List Filter Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "title": {"type": "string", "title": "Title"}, + "size": {"type": "integer", "title": "Size"}, + "description": {"type": "string", "title": "Description"}, + "sub": {"$ref": "#/components/schemas/SubItem"}, + "multi": { + "items": {"$ref": "#/components/schemas/SubItem"}, + "type": "array", + "title": "Multi", + "default": [], + }, + }, + "type": "object", + "required": ["title", "size", "sub"], + "title": "Item", + }, + "SubItem": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_pydantic_v1_v2_mixed.py b/tests/test_pydantic_v1_v2_mixed.py new file mode 100644 index 000000000..54d408827 --- /dev/null +++ b/tests/test_pydantic_v1_v2_mixed.py @@ -0,0 +1,1499 @@ +import sys +from typing import Any, List, Union + +from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi._compat.v1 import BaseModel +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel as NewBaseModel + + +class SubItem(BaseModel): + name: str + + +class Item(BaseModel): + title: str + size: int + description: Union[str, None] = None + sub: SubItem + multi: List[SubItem] = [] + + +class NewSubItem(NewBaseModel): + new_sub_name: str + + +class NewItem(NewBaseModel): + new_title: str + new_size: int + new_description: Union[str, None] = None + new_sub: NewSubItem + new_multi: List[NewSubItem] = [] + + +app = FastAPI() + + +@app.post("/v1-to-v2/item") +def handle_v1_item_to_v2(data: Item) -> NewItem: + return NewItem( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=NewSubItem(new_sub_name=data.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], + ) + + +@app.post("/v1-to-v2/item-filter", response_model=NewItem) +def handle_v1_item_to_v2_filter(data: Item) -> Any: + result = { + "new_title": data.title, + "new_size": data.size, + "new_description": data.description, + "new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"}, + "new_multi": [ + {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi + ], + "secret": "hidden_v1_to_v2", + } + return result + + +@app.post("/v2-to-v1/item") +def handle_v2_item_to_v1(data: NewItem) -> Item: + return Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=SubItem(name=data.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], + ) + + +@app.post("/v2-to-v1/item-filter", response_model=Item) +def handle_v2_item_to_v1_filter(data: NewItem) -> Any: + result = { + "title": data.new_title, + "size": data.new_size, + "description": data.new_description, + "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, + "multi": [ + {"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi + ], + "secret": "hidden_v2_to_v1", + } + return result + + +@app.post("/v1-to-v2/item-to-list") +def handle_v1_item_to_v2_list(data: Item) -> List[NewItem]: + converted = NewItem( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=NewSubItem(new_sub_name=data.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], + ) + return [converted, converted] + + +@app.post("/v1-to-v2/list-to-list") +def handle_v1_list_to_v2_list(data: List[Item]) -> List[NewItem]: + result = [] + for item in data: + result.append( + NewItem( + new_title=item.title, + new_size=item.size, + new_description=item.description, + new_sub=NewSubItem(new_sub_name=item.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], + ) + ) + return result + + +@app.post("/v1-to-v2/list-to-list-filter", response_model=List[NewItem]) +def handle_v1_list_to_v2_list_filter(data: List[Item]) -> Any: + result = [] + for item in data: + converted = { + "new_title": item.title, + "new_size": item.size, + "new_description": item.description, + "new_sub": {"new_sub_name": item.sub.name, "new_sub_secret": "sub_hidden"}, + "new_multi": [ + {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} + for s in item.multi + ], + "secret": "hidden_v2_to_v1", + } + result.append(converted) + return result + + +@app.post("/v1-to-v2/list-to-item") +def handle_v1_list_to_v2_item(data: List[Item]) -> NewItem: + if data: + item = data[0] + return NewItem( + new_title=item.title, + new_size=item.size, + new_description=item.description, + new_sub=NewSubItem(new_sub_name=item.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], + ) + return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name="")) + + +@app.post("/v2-to-v1/item-to-list") +def handle_v2_item_to_v1_list(data: NewItem) -> List[Item]: + converted = Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=SubItem(name=data.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], + ) + return [converted, converted] + + +@app.post("/v2-to-v1/list-to-list") +def handle_v2_list_to_v1_list(data: List[NewItem]) -> List[Item]: + result = [] + for item in data: + result.append( + Item( + title=item.new_title, + size=item.new_size, + description=item.new_description, + sub=SubItem(name=item.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], + ) + ) + return result + + +@app.post("/v2-to-v1/list-to-list-filter", response_model=List[Item]) +def handle_v2_list_to_v1_list_filter(data: List[NewItem]) -> Any: + result = [] + for item in data: + converted = { + "title": item.new_title, + "size": item.new_size, + "description": item.new_description, + "sub": {"name": item.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, + "multi": [ + {"name": s.new_sub_name, "sub_secret": "sub_hidden"} + for s in item.new_multi + ], + "secret": "hidden_v2_to_v1", + } + result.append(converted) + return result + + +@app.post("/v2-to-v1/list-to-item") +def handle_v2_list_to_v1_item(data: List[NewItem]) -> Item: + if data: + item = data[0] + return Item( + title=item.new_title, + size=item.new_size, + description=item.new_description, + sub=SubItem(name=item.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], + ) + return Item(title="", size=0, sub=SubItem(name="")) + + +client = TestClient(app) + + +def test_v1_to_v2_item(): + response = client.post( + "/v1-to-v2/item", + json={ + "title": "Old Item", + "size": 100, + "description": "V1 description", + "sub": {"name": "V1 Sub"}, + "multi": [{"name": "M1"}, {"name": "M2"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "new_title": "Old Item", + "new_size": 100, + "new_description": "V1 description", + "new_sub": {"new_sub_name": "V1 Sub"}, + "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], + } + + +def test_v1_to_v2_item_minimal(): + response = client.post( + "/v1-to-v2/item", + json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "new_title": "Minimal", + "new_size": 50, + "new_description": None, + "new_sub": {"new_sub_name": "MinSub"}, + "new_multi": [], + } + + +def test_v1_to_v2_item_filter(): + response = client.post( + "/v1-to-v2/item-filter", + json={ + "title": "Filtered Item", + "size": 50, + "sub": {"name": "Sub"}, + "multi": [{"name": "Multi1"}], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == snapshot( + { + "new_title": "Filtered Item", + "new_size": 50, + "new_description": None, + "new_sub": {"new_sub_name": "Sub"}, + "new_multi": [{"new_sub_name": "Multi1"}], + } + ) + # Verify secret fields are filtered out + assert "secret" not in result + assert "new_sub_secret" not in result["new_sub"] + assert "new_sub_secret" not in result["new_multi"][0] + + +def test_v2_to_v1_item(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "New Item", + "new_size": 200, + "new_description": "V2 description", + "new_sub": {"new_sub_name": "V2 Sub"}, + "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "New Item", + "size": 200, + "description": "V2 description", + "sub": {"name": "V2 Sub"}, + "multi": [{"name": "N1"}, {"name": "N2"}], + } + + +def test_v2_to_v1_item_minimal(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "MinimalNew", + "new_size": 75, + "new_sub": {"new_sub_name": "MinNewSub"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "MinimalNew", + "size": 75, + "description": None, + "sub": {"name": "MinNewSub"}, + "multi": [], + } + + +def test_v2_to_v1_item_filter(): + response = client.post( + "/v2-to-v1/item-filter", + json={ + "new_title": "Filtered New", + "new_size": 75, + "new_sub": {"new_sub_name": "NewSub"}, + "new_multi": [], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == snapshot( + { + "title": "Filtered New", + "size": 75, + "description": None, + "sub": {"name": "NewSub"}, + "multi": [], + } + ) + # Verify secret fields are filtered out + assert "secret" not in result + assert "sub_secret" not in result["sub"] + + +def test_v1_item_to_v2_list(): + response = client.post( + "/v1-to-v2/item-to-list", + json={ + "title": "Single to List", + "size": 150, + "description": "Convert to list", + "sub": {"name": "Sub1"}, + "multi": [], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == [ + { + "new_title": "Single to List", + "new_size": 150, + "new_description": "Convert to list", + "new_sub": {"new_sub_name": "Sub1"}, + "new_multi": [], + }, + { + "new_title": "Single to List", + "new_size": 150, + "new_description": "Convert to list", + "new_sub": {"new_sub_name": "Sub1"}, + "new_multi": [], + }, + ] + + +def test_v1_list_to_v2_list(): + response = client.post( + "/v1-to-v2/list-to-list", + json=[ + {"title": "Item1", "size": 10, "sub": {"name": "Sub1"}}, + { + "title": "Item2", + "size": 20, + "description": "Second item", + "sub": {"name": "Sub2"}, + "multi": [{"name": "M1"}, {"name": "M2"}], + }, + {"title": "Item3", "size": 30, "sub": {"name": "Sub3"}}, + ], + ) + assert response.status_code == 200, response.text + assert response.json() == [ + { + "new_title": "Item1", + "new_size": 10, + "new_description": None, + "new_sub": {"new_sub_name": "Sub1"}, + "new_multi": [], + }, + { + "new_title": "Item2", + "new_size": 20, + "new_description": "Second item", + "new_sub": {"new_sub_name": "Sub2"}, + "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], + }, + { + "new_title": "Item3", + "new_size": 30, + "new_description": None, + "new_sub": {"new_sub_name": "Sub3"}, + "new_multi": [], + }, + ] + + +def test_v1_list_to_v2_list_filter(): + response = client.post( + "/v1-to-v2/list-to-list-filter", + json=[{"title": "FilterMe", "size": 30, "sub": {"name": "SubF"}}], + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == snapshot( + [ + { + "new_title": "FilterMe", + "new_size": 30, + "new_description": None, + "new_sub": {"new_sub_name": "SubF"}, + "new_multi": [], + } + ] + ) + # Verify secret fields are filtered out + assert "secret" not in result[0] + assert "new_sub_secret" not in result[0]["new_sub"] + + +def test_v1_list_to_v2_item(): + response = client.post( + "/v1-to-v2/list-to-item", + json=[ + {"title": "First", "size": 100, "sub": {"name": "FirstSub"}}, + {"title": "Second", "size": 200, "sub": {"name": "SecondSub"}}, + ], + ) + assert response.status_code == 200, response.text + assert response.json() == { + "new_title": "First", + "new_size": 100, + "new_description": None, + "new_sub": {"new_sub_name": "FirstSub"}, + "new_multi": [], + } + + +def test_v1_list_to_v2_item_empty(): + response = client.post("/v1-to-v2/list-to-item", json=[]) + assert response.status_code == 200, response.text + assert response.json() == { + "new_title": "", + "new_size": 0, + "new_description": None, + "new_sub": {"new_sub_name": ""}, + "new_multi": [], + } + + +def test_v2_item_to_v1_list(): + response = client.post( + "/v2-to-v1/item-to-list", + json={ + "new_title": "Single New", + "new_size": 250, + "new_description": "New to list", + "new_sub": {"new_sub_name": "NewSub"}, + "new_multi": [], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [ + { + "title": "Single New", + "size": 250, + "description": "New to list", + "sub": {"name": "NewSub"}, + "multi": [], + }, + { + "title": "Single New", + "size": 250, + "description": "New to list", + "sub": {"name": "NewSub"}, + "multi": [], + }, + ] + + +def test_v2_list_to_v1_list(): + response = client.post( + "/v2-to-v1/list-to-list", + json=[ + {"new_title": "New1", "new_size": 15, "new_sub": {"new_sub_name": "NS1"}}, + { + "new_title": "New2", + "new_size": 25, + "new_description": "Second new", + "new_sub": {"new_sub_name": "NS2"}, + "new_multi": [{"new_sub_name": "NM1"}], + }, + ], + ) + assert response.status_code == 200, response.text + assert response.json() == [ + { + "title": "New1", + "size": 15, + "description": None, + "sub": {"name": "NS1"}, + "multi": [], + }, + { + "title": "New2", + "size": 25, + "description": "Second new", + "sub": {"name": "NS2"}, + "multi": [{"name": "NM1"}], + }, + ] + + +def test_v2_list_to_v1_list_filter(): + response = client.post( + "/v2-to-v1/list-to-list-filter", + json=[ + { + "new_title": "FilterNew", + "new_size": 35, + "new_sub": {"new_sub_name": "NSF"}, + } + ], + ) + assert response.status_code == 200, response.text + result = response.json() + assert result == snapshot( + [ + { + "title": "FilterNew", + "size": 35, + "description": None, + "sub": {"name": "NSF"}, + "multi": [], + } + ] + ) + # Verify secret fields are filtered out + assert "secret" not in result[0] + assert "sub_secret" not in result[0]["sub"] + + +def test_v2_list_to_v1_item(): + response = client.post( + "/v2-to-v1/list-to-item", + json=[ + { + "new_title": "FirstNew", + "new_size": 300, + "new_sub": {"new_sub_name": "FNS"}, + }, + { + "new_title": "SecondNew", + "new_size": 400, + "new_sub": {"new_sub_name": "SNS"}, + }, + ], + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "FirstNew", + "size": 300, + "description": None, + "sub": {"name": "FNS"}, + "multi": [], + } + + +def test_v2_list_to_v1_item_empty(): + response = client.post("/v2-to-v1/list-to-item", json=[]) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "", + "size": 0, + "description": None, + "sub": {"name": ""}, + "multi": [], + } + + +def test_v1_to_v2_validation_error(): + response = client.post("/v1-to-v2/item", json={"title": "Missing fields"}) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "size"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "sub"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_v1_to_v2_nested_validation_error(): + response = client.post( + "/v1-to-v2/item", + json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "sub", "name"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_v1_to_v2_type_validation_error(): + response = client.post( + "/v1-to-v2/item", + json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "size"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_v2_to_v1_validation_error(): + response = client.post( + "/v2-to-v1/item", + json={"new_title": "Missing fields"}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": pydantic_snapshot( + v2=snapshot( + [ + { + "type": "missing", + "loc": ["body", "new_size"], + "msg": "Field required", + "input": {"new_title": "Missing fields"}, + }, + { + "type": "missing", + "loc": ["body", "new_sub"], + "msg": "Field required", + "input": {"new_title": "Missing fields"}, + }, + ] + ), + v1=snapshot( + [ + { + "loc": ["body", "new_size"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "new_sub"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + ), + ) + } + ) + + +def test_v2_to_v1_nested_validation_error(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "Bad sub", + "new_size": 200, + "new_sub": {"wrong_field": "value"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + pydantic_snapshot( + v2=snapshot( + { + "type": "missing", + "loc": ["body", "new_sub", "new_sub_name"], + "msg": "Field required", + "input": {"wrong_field": "value"}, + } + ), + v1=snapshot( + { + "loc": ["body", "new_sub", "new_sub_name"], + "msg": "field required", + "type": "value_error.missing", + } + ), + ) + ] + } + ) + + +def test_v1_list_validation_error(): + response = client.post( + "/v1-to-v2/list-to-list", + json=[ + {"title": "Valid", "size": 10, "sub": {"name": "S"}}, + {"title": "Invalid"}, + ], + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", 1, "size"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", 1, "sub"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_v2_list_validation_error(): + response = client.post( + "/v2-to-v1/list-to-list", + json=[ + {"new_title": "Valid", "new_size": 10, "new_sub": {"new_sub_name": "NS"}}, + {"new_title": "Invalid"}, + ], + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": pydantic_snapshot( + v2=snapshot( + [ + { + "type": "missing", + "loc": ["body", 1, "new_size"], + "msg": "Field required", + "input": {"new_title": "Invalid"}, + }, + { + "type": "missing", + "loc": ["body", 1, "new_sub"], + "msg": "Field required", + "input": {"new_title": "Invalid"}, + }, + ] + ), + v1=snapshot( + [ + { + "loc": ["body", 1, "new_size"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", 1, "new_sub"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + ), + ) + } + ) + + +def test_invalid_list_structure_v1(): + response = client.post( + "/v1-to-v2/list-to-list", + json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}}, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid list", + "type": "type_error.list", + } + ] + } + ) + + +def test_invalid_list_structure_v2(): + response = client.post( + "/v2-to-v1/list-to-list", + json={ + "new_title": "Not a list", + "new_size": 100, + "new_sub": {"new_sub_name": "Sub"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": pydantic_snapshot( + v2=snapshot( + [ + { + "type": "list_type", + "loc": ["body"], + "msg": "Input should be a valid list", + "input": { + "new_title": "Not a list", + "new_size": 100, + "new_sub": {"new_sub_name": "Sub"}, + }, + } + ] + ), + v1=snapshot( + [ + { + "loc": ["body"], + "msg": "value is not a valid list", + "type": "type_error.list", + } + ] + ), + ) + } + ) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/v1-to-v2/item": { + "post": { + "summary": "Handle V1 Item To V2", + "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewItem" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/item-filter": { + "post": { + "summary": "Handle V1 Item To V2 Filter", + "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewItem" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item": { + "post": { + "summary": "Handle V2 Item To V1", + "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/NewItem"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item-filter": { + "post": { + "summary": "Handle V2 Item To V1 Filter", + "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/NewItem"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/item-to-list": { + "post": { + "summary": "Handle V1 Item To V2 List", + "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NewItem" + }, + "type": "array", + "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/list-to-list": { + "post": { + "summary": "Handle V1 List To V2 List", + "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NewItem" + }, + "type": "array", + "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/list-to-list-filter": { + "post": { + "summary": "Handle V1 List To V2 List Filter", + "operationId": "handle_v1_list_to_v2_list_filter_v1_to_v2_list_to_list_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NewItem" + }, + "type": "array", + "title": "Response Handle V1 List To V2 List Filter V1 To V2 List To List Filter Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/list-to-item": { + "post": { + "summary": "Handle V1 List To V2 Item", + "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewItem" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item-to-list": { + "post": { + "summary": "Handle V2 Item To V1 List", + "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/NewItem"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/list-to-list": { + "post": { + "summary": "Handle V2 List To V1 List", + "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NewItem" + }, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/list-to-list-filter": { + "post": { + "summary": "Handle V2 List To V1 List Filter", + "operationId": "handle_v2_list_to_v1_list_filter_v2_to_v1_list_to_list_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NewItem" + }, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item" + }, + "type": "array", + "title": "Response Handle V2 List To V1 List Filter V2 To V1 List To List Filter Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/list-to-item": { + "post": { + "summary": "Handle V2 List To V1 Item", + "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NewItem" + }, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "title": {"type": "string", "title": "Title"}, + "size": {"type": "integer", "title": "Size"}, + "description": {"type": "string", "title": "Description"}, + "sub": {"$ref": "#/components/schemas/SubItem"}, + "multi": { + "items": {"$ref": "#/components/schemas/SubItem"}, + "type": "array", + "title": "Multi", + "default": [], + }, + }, + "type": "object", + "required": ["title", "size", "sub"], + "title": "Item", + }, + "NewItem": { + "properties": { + "new_title": {"type": "string", "title": "New Title"}, + "new_size": {"type": "integer", "title": "New Size"}, + "new_description": pydantic_snapshot( + v2=snapshot( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "New Description", + } + ), + v1=snapshot( + {"type": "string", "title": "New Description"} + ), + ), + "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, + "new_multi": { + "items": {"$ref": "#/components/schemas/NewSubItem"}, + "type": "array", + "title": "New Multi", + "default": [], + }, + }, + "type": "object", + "required": ["new_title", "new_size", "new_sub"], + "title": "NewItem", + }, + "NewSubItem": { + "properties": { + "new_sub_name": {"type": "string", "title": "New Sub Name"} + }, + "type": "object", + "required": ["new_sub_name"], + "title": "NewSubItem", + }, + "SubItem": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_pydantic_v1_v2_multifile/__init__.py b/tests/test_pydantic_v1_v2_multifile/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_pydantic_v1_v2_multifile/main.py b/tests/test_pydantic_v1_v2_multifile/main.py new file mode 100644 index 000000000..8985cb7b4 --- /dev/null +++ b/tests/test_pydantic_v1_v2_multifile/main.py @@ -0,0 +1,142 @@ +from typing import List + +from fastapi import FastAPI + +from . import modelsv1, modelsv2, modelsv2b + +app = FastAPI() + + +@app.post("/v1-to-v2/item") +def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item: + return modelsv2.Item( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), + new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], + ) + + +@app.post("/v2-to-v1/item") +def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item: + return modelsv1.Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], + ) + + +@app.post("/v1-to-v2/item-to-list") +def handle_v1_item_to_v2_list(data: modelsv1.Item) -> List[modelsv2.Item]: + converted = modelsv2.Item( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), + new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], + ) + return [converted, converted] + + +@app.post("/v1-to-v2/list-to-list") +def handle_v1_list_to_v2_list(data: List[modelsv1.Item]) -> List[modelsv2.Item]: + result = [] + for item in data: + result.append( + modelsv2.Item( + new_title=item.title, + new_size=item.size, + new_description=item.description, + new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), + new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi], + ) + ) + return result + + +@app.post("/v1-to-v2/list-to-item") +def handle_v1_list_to_v2_item(data: List[modelsv1.Item]) -> modelsv2.Item: + if data: + item = data[0] + return modelsv2.Item( + new_title=item.title, + new_size=item.size, + new_description=item.description, + new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), + new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi], + ) + return modelsv2.Item( + new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="") + ) + + +@app.post("/v2-to-v1/item-to-list") +def handle_v2_item_to_v1_list(data: modelsv2.Item) -> List[modelsv1.Item]: + converted = modelsv1.Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], + ) + return [converted, converted] + + +@app.post("/v2-to-v1/list-to-list") +def handle_v2_list_to_v1_list(data: List[modelsv2.Item]) -> List[modelsv1.Item]: + result = [] + for item in data: + result.append( + modelsv1.Item( + title=item.new_title, + size=item.new_size, + description=item.new_description, + sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi], + ) + ) + return result + + +@app.post("/v2-to-v1/list-to-item") +def handle_v2_list_to_v1_item(data: List[modelsv2.Item]) -> modelsv1.Item: + if data: + item = data[0] + return modelsv1.Item( + title=item.new_title, + size=item.new_size, + description=item.new_description, + sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi], + ) + return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name="")) + + +@app.post("/v2-to-v1/same-name") +def handle_v2_same_name_to_v1( + item1: modelsv2.Item, item2: modelsv2b.Item +) -> modelsv1.Item: + return modelsv1.Item( + title=item1.new_title, + size=item2.dup_size, + description=item1.new_description, + sub=modelsv1.SubItem(name=item1.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi], + ) + + +@app.post("/v2-to-v1/list-of-items-to-list-of-items") +def handle_v2_items_in_list_to_v1_item_in_list( + data1: List[modelsv2.ItemInList], data2: List[modelsv2b.ItemInList] +) -> List[modelsv1.ItemInList]: + result = [] + item1 = data1[0] + item2 = data2[0] + result = [ + modelsv1.ItemInList(name1=item1.name2), + modelsv1.ItemInList(name1=item2.dup_name2), + ] + return result diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv1.py b/tests/test_pydantic_v1_v2_multifile/modelsv1.py new file mode 100644 index 000000000..889291a1a --- /dev/null +++ b/tests/test_pydantic_v1_v2_multifile/modelsv1.py @@ -0,0 +1,19 @@ +from typing import List, Union + +from fastapi._compat.v1 import BaseModel + + +class SubItem(BaseModel): + name: str + + +class Item(BaseModel): + title: str + size: int + description: Union[str, None] = None + sub: SubItem + multi: List[SubItem] = [] + + +class ItemInList(BaseModel): + name1: str diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2.py b/tests/test_pydantic_v1_v2_multifile/modelsv2.py new file mode 100644 index 000000000..2c8c6ea35 --- /dev/null +++ b/tests/test_pydantic_v1_v2_multifile/modelsv2.py @@ -0,0 +1,19 @@ +from typing import List, Union + +from pydantic import BaseModel + + +class SubItem(BaseModel): + new_sub_name: str + + +class Item(BaseModel): + new_title: str + new_size: int + new_description: Union[str, None] = None + new_sub: SubItem + new_multi: List[SubItem] = [] + + +class ItemInList(BaseModel): + name2: str diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2b.py b/tests/test_pydantic_v1_v2_multifile/modelsv2b.py new file mode 100644 index 000000000..dc0c06c66 --- /dev/null +++ b/tests/test_pydantic_v1_v2_multifile/modelsv2b.py @@ -0,0 +1,19 @@ +from typing import List, Union + +from pydantic import BaseModel + + +class SubItem(BaseModel): + dup_sub_name: str + + +class Item(BaseModel): + dup_title: str + dup_size: int + dup_description: Union[str, None] = None + dup_sub: SubItem + dup_multi: List[SubItem] = [] + + +class ItemInList(BaseModel): + dup_name2: str diff --git a/tests/test_pydantic_v1_v2_multifile/test_multifile.py b/tests/test_pydantic_v1_v2_multifile/test_multifile.py new file mode 100644 index 000000000..e66d102fb --- /dev/null +++ b/tests/test_pydantic_v1_v2_multifile/test_multifile.py @@ -0,0 +1,1226 @@ +import sys + +from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from .main import app + +client = TestClient(app) + + +def test_v1_to_v2_item(): + response = client.post( + "/v1-to-v2/item", + json={"title": "Test", "size": 10, "sub": {"name": "SubTest"}}, + ) + assert response.status_code == 200 + assert response.json() == { + "new_title": "Test", + "new_size": 10, + "new_description": None, + "new_sub": {"new_sub_name": "SubTest"}, + "new_multi": [], + } + + +def test_v2_to_v1_item(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "NewTest", + "new_size": 20, + "new_sub": {"new_sub_name": "NewSubTest"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "title": "NewTest", + "size": 20, + "description": None, + "sub": {"name": "NewSubTest"}, + "multi": [], + } + + +def test_v1_to_v2_item_to_list(): + response = client.post( + "/v1-to-v2/item-to-list", + json={"title": "ListTest", "size": 30, "sub": {"name": "SubListTest"}}, + ) + assert response.status_code == 200 + assert response.json() == [ + { + "new_title": "ListTest", + "new_size": 30, + "new_description": None, + "new_sub": {"new_sub_name": "SubListTest"}, + "new_multi": [], + }, + { + "new_title": "ListTest", + "new_size": 30, + "new_description": None, + "new_sub": {"new_sub_name": "SubListTest"}, + "new_multi": [], + }, + ] + + +def test_v1_to_v2_list_to_list(): + response = client.post( + "/v1-to-v2/list-to-list", + json=[ + {"title": "Item1", "size": 40, "sub": {"name": "Sub1"}}, + {"title": "Item2", "size": 50, "sub": {"name": "Sub2"}}, + ], + ) + assert response.status_code == 200 + assert response.json() == [ + { + "new_title": "Item1", + "new_size": 40, + "new_description": None, + "new_sub": {"new_sub_name": "Sub1"}, + "new_multi": [], + }, + { + "new_title": "Item2", + "new_size": 50, + "new_description": None, + "new_sub": {"new_sub_name": "Sub2"}, + "new_multi": [], + }, + ] + + +def test_v1_to_v2_list_to_item(): + response = client.post( + "/v1-to-v2/list-to-item", + json=[ + {"title": "FirstItem", "size": 60, "sub": {"name": "FirstSub"}}, + {"title": "SecondItem", "size": 70, "sub": {"name": "SecondSub"}}, + ], + ) + assert response.status_code == 200 + assert response.json() == { + "new_title": "FirstItem", + "new_size": 60, + "new_description": None, + "new_sub": {"new_sub_name": "FirstSub"}, + "new_multi": [], + } + + +def test_v2_to_v1_item_to_list(): + response = client.post( + "/v2-to-v1/item-to-list", + json={ + "new_title": "ListNew", + "new_size": 80, + "new_sub": {"new_sub_name": "SubListNew"}, + }, + ) + assert response.status_code == 200 + assert response.json() == [ + { + "title": "ListNew", + "size": 80, + "description": None, + "sub": {"name": "SubListNew"}, + "multi": [], + }, + { + "title": "ListNew", + "size": 80, + "description": None, + "sub": {"name": "SubListNew"}, + "multi": [], + }, + ] + + +def test_v2_to_v1_list_to_list(): + response = client.post( + "/v2-to-v1/list-to-list", + json=[ + { + "new_title": "New1", + "new_size": 90, + "new_sub": {"new_sub_name": "NewSub1"}, + }, + { + "new_title": "New2", + "new_size": 100, + "new_sub": {"new_sub_name": "NewSub2"}, + }, + ], + ) + assert response.status_code == 200 + assert response.json() == [ + { + "title": "New1", + "size": 90, + "description": None, + "sub": {"name": "NewSub1"}, + "multi": [], + }, + { + "title": "New2", + "size": 100, + "description": None, + "sub": {"name": "NewSub2"}, + "multi": [], + }, + ] + + +def test_v2_to_v1_list_to_item(): + response = client.post( + "/v2-to-v1/list-to-item", + json=[ + { + "new_title": "FirstNew", + "new_size": 110, + "new_sub": {"new_sub_name": "FirstNewSub"}, + }, + { + "new_title": "SecondNew", + "new_size": 120, + "new_sub": {"new_sub_name": "SecondNewSub"}, + }, + ], + ) + assert response.status_code == 200 + assert response.json() == { + "title": "FirstNew", + "size": 110, + "description": None, + "sub": {"name": "FirstNewSub"}, + "multi": [], + } + + +def test_v1_to_v2_list_to_item_empty(): + response = client.post("/v1-to-v2/list-to-item", json=[]) + assert response.status_code == 200 + assert response.json() == { + "new_title": "", + "new_size": 0, + "new_description": None, + "new_sub": {"new_sub_name": ""}, + "new_multi": [], + } + + +def test_v2_to_v1_list_to_item_empty(): + response = client.post("/v2-to-v1/list-to-item", json=[]) + assert response.status_code == 200 + assert response.json() == { + "title": "", + "size": 0, + "description": None, + "sub": {"name": ""}, + "multi": [], + } + + +def test_v2_same_name_to_v1(): + response = client.post( + "/v2-to-v1/same-name", + json={ + "item1": { + "new_title": "Title1", + "new_size": 100, + "new_description": "Description1", + "new_sub": {"new_sub_name": "Sub1"}, + "new_multi": [{"new_sub_name": "Multi1"}], + }, + "item2": { + "dup_title": "Title2", + "dup_size": 200, + "dup_description": "Description2", + "dup_sub": {"dup_sub_name": "Sub2"}, + "dup_multi": [ + {"dup_sub_name": "Multi2a"}, + {"dup_sub_name": "Multi2b"}, + ], + }, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "title": "Title1", + "size": 200, + "description": "Description1", + "sub": {"name": "Sub1"}, + "multi": [{"name": "Multi2a"}, {"name": "Multi2b"}], + } + + +def test_v2_items_in_list_to_v1_item_in_list(): + response = client.post( + "/v2-to-v1/list-of-items-to-list-of-items", + json={ + "data1": [{"name2": "Item1"}, {"name2": "Item2"}], + "data2": [{"dup_name2": "Item3"}, {"dup_name2": "Item4"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [ + {"name1": "Item1"}, + {"name1": "Item3"}, + ] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/v1-to-v2/item": { + "post": { + "summary": "Handle V1 Item To V2", + "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item": { + "post": { + "summary": "Handle V2 Item To V1", + "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + } + ), + v1=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + } + ), + ), + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/item-to-list": { + "post": { + "summary": "Handle V1 Item To V2 List", + "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + }, + "type": "array", + "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/list-to-list": { + "post": { + "summary": "Handle V1 List To V2 List", + "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + }, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + }, + "type": "array", + "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/list-to-item": { + "post": { + "summary": "Handle V1 List To V2 Item", + "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + }, + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item-to-list": { + "post": { + "summary": "Handle V2 Item To V1 List", + "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + } + ), + v1=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + } + ), + ), + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + }, + "type": "array", + "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/list-to-list": { + "post": { + "summary": "Handle V2 List To V1 List", + "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": pydantic_snapshot( + v2=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + } + ), + v1=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + } + ), + ), + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + }, + "type": "array", + "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/list-to-item": { + "post": { + "summary": "Handle V2 List To V1 Item", + "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": pydantic_snapshot( + v2=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + } + ), + v1=snapshot( + { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + } + ), + ), + "type": "array", + "title": "Data", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/same-name": { + "post": { + "summary": "Handle V2 Same Name To V1", + "operationId": "handle_v2_same_name_to_v1_v2_to_v1_same_name_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/list-of-items-to-list-of-items": { + "post": { + "summary": "Handle V2 Items In List To V1 Item In List", + "operationId": "handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList" + }, + "type": "array", + "title": "Response Handle V2 Items In List To V1 Item In List V2 To V1 List Of Items To List Of Items Post", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": pydantic_snapshot( + v1=snapshot( + { + "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": { + "properties": { + "data1": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList" + }, + "type": "array", + "title": "Data1", + }, + "data2": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList" + }, + "type": "array", + "title": "Data2", + }, + }, + "type": "object", + "required": ["data1", "data2"], + "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", + }, + "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": { + "properties": { + "item1": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" + }, + "item2": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item" + }, + }, + "type": "object", + "required": ["item1", "item2"], + "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": { + "properties": { + "title": {"type": "string", "title": "Title"}, + "size": {"type": "integer", "title": "Size"}, + "description": { + "type": "string", + "title": "Description", + }, + "sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" + }, + "multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" + }, + "type": "array", + "title": "Multi", + "default": [], + }, + }, + "type": "object", + "required": ["title", "size", "sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": { + "properties": { + "name1": {"type": "string", "title": "Name1"} + }, + "type": "object", + "required": ["name1"], + "title": "ItemInList", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": { + "properties": { + "name": {"type": "string", "title": "Name"} + }, + "type": "object", + "required": ["name"], + "title": "SubItem", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": { + "properties": { + "new_title": { + "type": "string", + "title": "New Title", + }, + "new_size": { + "type": "integer", + "title": "New Size", + }, + "new_description": { + "type": "string", + "title": "New Description", + }, + "new_sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" + }, + "new_multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" + }, + "type": "array", + "title": "New Multi", + "default": [], + }, + }, + "type": "object", + "required": ["new_title", "new_size", "new_sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": { + "properties": { + "name2": {"type": "string", "title": "Name2"} + }, + "type": "object", + "required": ["name2"], + "title": "ItemInList", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": { + "properties": { + "new_sub_name": { + "type": "string", + "title": "New Sub Name", + } + }, + "type": "object", + "required": ["new_sub_name"], + "title": "SubItem", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": { + "properties": { + "dup_title": { + "type": "string", + "title": "Dup Title", + }, + "dup_size": { + "type": "integer", + "title": "Dup Size", + }, + "dup_description": { + "type": "string", + "title": "Dup Description", + }, + "dup_sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" + }, + "dup_multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" + }, + "type": "array", + "title": "Dup Multi", + "default": [], + }, + }, + "type": "object", + "required": ["dup_title", "dup_size", "dup_sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": { + "properties": { + "dup_name2": { + "type": "string", + "title": "Dup Name2", + } + }, + "type": "object", + "required": ["dup_name2"], + "title": "ItemInList", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": { + "properties": { + "dup_sub_name": { + "type": "string", + "title": "Dup Sub Name", + } + }, + "type": "object", + "required": ["dup_sub_name"], + "title": "SubItem", + }, + } + ), + v2=snapshot( + { + "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": { + "properties": { + "data1": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList" + }, + "type": "array", + "title": "Data1", + }, + "data2": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList" + }, + "type": "array", + "title": "Data2", + }, + }, + "type": "object", + "required": ["data1", "data2"], + "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", + }, + "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": { + "properties": { + "item1": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + }, + "item2": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item" + }, + }, + "type": "object", + "required": ["item1", "item2"], + "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "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 new file mode 100644 index 000000000..d2d6f6635 --- /dev/null +++ b/tests/test_pydantic_v1_v2_noneable.py @@ -0,0 +1,766 @@ +import sys +from typing import Any, List, Union + +from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi._compat.v1 import BaseModel +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel as NewBaseModel + + +class SubItem(BaseModel): + name: str + + +class Item(BaseModel): + title: str + size: int + description: Union[str, None] = None + sub: SubItem + multi: List[SubItem] = [] + + +class NewSubItem(NewBaseModel): + new_sub_name: str + + +class NewItem(NewBaseModel): + new_title: str + new_size: int + new_description: Union[str, None] = None + new_sub: NewSubItem + new_multi: List[NewSubItem] = [] + + +app = FastAPI() + + +@app.post("/v1-to-v2/") +def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]: + if data.size < 0: + return None + return NewItem( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=NewSubItem(new_sub_name=data.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], + ) + + +@app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None]) +def handle_v1_item_to_v2_filter(data: Item) -> Any: + if data.size < 0: + return None + result = { + "new_title": data.title, + "new_size": data.size, + "new_description": data.description, + "new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"}, + "new_multi": [ + {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi + ], + "secret": "hidden_v1_to_v2", + } + return result + + +@app.post("/v2-to-v1/item") +def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]: + if data.new_size < 0: + return None + return Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=SubItem(name=data.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], + ) + + +@app.post("/v2-to-v1/item-filter", response_model=Union[Item, None]) +def handle_v2_item_to_v1_filter(data: NewItem) -> Any: + if data.new_size < 0: + return None + result = { + "title": data.new_title, + "size": data.new_size, + "description": data.new_description, + "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, + "multi": [ + {"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi + ], + "secret": "hidden_v2_to_v1", + } + return result + + +client = TestClient(app) + + +def test_v1_to_v2_item_success(): + response = client.post( + "/v1-to-v2/", + json={ + "title": "Old Item", + "size": 100, + "description": "V1 description", + "sub": {"name": "V1 Sub"}, + "multi": [{"name": "M1"}, {"name": "M2"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "new_title": "Old Item", + "new_size": 100, + "new_description": "V1 description", + "new_sub": {"new_sub_name": "V1 Sub"}, + "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], + } + + +def test_v1_to_v2_item_returns_none(): + response = client.post( + "/v1-to-v2/", + json={"title": "Invalid Item", "size": -10, "sub": {"name": "Sub"}}, + ) + assert response.status_code == 200, response.text + assert response.json() is None + + +def test_v1_to_v2_item_minimal(): + response = client.post( + "/v1-to-v2/", json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "new_title": "Minimal", + "new_size": 50, + "new_description": None, + "new_sub": {"new_sub_name": "MinSub"}, + "new_multi": [], + } + + +def test_v1_to_v2_item_filter_success(): + response = client.post( + "/v1-to-v2/item-filter", + json={ + "title": "Filtered Item", + "size": 50, + "sub": {"name": "Sub"}, + "multi": [{"name": "Multi1"}], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert result["new_title"] == "Filtered Item" + assert result["new_size"] == 50 + assert result["new_sub"]["new_sub_name"] == "Sub" + assert result["new_multi"][0]["new_sub_name"] == "Multi1" + # Verify secret fields are filtered out + assert "secret" not in result + assert "new_sub_secret" not in result["new_sub"] + assert "new_sub_secret" not in result["new_multi"][0] + + +def test_v1_to_v2_item_filter_returns_none(): + response = client.post( + "/v1-to-v2/item-filter", + json={"title": "Invalid", "size": -1, "sub": {"name": "Sub"}}, + ) + assert response.status_code == 200, response.text + assert response.json() is None + + +def test_v2_to_v1_item_success(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "New Item", + "new_size": 200, + "new_description": "V2 description", + "new_sub": {"new_sub_name": "V2 Sub"}, + "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "New Item", + "size": 200, + "description": "V2 description", + "sub": {"name": "V2 Sub"}, + "multi": [{"name": "N1"}, {"name": "N2"}], + } + + +def test_v2_to_v1_item_returns_none(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "Invalid New", + "new_size": -5, + "new_sub": {"new_sub_name": "NewSub"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() is None + + +def test_v2_to_v1_item_minimal(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "MinimalNew", + "new_size": 75, + "new_sub": {"new_sub_name": "MinNewSub"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "title": "MinimalNew", + "size": 75, + "description": None, + "sub": {"name": "MinNewSub"}, + "multi": [], + } + + +def test_v2_to_v1_item_filter_success(): + response = client.post( + "/v2-to-v1/item-filter", + json={ + "new_title": "Filtered New", + "new_size": 75, + "new_sub": {"new_sub_name": "NewSub"}, + "new_multi": [], + }, + ) + assert response.status_code == 200, response.text + result = response.json() + assert result["title"] == "Filtered New" + assert result["size"] == 75 + assert result["sub"]["name"] == "NewSub" + # Verify secret fields are filtered out + assert "secret" not in result + assert "sub_secret" not in result["sub"] + + +def test_v2_to_v1_item_filter_returns_none(): + response = client.post( + "/v2-to-v1/item-filter", + json={ + "new_title": "Invalid Filtered", + "new_size": -100, + "new_sub": {"new_sub_name": "Sub"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() is None + + +def test_v1_to_v2_validation_error(): + response = client.post("/v1-to-v2/", json={"title": "Missing fields"}) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "size"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "sub"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_v1_to_v2_nested_validation_error(): + response = client.post( + "/v1-to-v2/", + json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}}, + ) + assert response.status_code == 422, response.text + error_detail = response.json()["detail"] + assert len(error_detail) == 1 + assert error_detail[0]["loc"] == ["body", "sub", "name"] + + +def test_v1_to_v2_type_validation_error(): + response = client.post( + "/v1-to-v2/", + json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}}, + ) + assert response.status_code == 422, response.text + error_detail = response.json()["detail"] + assert len(error_detail) == 1 + assert error_detail[0]["loc"] == ["body", "size"] + + +def test_v2_to_v1_validation_error(): + response = client.post("/v2-to-v1/item", json={"new_title": "Missing fields"}) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": pydantic_snapshot( + v2=snapshot( + [ + { + "type": "missing", + "loc": ["body", "new_size"], + "msg": "Field required", + "input": {"new_title": "Missing fields"}, + }, + { + "type": "missing", + "loc": ["body", "new_sub"], + "msg": "Field required", + "input": {"new_title": "Missing fields"}, + }, + ] + ), + v1=snapshot( + [ + { + "loc": ["body", "new_size"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "new_sub"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + ), + ) + } + ) + + +def test_v2_to_v1_nested_validation_error(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "Bad sub", + "new_size": 200, + "new_sub": {"wrong_field": "value"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + pydantic_snapshot( + v2=snapshot( + { + "type": "missing", + "loc": ["body", "new_sub", "new_sub_name"], + "msg": "Field required", + "input": {"wrong_field": "value"}, + } + ), + v1=snapshot( + { + "loc": ["body", "new_sub", "new_sub_name"], + "msg": "field required", + "type": "value_error.missing", + } + ), + ) + ] + } + ) + + +def test_v2_to_v1_type_validation_error(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "Bad type", + "new_size": "not_a_number", + "new_sub": {"new_sub_name": "Sub"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + pydantic_snapshot( + v2=snapshot( + { + "type": "int_parsing", + "loc": ["body", "new_size"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "not_a_number", + } + ), + v1=snapshot( + { + "loc": ["body", "new_size"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ), + ) + ] + } + ) + + +def test_v1_to_v2_with_multi_items(): + response = client.post( + "/v1-to-v2/", + json={ + "title": "Complex Item", + "size": 300, + "description": "Item with multiple sub-items", + "sub": {"name": "Main Sub"}, + "multi": [{"name": "Sub1"}, {"name": "Sub2"}, {"name": "Sub3"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "new_title": "Complex Item", + "new_size": 300, + "new_description": "Item with multiple sub-items", + "new_sub": {"new_sub_name": "Main Sub"}, + "new_multi": [ + {"new_sub_name": "Sub1"}, + {"new_sub_name": "Sub2"}, + {"new_sub_name": "Sub3"}, + ], + } + ) + + +def test_v2_to_v1_with_multi_items(): + response = client.post( + "/v2-to-v1/item", + json={ + "new_title": "Complex New Item", + "new_size": 400, + "new_description": "New item with multiple sub-items", + "new_sub": {"new_sub_name": "Main New Sub"}, + "new_multi": [{"new_sub_name": "NewSub1"}, {"new_sub_name": "NewSub2"}], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "title": "Complex New Item", + "size": 400, + "description": "New item with multiple sub-items", + "sub": {"name": "Main New Sub"}, + "multi": [{"name": "NewSub1"}, {"name": "NewSub2"}], + } + ) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/v1-to-v2/": { + "post": { + "summary": "Handle V1 Item To V2", + "operationId": "handle_v1_item_to_v2_v1_to_v2__post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "anyOf": [ + { + "$ref": "#/components/schemas/NewItem" + }, + {"type": "null"}, + ], + "title": "Response Handle V1 Item To V2 V1 To V2 Post", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/NewItem"} + ), + ) + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v1-to-v2/item-filter": { + "post": { + "summary": "Handle V1 Item To V2 Filter", + "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/Item" + } + ], + "title": "Data", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/Item"} + ), + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": pydantic_snapshot( + v2=snapshot( + { + "anyOf": [ + { + "$ref": "#/components/schemas/NewItem" + }, + {"type": "null"}, + ], + "title": "Response Handle V1 Item To V2 Filter V1 To V2 Item Filter Post", + } + ), + v1=snapshot( + {"$ref": "#/components/schemas/NewItem"} + ), + ) + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item": { + "post": { + "summary": "Handle V2 Item To V1", + "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/NewItem"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/v2-to-v1/item-filter": { + "post": { + "summary": "Handle V2 Item To V1 Filter", + "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/NewItem"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "title": {"type": "string", "title": "Title"}, + "size": {"type": "integer", "title": "Size"}, + "description": {"type": "string", "title": "Description"}, + "sub": {"$ref": "#/components/schemas/SubItem"}, + "multi": { + "items": {"$ref": "#/components/schemas/SubItem"}, + "type": "array", + "title": "Multi", + "default": [], + }, + }, + "type": "object", + "required": ["title", "size", "sub"], + "title": "Item", + }, + "NewItem": { + "properties": { + "new_title": {"type": "string", "title": "New Title"}, + "new_size": {"type": "integer", "title": "New Size"}, + "new_description": pydantic_snapshot( + v2=snapshot( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "New Description", + } + ), + v1=snapshot( + {"type": "string", "title": "New Description"} + ), + ), + "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, + "new_multi": { + "items": {"$ref": "#/components/schemas/NewSubItem"}, + "type": "array", + "title": "New Multi", + "default": [], + }, + }, + "type": "object", + "required": ["new_title", "new_size", "new_sub"], + "title": "NewItem", + }, + "NewSubItem": { + "properties": { + "new_sub_name": {"type": "string", "title": "New Sub Name"} + }, + "type": "object", + "required": ["new_sub_name"], + "title": "NewSubItem", + }, + "SubItem": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 6948430a1..1745c69b6 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -7,6 +7,8 @@ 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 @@ -509,6 +511,26 @@ 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 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_top_level_security_scheme_in_openapi.py b/tests/test_top_level_security_scheme_in_openapi.py new file mode 100644 index 000000000..e2de31af5 --- /dev/null +++ b/tests/test_top_level_security_scheme_in_openapi.py @@ -0,0 +1,60 @@ +# Test security scheme at the top level, including OpenAPI +# Ref: https://github.com/fastapi/fastapi/discussions/14263 +# Ref: https://github.com/fastapi/fastapi/issues/14271 +from fastapi import Depends, FastAPI +from fastapi.security import HTTPBearer +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +app = FastAPI() + +bearer_scheme = HTTPBearer() + + +@app.get("/", dependencies=[Depends(bearer_scheme)]) +async def get_root(): + return {"message": "Hello, World!"} + + +client = TestClient(app) + + +def test_get_root(): + response = client.get("/", headers={"Authorization": "Bearer token"}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello, World!"} + + +def test_get_root_no_token(): + response = client.get("/") + assert response.status_code == 403, response.text + assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Get Root", + "operationId": "get_root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [{"HTTPBearer": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} + }, + } + ) diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008e.py b/tests/test_tutorial/test_dependencies/test_tutorial008e.py new file mode 100644 index 000000000..1ae9ab2cd --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial008e.py @@ -0,0 +1,27 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial008e", + "tutorial008e_an", + pytest.param("tutorial008e_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_get_users_me(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 200, response.text + assert response.json() == "Rick" diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py b/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py new file mode 100644 index 000000000..3075a05f5 --- /dev/null +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py @@ -0,0 +1,37 @@ +import sys +from typing import Any + +import pytest +from fastapi._compat import PYDANTIC_V2 + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + + +if not PYDANTIC_V2: + pytest.skip("This test is only for Pydantic v2", allow_module_level=True) + +import importlib + +import pytest + +from ...utils import needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial001_an", + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") + return mod + + +def test_model(mod: Any): + item = mod.Item(name="Foo", size=3.4) + assert item.dict() == {"name": "Foo", "description": None, "size": 3.4} diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py new file mode 100644 index 000000000..a402c663d --- /dev/null +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py @@ -0,0 +1,140 @@ +import sys + +import pytest +from fastapi._compat import PYDANTIC_V2 +from inline_snapshot import snapshot + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + + +if not PYDANTIC_V2: + pytest.skip("This test is only for Pydantic v2", allow_module_level=True) + +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial002_an", + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") + + c = TestClient(mod.app) + return c + + +def test_call(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "size": 3.4}) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": None, + "size": 3.4, + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "size": {"type": "number", "title": "Size"}, + }, + "type": "object", + "required": ["name", "size"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py new file mode 100644 index 000000000..03155c924 --- /dev/null +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py @@ -0,0 +1,154 @@ +import sys + +import pytest +from fastapi._compat import PYDANTIC_V2 +from inline_snapshot import snapshot + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +if not PYDANTIC_V2: + pytest.skip("This test is only for Pydantic v2", allow_module_level=True) + + +import importlib + +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial003_an", + pytest.param("tutorial003_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") + + c = TestClient(mod.app) + return c + + +def test_call(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "size": 3.4}) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": None, + "size": 3.4, + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemV2" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "size": {"type": "number", "title": "Size"}, + }, + "type": "object", + "required": ["name", "size"], + "title": "Item", + }, + "ItemV2": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "size": {"type": "number", "title": "Size"}, + }, + "type": "object", + "required": ["name", "size"], + "title": "ItemV2", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py new file mode 100644 index 000000000..d2e204dda --- /dev/null +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py @@ -0,0 +1,153 @@ +import sys + +import pytest +from fastapi._compat import PYDANTIC_V2 +from inline_snapshot import snapshot + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +if not PYDANTIC_V2: + pytest.skip("This test is only for Pydantic v2", allow_module_level=True) + + +import importlib + +from fastapi.testclient import TestClient + +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial004_an", + pytest.param("tutorial004_an_py39", marks=needs_py39), + pytest.param("tutorial004_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") + + c = TestClient(mod.app) + return c + + +def test_call(client: TestClient): + response = client.post("/items/", json={"item": {"name": "Foo", "size": 3.4}}) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": None, + "size": 3.4, + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_item_items__post" + } + ], + "title": "Body", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "Body_create_item_items__post": { + "properties": { + "item": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + } + }, + "type": "object", + "required": ["item"], + "title": "Body_create_item_items__post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "size": {"type": "number", "title": "Size"}, + }, + "type": "object", + "required": ["name", "size"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_sql_databases/test_tutorial001.py index cc7e590df..6604a2fd3 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial001.py @@ -45,6 +45,8 @@ def get_client(request: pytest.FixtureRequest): with TestClient(mod.app) as c: yield c + # Clean up connection explicitely to avoid resource warning + mod.engine.dispose() def test_crud_app(client: TestClient): diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial002.py b/tests/test_tutorial/test_sql_databases/test_tutorial002.py index 8a98f9a2d..2c4e0988c 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial002.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial002.py @@ -45,6 +45,8 @@ def get_client(request: pytest.FixtureRequest): with TestClient(mod.app) as c: yield c + # Clean up connection explicitely to avoid resource warning + mod.engine.dispose() def test_crud_app(client: TestClient): diff --git a/tests/test_union_body_discriminator.py b/tests/test_union_body_discriminator.py new file mode 100644 index 000000000..6af9e1d22 --- /dev/null +++ b/tests/test_union_body_discriminator.py @@ -0,0 +1,188 @@ +from typing import Any, Dict, Union + +from dirty_equals import IsDict +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +from .utils import needs_pydanticv2 + + +@needs_pydanticv2 +def test_discriminator_pydantic_v2() -> None: + from pydantic import Tag + + app = FastAPI() + + class FirstItem(BaseModel): + value: Literal["first"] + price: int + + class OtherItem(BaseModel): + value: Literal["other"] + price: float + + Item = Annotated[ + Union[Annotated[FirstItem, Tag("first")], Annotated[OtherItem, Tag("other")]], + Field(discriminator="value"), + ] + + @app.post("/items/") + def save_union_body_discriminator( + item: Item, q: Annotated[str, Field(description="Query string")] + ) -> Dict[str, Any]: + return {"item": item} + + client = TestClient(app) + response = client.post("/items/?q=first", json={"value": "first", "price": 100}) + assert response.status_code == 200, response.text + assert response.json() == {"item": {"value": "first", "price": 100}} + + response = client.post("/items/?q=other", json={"value": "other", "price": 100.5}) + assert response.status_code == 200, response.text + assert response.json() == {"item": {"value": "other", "price": 100.5}} + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Save Union Body Discriminator", + "operationId": "save_union_body_discriminator_items__post", + "parameters": [ + { + "name": "q", + "in": "query", + "required": True, + "schema": { + "type": "string", + "description": "Query string", + "title": "Q", + }, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "oneOf": [ + {"$ref": "#/components/schemas/FirstItem"}, + {"$ref": "#/components/schemas/OtherItem"}, + ], + "discriminator": { + "propertyName": "value", + "mapping": { + "first": "#/components/schemas/FirstItem", + "other": "#/components/schemas/OtherItem", + }, + }, + "title": "Item", + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": IsDict( + { + # Pydantic 2.10, in Python 3.8 + # TODO: remove when dropping support for Python 3.8 + "type": "object", + "title": "Response Save Union Body Discriminator Items Post", + } + ) + | IsDict( + { + "type": "object", + "additionalProperties": True, + "title": "Response Save Union Body Discriminator Items Post", + } + ) + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "FirstItem": { + "properties": { + "value": { + "type": "string", + "const": "first", + "title": "Value", + }, + "price": {"type": "integer", "title": "Price"}, + }, + "type": "object", + "required": ["value", "price"], + "title": "FirstItem", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "OtherItem": { + "properties": { + "value": { + "type": "string", + "const": "other", + "title": "Value", + }, + "price": {"type": "number", "title": "Price"}, + }, + "type": "object", + "required": ["value", "price"], + "title": "OtherItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/utils.py b/tests/utils.py index ae9543e3b..691e92bbf 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,10 +8,19 @@ needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires pyth needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) +needs_py_lt_314 = pytest.mark.skipif( + sys.version_info > (3, 13), reason="requires python3.13-" +) needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2") needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1") +def skip_module_if_py_gte_314(): + """Skip entire module on Python 3.14+ at import time.""" + if sys.version_info >= (3, 14): + pytest.skip("requires python3.13-", allow_module_level=True) + + def pydantic_snapshot( *, v2: Snapshot,