From 71ea74fb8f331e6c24cc0a8cdcd28a8cc76d2a35 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:05:27 +0200 Subject: [PATCH 001/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20FastAPI=20Cloud?= =?UTF-8?q?=20deployment=20instructions=20(#15724)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- README.md | 6 +++--- docs/en/docs/deployment/fastapicloud.md | 24 +++--------------------- docs/en/docs/index.md | 6 +++--- docs/en/docs/tutorial/first-steps.md | 20 +++----------------- 4 files changed, 12 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 86e751090..7688e9989 100644 --- a/README.md +++ b/README.md @@ -450,9 +450,7 @@ For a more complete example including more features, see the @@ -468,6 +466,8 @@ Deploying to FastAPI Cloud... +The CLI will automatically detect your FastAPI application and deploy it to the cloud. If you are not logged in, your browser will open to complete the authentication process. + That's it! Now you can access your app at that URL. ✹ #### About FastAPI Cloud diff --git a/docs/en/docs/deployment/fastapicloud.md b/docs/en/docs/deployment/fastapicloud.md index f342c3ee2..dbc44f41a 100644 --- a/docs/en/docs/deployment/fastapicloud.md +++ b/docs/en/docs/deployment/fastapicloud.md @@ -1,26 +1,6 @@ # FastAPI Cloud { #fastapi-cloud } -You can deploy your FastAPI app to [FastAPI Cloud](https://fastapicloud.com) with **one command**, go and join the waiting list if you haven't. 🚀 - -## Login { #login } - -Make sure you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉). - -Then log in: - -
- -```console -$ fastapi login - -You are logged in to FastAPI Cloud 🚀 -``` - -
- -## Deploy { #deploy } - -Now deploy your app, with **one command**: +You can deploy your FastAPI app to [FastAPI Cloud](https://fastapicloud.com) with just **one command**. 🚀
@@ -36,6 +16,8 @@ Deploying to FastAPI Cloud...
+The CLI will automatically detect your FastAPI application and deploy it to the cloud. If you are not logged in, your browser will open to complete the authentication process. + That's it! Now you can access your app at that URL. ✹ ## About FastAPI Cloud { #about-fastapi-cloud } diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 0aeee755e..5f76d664e 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -492,9 +492,7 @@ For a more complete example including more features, see the
@@ -510,6 +508,8 @@ Deploying to FastAPI Cloud... +The CLI will automatically detect your FastAPI application and deploy it to the cloud. If you are not logged in, your browser will open to complete the authentication process. + That's it! Now you can access your app at that URL. ✹ #### About FastAPI Cloud { #about-fastapi-cloud } diff --git a/docs/en/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md index ae43e401b..f1a4b4702 100644 --- a/docs/en/docs/tutorial/first-steps.md +++ b/docs/en/docs/tutorial/first-steps.md @@ -200,23 +200,7 @@ Additionally, other tools might not be able to find it, for example the [VS Code ### Deploy your app (optional) { #deploy-your-app-optional } -You can optionally deploy your FastAPI app to [FastAPI Cloud](https://fastapicloud.com), go and join the waiting list if you haven't. 🚀 - -If you already have a **FastAPI Cloud** account (we invited you from the waiting list 😉), you can deploy your application with one command. - -Before deploying, make sure you are logged in: - -
- -```console -$ fastapi login - -You are logged in to FastAPI Cloud 🚀 -``` - -
- -Then deploy your app: +You can optionally deploy your FastAPI app to [FastAPI Cloud](https://fastapicloud.com) with a single command. 🚀
@@ -232,6 +216,8 @@ Deploying to FastAPI Cloud...
+The CLI will automatically detect your FastAPI application and deploy it to the cloud. If you are not logged in, your browser will open to complete the authentication process. + That's it! Now you can access your app at that URL. ✹ ## Recap, step by step { #recap-step-by-step } From 706e20da5e6c6729299cc643ed2ff383c0bd5440 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Jun 2026 12:05:54 +0000 Subject: [PATCH 002/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1ab8e490c..de52307b4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Update FastAPI Cloud deployment instructions. PR [#15724](https://github.com/fastapi/fastapi/pull/15724) by [@alejsdev](https://github.com/alejsdev). * ✏ Use `Annotated` in inline example in `docs/en/docs/tutorial/body-multiple-params.md`. PR [#15591](https://github.com/fastapi/fastapi/pull/15591) by [@TheArchons](https://github.com/TheArchons). * 📝 Remove "NGINX Unit" from the list of ASGI-servers in docs. PR [#15475](https://github.com/fastapi/fastapi/pull/15475) by [@angryfoxx](https://github.com/angryfoxx). * 📝 Update `docs/en/docs/tutorial/security/oauth2-jwt.md`. PR [#14781](https://github.com/fastapi/fastapi/pull/14781) by [@zadevhub](https://github.com/zadevhub). From 0cf3f76e7c23dd8396a66c30317bf7352bb61898 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:49:44 +0200 Subject: [PATCH 003/188] =?UTF-8?q?=E2=AC=86=20Bump=20https://github.com/c?= =?UTF-8?q?rate-ci/typos=20from=20v1.46.0=20to=20v1.47.1=20in=20the=20pre-?= =?UTF-8?q?commit=20group=20(#15719)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yurii Motov <109919500+YuriiMotov@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b53e2c9ea..0cb5b86e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/crate-ci/typos - rev: bbaefadf97b0ec5fdc942684b647f1a6ab250274 # v1.46.0 + rev: 44e2070e6017f834bf069503acb35ca0ca0b75f2 # v1.47.1 hooks: - id: typos args: [--force-exclude] From e42d913c604f506da17feb445fc8385b841cac65 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Jun 2026 21:50:10 +0000 Subject: [PATCH 004/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index de52307b4..a2633a94c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* ⬆ Bump https://github.com/crate-ci/typos from v1.46.0 to v1.47.1 in the pre-commit group. PR [#15719](https://github.com/fastapi/fastapi/pull/15719) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update sponsors, add Rapidproxy. PR [#15689](https://github.com/fastapi/fastapi/pull/15689) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: Remove TestMu. PR [#15688](https://github.com/fastapi/fastapi/pull/15688) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump the python-packages group across 1 directory with 11 updates. PR [#15683](https://github.com/fastapi/fastapi/pull/15683) by [@dependabot[bot]](https://github.com/apps/dependabot). From 8b80abfbf3bb93f5af2f1b25fbbffcb5177d7e1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:51:55 +0200 Subject: [PATCH 005/188] =?UTF-8?q?=E2=AC=86=20Bump=20starlette=20from=201?= =?UTF-8?q?.1.0=20to=201.2.1=20(#15722)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 14f0d9ccc..52b3df920 100644 --- a/uv.lock +++ b/uv.lock @@ -5174,15 +5174,15 @@ wheels = [ [[package]] name = "starlette" -version = "1.1.0" +version = "1.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/66/4d20cdf39a8d6a51e663b7038e3b828ff211d3891a43a713fe7e4643f3a8/starlette-1.1.0.tar.gz", hash = "sha256:e83c7fe0ddecd8719c5b840080325aec0260acec86e9832899e377b91d65e90f", size = 2660060, upload-time = "2026-05-23T16:55:41.376Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/44/ec35f1b6e83094b997da438a02c8c9b0ade2b1e84cfc48bd4656780760a6/starlette-1.2.1.tar.gz", hash = "sha256:9b9b5ebb992e67d6093741e63c2f59e4f6fff986f81163c087867bd7b924b3f6", size = 2701854, upload-time = "2026-05-31T01:07:51.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/79/920b8e0a8b20f793e8d64855095cb8febabf6175b8550b6f7a547d813891/starlette-1.1.0-py3-none-any.whl", hash = "sha256:7f0dfd38e428aad5cb6f9f667f0ca1d2d8ca3f3385dccac8305f79ec98458382", size = 72899, upload-time = "2026-05-23T16:55:39.201Z" }, + { url = "https://files.pythonhosted.org/packages/1c/54/196d0c1db10af76baa4f64894448505d60d3cdf70ef92cbb35f46a4e4c71/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", size = 73350, upload-time = "2026-05-31T01:07:50.09Z" }, ] [[package]] From b6ed4666fe0a78c392692679fd66937a1b2e7748 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Jun 2026 21:52:25 +0000 Subject: [PATCH 006/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a2633a94c..4b55e9ef1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* ⬆ Bump starlette from 1.1.0 to 1.2.1. PR [#15722](https://github.com/fastapi/fastapi/pull/15722) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump https://github.com/crate-ci/typos from v1.46.0 to v1.47.1 in the pre-commit group. PR [#15719](https://github.com/fastapi/fastapi/pull/15719) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update sponsors, add Rapidproxy. PR [#15689](https://github.com/fastapi/fastapi/pull/15689) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: Remove TestMu. PR [#15688](https://github.com/fastapi/fastapi/pull/15688) by [@tiangolo](https://github.com/tiangolo). From 13b2b4cb346eb2ba18041decf709a528ca05043d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:54:29 +0200 Subject: [PATCH 007/188] =?UTF-8?q?=E2=AC=86=20Bump=20the=20github-actions?= =?UTF-8?q?=20group=20with=203=20updates=20(#15720)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yurii Motov <109919500+YuriiMotov@users.noreply.github.com> --- .github/workflows/build-docs.yml | 6 +++--- .github/workflows/contributors.yml | 4 ++-- .github/workflows/create-draft-release.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/label-approved.yml | 2 +- .github/workflows/latest-changes.yml | 4 ++-- .github/workflows/notify-translations.yml | 4 ++-- .github/workflows/people.yml | 4 ++-- .github/workflows/pre-commit.yml | 4 ++-- .github/workflows/prepare-release.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/sponsors.yml | 4 ++-- .github/workflows/test-redistribute.yml | 2 +- .github/workflows/test.yml | 10 +++++----- .github/workflows/topic-repos.yml | 2 +- .github/workflows/translate.yml | 6 +++--- .github/workflows/zizmor.yml | 2 +- 18 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 128b69e94..fc40a01ad 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -21,7 +21,7 @@ jobs: outputs: docs: ${{ steps.filter.outputs.docs }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # For pull requests it's not necessary to checkout the code but for the main branch it is @@ -47,7 +47,7 @@ jobs: outputs: langs: ${{ steps.show-langs.outputs.langs }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Python @@ -86,7 +86,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Python diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml index cc963ee55..0335ffd50 100644 --- a/.github/workflows/contributors.yml +++ b/.github/workflows/contributors.yml @@ -23,7 +23,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: true # Required for `git push` in `contributors.py` - name: Set up Python @@ -44,7 +44,7 @@ jobs: run: uv sync --locked --no-dev --group github-actions # Allow debugging with tmate - name: Setup tmate session - uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 + uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3.24 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true diff --git a/.github/workflows/create-draft-release.yml b/.github/workflows/create-draft-release.yml index 2f6134341..92cdf1c00 100644 --- a/.github/workflows/create-draft-release.yml +++ b/.github/workflows/create-draft-release.yml @@ -22,7 +22,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.repository.default_branch }} persist-credentials: true diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 1009ec6aa..5932144d7 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -22,7 +22,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Python diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index 55ec5c1c1..cdfb70ec0 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Python diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 92f8f24c9..9390bc538 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -28,14 +28,14 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.FASTAPI_LATEST_CHANGES }} # zizmor: ignore[secrets-outside-env] persist-credentials: true # required by tiangolo/latest-changes # Allow debugging with tmate - name: Setup tmate session - uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 + uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3.24 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 820ac7040..49f34cfec 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -30,7 +30,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Python @@ -51,7 +51,7 @@ jobs: run: uv sync --locked --no-dev --group github-actions # Allow debugging with tmate - name: Setup tmate session - uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 + uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3.24 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index b9c0502a5..8c3f47983 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -23,7 +23,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: true # Required for `git push` in `people.py` - name: Set up Python @@ -44,7 +44,7 @@ jobs: run: uv sync --locked --no-dev --group github-actions # Allow debugging with tmate - name: Setup tmate session - uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 + uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3.24 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 1e156b249..aeadd8d43 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -21,7 +21,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 name: Checkout PR for own repo if: env.HAS_SECRETS == 'true' with: @@ -34,7 +34,7 @@ jobs: token: ${{ secrets.PRE_COMMIT }} # zizmor: ignore[secrets-outside-env] persist-credentials: true # Required for `git push` command # pre-commit lite ci needs the default checkout configs to work - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 name: Checkout PR for fork if: env.HAS_SECRETS == 'false' with: diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 73bf4affc..019dc2462 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -34,7 +34,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: token: ${{ secrets.FASTAPI_LATEST_CHANGES }} # zizmor: ignore[secrets-outside-env] persist-credentials: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 57a6af204..011b6f031 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Python diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 27bb8b195..dd1cb2f43 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index f1538caef..6b0fd6e81 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -24,7 +24,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: true # Required for `git push` in `sponsors.py` - name: Set up Python @@ -45,7 +45,7 @@ jobs: run: uv sync --locked --no-dev --group github-actions # Allow debugging with tmate - name: Setup tmate session - uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 + uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3.24 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index c78fbff56..1555a7643 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -20,7 +20,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Python diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index df77c9bde..97ecd1396 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: outputs: src: ${{ steps.filter.outputs.src }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # For pull requests it's not necessary to checkout the code but for the main branch it is @@ -110,7 +110,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Python @@ -174,7 +174,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Python @@ -194,7 +194,7 @@ jobs: - name: Install Dependencies run: uv sync --no-dev --group tests --extra all - name: CodSpeed benchmarks - uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1 + uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0 with: mode: simulation run: uv run --no-sync pytest tests/benchmarks --codspeed @@ -209,7 +209,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/topic-repos.yml b/.github/workflows/topic-repos.yml index 1b34f1f58..30d1f5a75 100644 --- a/.github/workflows/topic-repos.yml +++ b/.github/workflows/topic-repos.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: true # Required for `git push` in `topic_repos.py` - name: Set up Python diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index 4c624c93c..543105f7d 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -50,7 +50,7 @@ jobs: langs: ${{ steps.show-langs.outputs.langs }} commands: ${{ steps.show-langs.outputs.commands }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Python @@ -92,7 +92,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 persist-credentials: true # Required for `git push` in `translate.py` @@ -113,7 +113,7 @@ jobs: run: uv sync --locked --no-dev --group github-actions --group translations # Allow debugging with tmate - name: Setup tmate session - uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 + uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3.24 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 7cf628679..f68ec5c4a 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -17,7 +17,7 @@ jobs: security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files. steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Run zizmor From 3c5d5bb497469d5004c89f0b5f90dbfd51dd12c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Jun 2026 21:55:01 +0000 Subject: [PATCH 008/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4b55e9ef1..65736b7d5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* ⬆ Bump the github-actions group with 3 updates. PR [#15720](https://github.com/fastapi/fastapi/pull/15720) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump starlette from 1.1.0 to 1.2.1. PR [#15722](https://github.com/fastapi/fastapi/pull/15722) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump https://github.com/crate-ci/typos from v1.46.0 to v1.47.1 in the pre-commit group. PR [#15719](https://github.com/fastapi/fastapi/pull/15719) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update sponsors, add Rapidproxy. PR [#15689](https://github.com/fastapi/fastapi/pull/15689) by [@tiangolo](https://github.com/tiangolo). From 2015b63561b47b52306563c02590748db1d03eaf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 00:04:26 +0200 Subject: [PATCH 009/188] =?UTF-8?q?=E2=AC=86=20Bump=20python-multipart=20f?= =?UTF-8?q?rom=200.0.29=20to=200.0.30=20(#15723)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yurii Motov <109919500+YuriiMotov@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 52b3df920..16bae5e46 100644 --- a/uv.lock +++ b/uv.lock @@ -4361,11 +4361,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.29" +version = "0.0.30" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/82/c8cd43a6e0719bf5a3b034f6726dd701f75829c08944c83d4b95d02ed0e8/python_multipart-0.0.30.tar.gz", hash = "sha256:0edfe0475c1f46ddd3ff7785a626f6118af32bdcf359bb21260367313bb32118", size = 46316, upload-time = "2026-05-31T19:24:55.198Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/0318007beb234790993d3ec5afd051d1dbceb733e81e3afe2b981ece3f37/python_multipart-0.0.30-py3-none-any.whl", hash = "sha256:830964def8c90607ac5daa00514e3987815865713ade8d20febc9177ac0c3c5b", size = 29730, upload-time = "2026-05-31T19:24:53.814Z" }, ] [[package]] From 4be770779e93519b35f233f5adaf302ed9ff5dc2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Jun 2026 22:04:54 +0000 Subject: [PATCH 010/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 65736b7d5..05cd936b0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* ⬆ Bump python-multipart from 0.0.29 to 0.0.30. PR [#15723](https://github.com/fastapi/fastapi/pull/15723) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump the github-actions group with 3 updates. PR [#15720](https://github.com/fastapi/fastapi/pull/15720) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump starlette from 1.1.0 to 1.2.1. PR [#15722](https://github.com/fastapi/fastapi/pull/15722) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump https://github.com/crate-ci/typos from v1.46.0 to v1.47.1 in the pre-commit group. PR [#15719](https://github.com/fastapi/fastapi/pull/15719) by [@dependabot[bot]](https://github.com/apps/dependabot). From e133ffbc1b41bd9f2b4f96f9887e2eb1383530bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 00:05:55 +0200 Subject: [PATCH 011/188] =?UTF-8?q?=E2=AC=86=20Bump=20the=20python-package?= =?UTF-8?q?s=20group=20across=201=20directory=20with=206=20updates=20(#157?= =?UTF-8?q?21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 157 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 83 insertions(+), 74 deletions(-) diff --git a/uv.lock b/uv.lock index 16bae5e46..a7552eb80 100644 --- a/uv.lock +++ b/uv.lock @@ -222,7 +222,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.104.1" +version = "0.109.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -234,9 +234,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/c7/7a655b948916f777354648ce979f68b94d5b8dbdb5f61fed1f37fad9378c/anthropic-0.104.1.tar.gz", hash = "sha256:17362b6c45f527afcc9b0fdf62011ffd359726ab2ebcb1978ea0cc41bd8d8d40", size = 850081, upload-time = "2026-05-22T15:36:57.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/3b/96c84c2d70fb7e185d240fbd101c185f7f9b0f6fea39d0f86d22c628add5/anthropic-0.109.0.tar.gz", hash = "sha256:86719114c62f21b50bf7148a373371605f065d22e6eeee3d4d361ac0ea079d0e", size = 927503, upload-time = "2026-06-09T20:04:24.701Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/12/d9ab42790494d7c428391a46cd28492395566a6a8ccb138d681978594455/anthropic-0.104.1-py3-none-any.whl", hash = "sha256:35c8cb456f5a4405aafe1f10f03f6fcc54fa51fa8ec01d655cc4b437d120e9b7", size = 832996, upload-time = "2026-05-22T15:36:59.519Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e9/267ea711185049714c4f3d1256b23e26f6968594651c155910584cc533bb/anthropic-0.109.0-py3-none-any.whl", hash = "sha256:b5d930af26fdeeb7c7e7a6ff6c4bcdaf46ee50403b26ada25d08fd853c751fff", size = 923835, upload-time = "2026-06-09T20:04:26.098Z" }, ] [[package]] @@ -2160,15 +2160,15 @@ wheels = [ [[package]] name = "httpcore2" -version = "2.2.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, { name = "h11" }, + { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/7e/8ab39aab1d392845b6512009a9be57d24a5bd4ec7a22d02e513d0645e7a8/httpcore2-2.2.0.tar.gz", hash = "sha256:10e0e142f1ecc1c1cb2a9ebbce82e57f16169f61d163ea336abf36799e89294b", size = 63533, upload-time = "2026-05-17T05:29:55.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/34/18f1c596e677962f040284246f393b10a1f8ce440b3a7e69c637d0f1c7ad/httpcore2-2.3.0.tar.gz", hash = "sha256:07327e251560960eea8e969d92d4c6a325feb13cca39e25340731336c3baf924", size = 64300, upload-time = "2026-06-01T13:15:02.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/22/64de17e7956e8c002f7558ed667d924c2a288344aeff4bd8ff5dc5fdb70b/httpcore2-2.2.0-py3-none-any.whl", hash = "sha256:ce859f268bf8d34fa2d7753e09e4dd5194f557e1b3038439b68a89b2999572fa", size = 79288, upload-time = "2026-05-17T05:29:52.56Z" }, + { url = "https://files.pythonhosted.org/packages/c2/dd/3357218c69360d1cecc196c230c9a1d5c9afd5dba362056e23e60a5e64e5/httpcore2-2.3.0-py3-none-any.whl", hash = "sha256:477e9e334f74e5240dcac002e890580f36a57d40ff0fb14cc9655731d23b8415", size = 80024, upload-time = "2026-06-01T13:15:00.001Z" }, ] [[package]] @@ -2240,17 +2240,17 @@ wheels = [ [[package]] name = "httpx2" -version = "2.2.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "certifi" }, { name = "httpcore2" }, { name = "idna" }, + { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/aa/c3119de1aa7ad870a01aaddbf3bc3445ed9a681c31d45e3838fd8b7bc155/httpx2-2.2.0.tar.gz", hash = "sha256:f3428d59b1752b8f5629826277262fb4d65e3a683f48af8a5b16c4d012e0b801", size = 80477, upload-time = "2026-05-17T05:29:57.376Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/9a/cca0b9145f13d8ae34b885ae28d403a1469a433abc78e0f94f4ce94e650b/httpx2-2.3.0.tar.gz", hash = "sha256:227e7c41d95a76d4077a52640564132777215fc3394e07b66a3116c33d668fa9", size = 81115, upload-time = "2026-06-01T13:15:04.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/e0/e0a52596c14194e428c20de4903f4abec38c0dfb5364d20f1d4a2b6266ef/httpx2-2.2.0-py3-none-any.whl", hash = "sha256:12347ebd2daeaefd50b529359778fff767082a09c5826752c963e71269722ff0", size = 74083, upload-time = "2026-05-17T05:29:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/ce/ae2911859847f9ba1d6b23027e53481cbeb50b93234f355a968d300ca2cb/httpx2-2.3.0-py3-none-any.whl", hash = "sha256:6f393663bdf6dbe7fe90118e3eb5b2bd024a675cae0390ac08cec9198812d8b7", size = 74538, upload-time = "2026-06-01T13:15:01.566Z" }, ] [[package]] @@ -2306,7 +2306,7 @@ wheels = [ [[package]] name = "inline-snapshot" -version = "0.33.0" +version = "0.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asttokens" }, @@ -2316,9 +2316,9 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/e8/5f661ea222480a5b512c3713abe4ce080a09105ae8212549415f40021fcc/inline_snapshot-0.33.0.tar.gz", hash = "sha256:856cfc18dea755dd78ffa0fbac8c161038ca0bfb4bd0bbb5d519f4bca3dfeff4", size = 2637158, upload-time = "2026-05-12T18:39:47.923Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/6b/70c657a50e61b51d6ac81611230edf50a30aca16b019c2a2fb77efee92a1/inline_snapshot-0.34.0.tar.gz", hash = "sha256:bb814ada843aba77356516dce5e07857bb76591c4258296a087a7cbe9e0d70f7", size = 2638680, upload-time = "2026-05-29T21:01:05.087Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/2c/4b209e9dd6700cea0c0e39d7e5e70e9f494f817a374174a823bd11561d31/inline_snapshot-0.33.0-py3-none-any.whl", hash = "sha256:76b8c2c5899d27d3d464d1160eb3b8eee179ba635bb80a8e5e93220f10b60207", size = 89625, upload-time = "2026-05-12T18:39:46.43Z" }, + { url = "https://files.pythonhosted.org/packages/a2/cb/c279d8d0775ffabb8c10a7f9ff991cad155a9933cb931e98e81c1c2f9050/inline_snapshot-0.34.0-py3-none-any.whl", hash = "sha256:21a89c1ff18f1a20fa1a9b16567e8a29c01874458c3f2d80a0f97550f1809354", size = 90010, upload-time = "2026-05-29T21:01:03.621Z" }, ] [[package]] @@ -3828,19 +3828,19 @@ email = [ [[package]] name = "pydantic-ai" -version = "1.103.0" +version = "1.105.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "spec", "temporal", "ui", "vertexai", "xai"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/2b/0b159b94238b4b32a72092ed07345b3ce5d2de2c25cd928290bafc2244ca/pydantic_ai-1.103.0.tar.gz", hash = "sha256:9c07deb5dbcc3374ab4aab7caf12256380eed99354f987cbc6a078ffe5d6959f", size = 18091, upload-time = "2026-05-27T02:49:01.878Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/b5/f9d3333b994a626e8dc2db49eacd24f7f16bf1810664f5ffd5918f57781a/pydantic_ai-1.105.0.tar.gz", hash = "sha256:f4f4ecd357c0f13f97ce5976e0bd29c98cc1d08b68c1477cf9711c96853c8120", size = 18368, upload-time = "2026-06-02T06:19:59.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/a4/5d14afbb989c7a135f8c2b47ab05dfe1b3275836393000ba58730021f10b/pydantic_ai-1.103.0-py3-none-any.whl", hash = "sha256:e6dd3d642905841f4cb93375f1cec313f511eb9dea5f100535812b76f927e499", size = 7587, upload-time = "2026-05-27T02:48:52.83Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5e/19cd865dd7e356ca6f43ae761421f27dbcfb658bf0ccc294eeeb35563819/pydantic_ai-1.105.0-py3-none-any.whl", hash = "sha256:186d2735a7c424e2cf9bb8f1c74684e08d07f5e06adae4cf43c6e24ab619a5e6", size = 7587, upload-time = "2026-06-02T06:19:49.795Z" }, ] [[package]] name = "pydantic-ai-slim" -version = "1.103.0" +version = "1.105.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, @@ -3852,9 +3852,9 @@ dependencies = [ { name = "pydantic-graph" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/ce/ae3cd88af67e418e8d68749b1014d52aa84188acee438241ec819918a31a/pydantic_ai_slim-1.103.0.tar.gz", hash = "sha256:b0fda8eea125440b74c1de9cb5c8ed4fa6cfae72586b7de03581adf263903b8a", size = 748460, upload-time = "2026-05-27T02:49:04.141Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/ae/1b0370f9b9f1ca7ccf2e6b51ec5a8d11da11d9dd621e5eb015c6420c5e9b/pydantic_ai_slim-1.105.0.tar.gz", hash = "sha256:8b4ad8034b40ab3bde8e0c6285082a204ecd203007150a47943f192b474e06e9", size = 772048, upload-time = "2026-06-02T06:20:01.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/36/261c0891cc0925a59f0f3741ef2057ff3074c9a7940220ea39f83e9dd434/pydantic_ai_slim-1.103.0-py3-none-any.whl", hash = "sha256:866d5a6376f113bcb7e20749b87640db919efd9a44fdbb73574b77f06bc83c3d", size = 927993, upload-time = "2026-05-27T02:48:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/8afdff693d21c0743ee71d792ce90afc27d4ddbaf7270d969a84452cfd0d/pydantic_ai_slim-1.105.0-py3-none-any.whl", hash = "sha256:1e65561ba9a58a9d8fc3a63b550c3c2b2c4017da275dea78291e526aa06298d8", size = 956108, upload-time = "2026-06-02T06:19:52.821Z" }, ] [package.optional-dependencies] @@ -4046,7 +4046,7 @@ wheels = [ [[package]] name = "pydantic-evals" -version = "1.103.0" +version = "1.105.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -4056,9 +4056,9 @@ dependencies = [ { name = "pyyaml" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/6c/b9f5d6091acee5d19674cedbef4376786230cc53abf6707e85f40fd28118/pydantic_evals-1.103.0.tar.gz", hash = "sha256:44e68377559b7fd29373ffbcc95fa2562ebddcf2fb52a5e9e02c4b4eaab5b0fb", size = 78555, upload-time = "2026-05-27T02:49:05.392Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/9a/aa1b99d2600aa61365f792bd093ca9eb39d75a2223bcb699d64e6cc1c3bf/pydantic_evals-1.105.0.tar.gz", hash = "sha256:9a12b1062329fbc26ca2b75e30c79bf0938146ed3e080a082d36442428bf4926", size = 78534, upload-time = "2026-06-02T06:20:04.047Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/28/62bd5c07d06decf4594a7a88d280f448f81dec191782fe56e38470e7f826/pydantic_evals-1.103.0-py3-none-any.whl", hash = "sha256:53b7b58b74fb54637e7199e118415b4269926b0a10bfc9eeb0d47b9e9fa9c443", size = 93528, upload-time = "2026-05-27T02:48:58.126Z" }, + { url = "https://files.pythonhosted.org/packages/14/ea/7e3da28a264e9d63628f419fcc15d81777e1261c61f85176f9210b196838/pydantic_evals-1.105.0-py3-none-any.whl", hash = "sha256:990a5d46255c43dbf2e04c3143f4c25212f492357259f7b358ffc5ef3d2625ff", size = 93531, upload-time = "2026-06-02T06:19:55.586Z" }, ] [[package]] @@ -4076,7 +4076,7 @@ wheels = [ [[package]] name = "pydantic-graph" -version = "1.103.0" +version = "1.105.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -4084,9 +4084,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/f9/301c8477cc2309d827ef0e99e73c36581bd9d8811bd85a2a8bdd58a17a74/pydantic_graph-1.103.0.tar.gz", hash = "sha256:174ce80e3a67e393a227c70f5c9531497a15ce064fae8aef4635c1a5d9507c68", size = 62589, upload-time = "2026-05-27T02:49:06.61Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/98/0361e1eb28f8d107e4e12dcd2d14eabef55f4a8ca18b1a6f185df74934c0/pydantic_graph-1.105.0.tar.gz", hash = "sha256:3f5cf97d544b900098d3cc2dbd6a8cdd79ea59dac610d7651f86c9228d33c0b9", size = 62570, upload-time = "2026-06-02T06:20:05.158Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/fb/60037718a0219933ae4fd2c393c69529260593a49f3fd334b3e7f9e3a912/pydantic_graph-1.103.0-py3-none-any.whl", hash = "sha256:98687e53811db9e324fabbf5f4692b5dd6af458591461ab6255230d8bc4bebf3", size = 80100, upload-time = "2026-05-27T02:48:59.756Z" }, + { url = "https://files.pythonhosted.org/packages/be/1b/13882fd4d70299dc2995bee20f21599cb8d453b27f44e239f82384d4ea3f/pydantic_graph-1.105.0-py3-none-any.whl", hash = "sha256:ba76d77ad21a13f2961fbda9d988f3d5a3d9ffc1817ee912e0ea59b0b5a9e825", size = 80099, upload-time = "2026-06-02T06:19:57.098Z" }, ] [[package]] @@ -4970,27 +4970,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" }, - { url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" }, - { url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" }, - { url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" }, - { url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" }, - { url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" }, - { url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" }, - { url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" }, - { url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" }, - { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" }, +version = "0.15.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, + { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, + { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, + { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, + { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, ] [[package]] @@ -5435,34 +5435,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030, upload-time = "2025-10-31T07:18:15.885Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "ty" -version = "0.0.40" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/f8/a754c96967b71de8723f88be17df8738216bd382ffed229cd500b7a24d13/ty-0.0.40.tar.gz", hash = "sha256:883b53dd98f6e5b33ab1c8e1a3cd94b0f29c762ef22cdf1e86aaffb4fd711c67", size = 5726484, upload-time = "2026-05-27T17:55:43.615Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/42/d029a72165ad39f95228b67355927fbd35c821dc8e3e475d49f47c2eeb1e/ty-0.0.40-py3-none-linux_armv6l.whl", hash = "sha256:9defb4742450e569a6a09de286a04008d6c2e815112da4362c88b6eaa2f52a36", size = 11406372, upload-time = "2026-05-27T17:55:49.633Z" }, - { url = "https://files.pythonhosted.org/packages/23/99/7f8ea09b7e49afbf795cb3341a3217f30f228db7e62a2268ed8cbbf813d6/ty-0.0.40-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:868258a3330db88b683fcafe2c4e936d6226a6312799bf15b585d93557b2d38c", size = 11159782, upload-time = "2026-05-27T17:55:47.405Z" }, - { url = "https://files.pythonhosted.org/packages/04/d8/1ea745ee97a98b26ae9564d19a430a76a35297cd450e84dcaad22e1f7ee8/ty-0.0.40-py3-none-macosx_11_0_arm64.whl", hash = "sha256:589c81060cf1e7a9ffa2f45bfa35ffd9b9fbd214104e3f13959f113627efcd91", size = 10594139, upload-time = "2026-05-27T17:55:37.206Z" }, - { url = "https://files.pythonhosted.org/packages/39/1a/fbef21273c6617ff4715b4827ee1c0b6550aa7d1df4b8c43b325545c1cf4/ty-0.0.40-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b06108990cb338d941c315ae6e9ba2fff8f518bc15d3f33e5619ff6a6c9beab", size = 11114156, upload-time = "2026-05-27T17:55:56.11Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f9/389fc4976d7ec016a7473cf1274bf9c4f491bb54c66649bd022bff9f2b6a/ty-0.0.40-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3913ef37336bec4f96bd2512f8c3a543ca34c259b7170f7eb5adf75b3ed7f04c", size = 11189050, upload-time = "2026-05-27T17:55:54.099Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a9/4ecabbf4bdda7df0d99d8d3892c6edac0efc8c4cae756a5109178a3d0e86/ty-0.0.40-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fd1486bd5fe48779a8aa857137f3642a0a9161f5cf57d4380f4a0ecea01c8f3", size = 11664266, upload-time = "2026-05-27T17:55:28.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/02/0aa78730116507c265afb1d6d5961c583b49d4c2e368c4a49fd81bcae6dc/ty-0.0.40-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1668364d5254a734329917ee66c2c5fdd5665389d41043f6fce0f22ddb32b749", size = 12187743, upload-time = "2026-05-27T17:56:04.337Z" }, - { url = "https://files.pythonhosted.org/packages/e6/68/ccabf2d173523598271a385c1d3f864dbda23e5ebdc67f5969b9e830ea05/ty-0.0.40-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f77a73edb91e5dfa2ab9af7c4cac64614f8cc121f38a8875f22e830d3aba6a", size = 11862999, upload-time = "2026-05-27T17:55:58.087Z" }, - { url = "https://files.pythonhosted.org/packages/03/8d/6d7ec22771bb23d534797cdb446eb644bccfe7a62b729bb99e7235a02fc3/ty-0.0.40-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1274ce0212ecbfed01bda7c3659c46e8bd0068e32d00c46c790466a95274c3df", size = 11743896, upload-time = "2026-05-27T17:56:00.017Z" }, - { url = "https://files.pythonhosted.org/packages/cd/a4/f9fa076b010c91cb249b1fcc3476569b7b8462cb4b688da2d04c23a0622f/ty-0.0.40-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5ee1261dbc363e5cc1a0c5bb0c8612c192bfe53491214df8bc85a540835685f9", size = 11883581, upload-time = "2026-05-27T17:56:02.319Z" }, - { url = "https://files.pythonhosted.org/packages/fd/0f/5b776a2328c756d574dd4d6afbd30fc24e1ab4b76935c7c3c23f27ebbcb9/ty-0.0.40-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6220e2cd5cdc4683dd87fb150d195bbd9f1a021395e04cb08bd3c66ea6da6ef8", size = 11093946, upload-time = "2026-05-27T17:55:33.284Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/eb23154bae83ad7c2935e9e5916660fb3e31598a92ee232aebd79410480c/ty-0.0.40-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:46b9ed69d01d98ef046afac9983c68336f572605ea2a27b90fbe6f80bfc8d6b7", size = 11210737, upload-time = "2026-05-27T17:55:45.523Z" }, - { url = "https://files.pythonhosted.org/packages/ff/19/1fb2529703f708cacfd13a89f98613cae2907dfa941b26976467e6119803/ty-0.0.40-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ddbca9fab4406260f141674ab5efcfe7b02bd468e6985e4cdde0a21626e69ffe", size = 11332563, upload-time = "2026-05-27T17:55:41.674Z" }, - { url = "https://files.pythonhosted.org/packages/87/69/b3f5a8ef26c31204e0391147b3adcdb0674eda3e7d99868478ef168a41c6/ty-0.0.40-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1fcc082a749e6dc11b68fe9aab0420238bbf2a2374c2c7aa3c22e8c1618b136", size = 11843216, upload-time = "2026-05-27T17:55:35.367Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e8/20193069d32787f3e1a6ec8940aaa3759d3de8f48f9281bcc0c5cb0939da/ty-0.0.40-py3-none-win32.whl", hash = "sha256:75feb115b3587824c5bdf8f8305e9547b0d1e398e3077b0addc7a1988ea9bb50", size = 10670731, upload-time = "2026-05-27T17:55:31.316Z" }, - { url = "https://files.pythonhosted.org/packages/a3/f9/8b2aa4da61db81322d4a2f9db227afeb48110ca15ae31d380f64c64ceb63/ty-0.0.40-py3-none-win_amd64.whl", hash = "sha256:b0f905edaad788bd61f779a85801b60a267a25ed57fca05aaddd168d9d8896be", size = 11766211, upload-time = "2026-05-27T17:55:51.898Z" }, - { url = "https://files.pythonhosted.org/packages/04/87/369056ed46f1b235130ec0595393262f9cd2061ca3dab276d490980f9343/ty-0.0.40-py3-none-win_arm64.whl", hash = "sha256:07da2b09d9130e2c9a257d2a29beb53105835b0256ee5fdb288fe1aab83fee47", size = 11117369, upload-time = "2026-05-27T17:55:39.329Z" }, +version = "0.0.42" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/91/5b5ec4ed8721c18be8d9611778d7c07723cd755676f03b41bf0ea0caa5d3/ty-0.0.42.tar.gz", hash = "sha256:70f5553ac678fc63558d4d77b08a18a68a228f44be2a2fe1afc3f5988db662e7", size = 5769116, upload-time = "2026-06-01T19:40:32.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/7c/2df5136ad7c0db69a3973b0b19da8f52bfacdc453c7dffc832d1bf7d23ff/ty-0.0.42-py3-none-linux_armv6l.whl", hash = "sha256:c08a0066610c13627b7d7ad758adb96ca99685791e641eb26837e20803851c53", size = 11544141, upload-time = "2026-06-01T19:40:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/0a/82/96cf406d39d8976e825361a27e332224445812793060ac9506d8a5d32b39/ty-0.0.42-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3e944ee4e3d5cdaf70e4ea87f9dd474cc3db612837b50a3ce57afa8da400ecc2", size = 11283538, upload-time = "2026-06-01T19:40:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fe/813b60b9332df835c16c05859ff5aac1896593d01b638ba0e461ede415ac/ty-0.0.42-py3-none-macosx_11_0_arm64.whl", hash = "sha256:603085306e4aac2ce592b39119a4b49ebf8b780cd394e2cfc7dbf3fd8228f954", size = 10711874, upload-time = "2026-06-01T19:40:28.77Z" }, + { url = "https://files.pythonhosted.org/packages/07/64/1f609265be0302ce0f51aa03a27636d018947a76100ff1405258d8445e6c/ty-0.0.42-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a58f17834d7f078c49326a01111a5aac16c979a774b98cdfd8e2350068316676", size = 11213021, upload-time = "2026-06-01T19:40:45.01Z" }, + { url = "https://files.pythonhosted.org/packages/88/c0/f147b2fde7cd01b5f77682937e85a5cdfe35f48b3f1d0f41021024cbd927/ty-0.0.42-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e6ed1027f313202c5c74e376007d1eb5d214494299beb0ea047078b8ad307d40", size = 11321604, upload-time = "2026-06-01T19:40:55.164Z" }, + { url = "https://files.pythonhosted.org/packages/a9/72/74a5e68a9bd194681f15c4aac7a0dfab378e76e7a107e8ecd93971a22377/ty-0.0.42-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:063838d2360c1d2c065b45ca76a56ecd6df07fff6570813e74183236559e16d9", size = 11802178, upload-time = "2026-06-01T19:40:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/3c/9f/06a31dc9cc91faa2cb8a4bf2f0ba5f7a9a96a4828fb8434338682954ca86/ty-0.0.42-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3f9ed508dfae4cbc943d7766324dd9c57ac8302c8543505fc29cae8ed425fe9", size = 12358436, upload-time = "2026-06-01T19:40:48.968Z" }, + { url = "https://files.pythonhosted.org/packages/06/33/a5bb1afcb671e0b9197f007264682b22f1063bf0e83c151eeaf9958f9047/ty-0.0.42-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fe1eb7d98472ac56ac19fec51c6ed8fe56d86ea0d232a11a127e8c62c882a66", size = 11997849, upload-time = "2026-06-01T19:40:42.951Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8d/560e4ec4c2f69e68fa094bb93cd19b5eb92c9732d2e0f5e7cc3accde84c4/ty-0.0.42-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de75f9e78bfa81209f2f297528758977cfd4518ba35ef45a0acb516c892a27a5", size = 11869087, upload-time = "2026-06-01T19:40:24.38Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/3794db15199c03eda60961690046a56c4fbf5d8ef073c82fe2402c851b8c/ty-0.0.42-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c63281f2f1d4df339117fcd4a6dfe17cb999f84eafe707b30e9ebbe26f0bb54a", size = 12059000, upload-time = "2026-06-01T19:40:34.982Z" }, + { url = "https://files.pythonhosted.org/packages/98/9a/02b61cc65ecbd90f18bd02178845c5b756c78fb92643571e77df52c6eed8/ty-0.0.42-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:19e3856477f25255f772851fa7f16f5356c4e1927324d074d49c7bc9a9b211e1", size = 11195698, upload-time = "2026-06-01T19:40:37.109Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d6/3927335c956b06a806269dcd2e5b46bc4284b0a95ecc6b0246094a1de28c/ty-0.0.42-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f0f4acac9028264cee5ea0b88229df0b9b2586fc917dbadb6ee35a0e99e8b06", size = 11353487, upload-time = "2026-06-01T19:40:47.035Z" }, + { url = "https://files.pythonhosted.org/packages/d6/87/79e7ae4f5f9fb3bea5f3cfac2b4f8c60e905f13962e3c0d97f8b51a5bff6/ty-0.0.42-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae244c84e30fdf2bb1a3cbf2b973da8aa535e57c701f12db44b2939604586c04", size = 11463474, upload-time = "2026-06-01T19:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/73305ead1b3ccc3d79c04258877db2cd7908c6a6c2060d4e598deca384d2/ty-0.0.42-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2430434f4a52bec0da552ff6a061dcc1c5d11973259248679a1146d776c12f37", size = 11961710, upload-time = "2026-06-01T19:40:39.056Z" }, + { url = "https://files.pythonhosted.org/packages/e1/04/52b7325dad8d1a86653f90240208f3e3657bed5e3c8144eed367a0f736b0/ty-0.0.42-py3-none-win32.whl", hash = "sha256:984a55c2fe63b40dac03f5a144b99033c7ed720eb7611787e3f0bd49af8dcf12", size = 10783897, upload-time = "2026-06-01T19:40:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/94/d2/3d2d61255c76c0843766f00b39290115c23cc1cd4fcb0471d84a48f482f1/ty-0.0.42-py3-none-win_amd64.whl", hash = "sha256:f7afd81b10b377d9d4ce6aad355a4f47fd37d47f443118c01ca6e79d46fe6608", size = 11878640, upload-time = "2026-06-01T19:40:50.903Z" }, + { url = "https://files.pythonhosted.org/packages/b4/91/1eb0c1e3d558707ead7424f8bfd89b58f42e576714cbed7ad46dcceef34a/ty-0.0.42-py3-none-win_arm64.whl", hash = "sha256:4068c24b0b264fc9f1901e06b97988a041fcaa36c90f18d7747f05124701c7b3", size = 11202335, upload-time = "2026-06-01T19:40:53.155Z" }, ] [[package]] name = "typer" -version = "0.26.2" +version = "0.26.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -5470,9 +5479,9 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/a5/756f2e6bc81a7dd79aa3c625dd01b74cabc4516628cace2caaec09ca6ff2/typer-0.26.2.tar.gz", hash = "sha256:9b4f19e08fcc9427a822d1ef467b1fe76737a2f65c7926bdeba2337d73569b68", size = 198991, upload-time = "2026-05-27T10:41:39.166Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8a/8dc5733b8939e7f1a71173091a6e27a1658345edbff548a0bf3f5bb26173/typer-0.26.6.tar.gz", hash = "sha256:cdbc160fe7e795b835fb6016419494a521a67bfb86b9476a1ccd0e7727d3ae5b", size = 201595, upload-time = "2026-06-02T13:47:50.536Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/a5/6ffd702beda8798b2b82ff70805ed4a66d963557e43a5d1823ab456251a4/typer-0.26.2-py3-none-any.whl", hash = "sha256:39beff72ffbb31978a5b545f677d57edb97c6f980f433b38556deb0af25f094d", size = 123123, upload-time = "2026-05-27T10:41:40.504Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/519138260db5e2fe03a509bf9e8ef6af9a514d3565c8fa74fc4fededbae1/typer-0.26.6-py3-none-any.whl", hash = "sha256:49f96d9ee5730cef607bbe155042f40b41fa4c0d0dec04990d580837493805be", size = 122464, upload-time = "2026-06-02T13:47:51.768Z" }, ] [[package]] @@ -5912,7 +5921,7 @@ wheels = [ [[package]] name = "xai-sdk" -version = "1.6.1" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -5924,9 +5933,9 @@ dependencies = [ { name = "pydantic" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/66/1e0163eac090733d0ed0836a0cd3c14f5b59abeaa6fdba71c7b56b1916e4/xai_sdk-1.6.1.tar.gz", hash = "sha256:b55528df188f8c8448484021d735f75b0e7d71719ddeb432c5f187ac67e3c983", size = 388223, upload-time = "2026-01-29T03:13:07.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/0a/74a835cff3c2c5135bcbdca152eed1bd0b7b2c9156e457dfc0d9af987df9/xai_sdk-1.15.0.tar.gz", hash = "sha256:6508b702d01da9c55c15cdcb329c4ad58eb9251340b2514d0974bcba11e764e3", size = 433102, upload-time = "2026-05-30T01:42:15.977Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/98/8b4019b35f2200295c5eec8176da4b779ec3a0fd60eba7196b618f437e1f/xai_sdk-1.6.1-py3-none-any.whl", hash = "sha256:f478dee9bd8839b8d341bd075277d0432aff5cd7120a4284547d25c6c9e7ab3b", size = 240917, upload-time = "2026-01-29T03:13:05.626Z" }, + { url = "https://files.pythonhosted.org/packages/f5/39/7513677c0dafc5171ca4a78f9af3a2acfb9c8798aab62bc4464452ac277f/xai_sdk-1.15.0-py3-none-any.whl", hash = "sha256:de1dcb856941bcbc64c0e61b7202395593c1950e0e00e6aa0ca7657448c03aca", size = 260289, upload-time = "2026-05-30T01:42:14.431Z" }, ] [[package]] From b31720337af8b5ba5c78e4f0c44ac0ce25d7a5e2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Jun 2026 22:07:36 +0000 Subject: [PATCH 012/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 05cd936b0..2ea9af204 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* ⬆ Bump the python-packages group across 1 directory with 6 updates. PR [#15721](https://github.com/fastapi/fastapi/pull/15721) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump python-multipart from 0.0.29 to 0.0.30. PR [#15723](https://github.com/fastapi/fastapi/pull/15723) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump the github-actions group with 3 updates. PR [#15720](https://github.com/fastapi/fastapi/pull/15720) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump starlette from 1.1.0 to 1.2.1. PR [#15722](https://github.com/fastapi/fastapi/pull/15722) by [@dependabot[bot]](https://github.com/apps/dependabot). From e52919282ab7ae101bcea400542088ff5791f4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 11 Jun 2026 00:44:29 +0200 Subject: [PATCH 013/188] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors:=20rem?= =?UTF-8?q?ove=20Scalar=20(#15730)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 - docs/en/data/sponsors.yml | 3 --- docs/en/overrides/main.html | 6 ------ 3 files changed, 10 deletions(-) diff --git a/README.md b/README.md index 7688e9989..5c86e97ba 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,6 @@ The key features are: ### Gold Sponsors
- diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 1466536d0..7fa140f73 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -6,9 +6,6 @@ gold: - url: https://blockbee.io?ref=fastapi title: BlockBee Cryptocurrency Payment Gateway img: https://fastapi.tiangolo.com/img/sponsors/blockbee.png - - url: https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge - title: "Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files" - img: https://fastapi.tiangolo.com/img/sponsors/scalar.svg - url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge title: Auth, user management and more for your B2B product img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 7559de529..da7e270de 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -46,12 +46,6 @@ -
- - - - -
From b98fb552e6ba43364f7ae39fd6ef96ecc6f38873 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Jun 2026 22:44:58 +0000 Subject: [PATCH 014/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2ea9af204..235fe8605 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* 🔧 Update sponsors: remove Scalar. PR [#15730](https://github.com/fastapi/fastapi/pull/15730) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump the python-packages group across 1 directory with 6 updates. PR [#15721](https://github.com/fastapi/fastapi/pull/15721) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump python-multipart from 0.0.29 to 0.0.30. PR [#15723](https://github.com/fastapi/fastapi/pull/15723) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump the github-actions group with 3 updates. PR [#15720](https://github.com/fastapi/fastapi/pull/15720) by [@dependabot[bot]](https://github.com/apps/dependabot). From ee036a111ed1e96a7cb425158bda3dc0b45e3d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 11 Jun 2026 00:59:29 +0200 Subject: [PATCH 015/188] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors:=20rem?= =?UTF-8?q?ove=20Liblab=20(#15731)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 - docs/en/data/sponsors.yml | 3 --- docs/en/docs/advanced/generate-clients.md | 1 - docs/en/overrides/main.html | 6 ------ 4 files changed, 11 deletions(-) diff --git a/README.md b/README.md index 5c86e97ba..9936ab88a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,6 @@ The key features are: - diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 7fa140f73..e72cbbd9f 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -9,9 +9,6 @@ gold: - url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge title: Auth, user management and more for your B2B product img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png - - url: https://liblab.com?utm_source=fastapi - title: liblab - Generate SDKs from FastAPI - img: https://fastapi.tiangolo.com/img/sponsors/liblab.png - url: https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi title: Deploy & scale any full-stack web app on Render. Focus on building apps, not infra. img: https://fastapi.tiangolo.com/img/sponsors/render.svg diff --git a/docs/en/docs/advanced/generate-clients.md b/docs/en/docs/advanced/generate-clients.md index 1fff3c9dc..eb1c53ab7 100644 --- a/docs/en/docs/advanced/generate-clients.md +++ b/docs/en/docs/advanced/generate-clients.md @@ -31,7 +31,6 @@ Their sponsorship also demonstrates a strong commitment to the FastAPI **communi For example, you might want to try: * [Stainless](https://www.stainless.com/?utm_source=fastapi&utm_medium=referral) -* [liblab](https://developers.liblab.com/tutorials/sdk-for-fastapi?utm_source=fastapi) Some of these solutions may also be open source or offer free tiers, so you can try them without a financial commitment. Other commercial SDK generators are available and can be found online. đŸ€“ diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index da7e270de..f890dbcd9 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -52,12 +52,6 @@
-
- - - - -
From e680fd6c8c1c7e3682c9e02d675a9b4c643493e8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Jun 2026 22:59:54 +0000 Subject: [PATCH 016/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 235fe8605..a29341857 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* 🔧 Update sponsors: remove Liblab. PR [#15731](https://github.com/fastapi/fastapi/pull/15731) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: remove Scalar. PR [#15730](https://github.com/fastapi/fastapi/pull/15730) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump the python-packages group across 1 directory with 6 updates. PR [#15721](https://github.com/fastapi/fastapi/pull/15721) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump python-multipart from 0.0.29 to 0.0.30. PR [#15723](https://github.com/fastapi/fastapi/pull/15723) by [@dependabot[bot]](https://github.com/apps/dependabot). From 5d2dae82c4c6ff107147167d70d4fdbd35bc0706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 11 Jun 2026 01:04:41 +0200 Subject: [PATCH 017/188] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors:=20rem?= =?UTF-8?q?ove=20InterviewPal=20(#15735)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 - docs/en/data/sponsors.yml | 3 --- 2 files changed, 4 deletions(-) diff --git a/README.md b/README.md index 9936ab88a..7f8327609 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ The key features are: - diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index e72cbbd9f..9c118e0e7 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -40,9 +40,6 @@ silver: - url: https://www.permit.io/blog/implement-authorization-in-fastapi?utm_source=github&utm_medium=referral&utm_campaign=fastapi title: Fine-Grained Authorization for FastAPI img: https://fastapi.tiangolo.com/img/sponsors/permit.png - - url: https://www.interviewpal.com/?utm_source=fastapi&utm_medium=open-source&utm_campaign=dev-hiring - title: InterviewPal - AI Interview Coach for Engineers and Devs - img: https://fastapi.tiangolo.com/img/sponsors/interviewpal.png - url: https://dribia.com/en/ title: Dribia - Data Science within your reach img: https://fastapi.tiangolo.com/img/sponsors/dribia.png From 944fb706228734cc64fd5f8d5937bc97792c346b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Jun 2026 23:05:07 +0000 Subject: [PATCH 018/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a29341857..904a2c40e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* 🔧 Update sponsors: remove InterviewPal. PR [#15735](https://github.com/fastapi/fastapi/pull/15735) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: remove Liblab. PR [#15731](https://github.com/fastapi/fastapi/pull/15731) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: remove Scalar. PR [#15730](https://github.com/fastapi/fastapi/pull/15730) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump the python-packages group across 1 directory with 6 updates. PR [#15721](https://github.com/fastapi/fastapi/pull/15721) by [@dependabot[bot]](https://github.com/apps/dependabot). From e4b6a36c4fc0d0b37871fe317ab29fe074a3cdbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 11 Jun 2026 01:13:56 +0200 Subject: [PATCH 019/188] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors:=20rem?= =?UTF-8?q?ove=20ExoFlare=20(#15736)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors.yml | 3 --- docs/en/docs/fastapi-people.md | 10 ---------- 2 files changed, 13 deletions(-) diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 9c118e0e7..0c8e221c8 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -50,9 +50,6 @@ silver: title: Try RapidProxy for free - Residential Proxies with 90M+ Global IPs. Starting from $0.65/GB for web scraping, automation, and data collection. img: https://fastapi.tiangolo.com/img/sponsors/rapidproxy.png bronze: - - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source - title: Biosecurity risk assessments made easy. - img: https://fastapi.tiangolo.com/img/sponsors/exoflare.png # - url: https://testdriven.io/courses/tdd-fastapi/ # title: Learn to build high-quality web apps with best practices # img: https://fastapi.tiangolo.com/img/sponsors/testdriven.svg diff --git a/docs/en/docs/fastapi-people.md b/docs/en/docs/fastapi-people.md index ad32966e5..f8e462821 100644 --- a/docs/en/docs/fastapi-people.md +++ b/docs/en/docs/fastapi-people.md @@ -250,16 +250,6 @@ They are supporting my work with **FastAPI** (and others), mainly through [GitHu {% endfor %} {% endif %} - -{% if sponsors.bronze %} - -### Bronze Sponsors - -{% for sponsor in sponsors.bronze -%} - -{% endfor %} -{% endif %} - {% endif %} ### Individual Sponsors From d3e6a2931f95dcb044c09cbfc39e4b0e9e620fd4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Jun 2026 23:14:26 +0000 Subject: [PATCH 020/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 904a2c40e..b0b31ad5b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* 🔧 Update sponsors: remove ExoFlare. PR [#15736](https://github.com/fastapi/fastapi/pull/15736) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: remove InterviewPal. PR [#15735](https://github.com/fastapi/fastapi/pull/15735) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: remove Liblab. PR [#15731](https://github.com/fastapi/fastapi/pull/15731) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: remove Scalar. PR [#15730](https://github.com/fastapi/fastapi/pull/15730) by [@tiangolo](https://github.com/tiangolo). From e2fcd5562f41f2a0d01b1ff43773711bbeeb19e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2026 20:22:22 +0200 Subject: [PATCH 021/188] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors:=20rem?= =?UTF-8?q?ove=20TalorData=20(#15744)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 - docs/en/data/sponsors.yml | 3 --- 2 files changed, 4 deletions(-) diff --git a/README.md b/README.md index 7f8327609..5480887cd 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,6 @@ The key features are: - diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 0c8e221c8..671ede5e8 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -43,9 +43,6 @@ silver: - url: https://dribia.com/en/ title: Dribia - Data Science within your reach img: https://fastapi.tiangolo.com/img/sponsors/dribia.png - - url: https://talordata.com/?campaignid=oh5dVZ3Zc3YGiAI2&utm_source=fastapi&utm_term=fastapi - title: TalorData SERP API - Multi-Engine Search Results Data - img: https://fastapi.tiangolo.com/img/sponsors/talordata.png - url: https://www.rapidproxy.io/?ref=fastapi title: Try RapidProxy for free - Residential Proxies with 90M+ Global IPs. Starting from $0.65/GB for web scraping, automation, and data collection. img: https://fastapi.tiangolo.com/img/sponsors/rapidproxy.png From 016ab760faba1813ba235d5582a014a58bd2f417 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 13 Jun 2026 18:22:48 +0000 Subject: [PATCH 022/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b0b31ad5b..5303b55df 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Internal +* 🔧 Update sponsors: remove TalorData. PR [#15744](https://github.com/fastapi/fastapi/pull/15744) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: remove ExoFlare. PR [#15736](https://github.com/fastapi/fastapi/pull/15736) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: remove InterviewPal. PR [#15735](https://github.com/fastapi/fastapi/pull/15735) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: remove Liblab. PR [#15731](https://github.com/fastapi/fastapi/pull/15731) by [@tiangolo](https://github.com/tiangolo). From 8e1d774cef03ab9e2552c26e850cbfc0c63974c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 14 Jun 2026 14:35:48 +0200 Subject: [PATCH 023/188] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20interna?= =?UTF-8?q?ls=20to=20preserve=20`APIRouter`=20and=20`APIRoute`=20instances?= =?UTF-8?q?=20(#15745)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/openapi-callbacks.md | 4 +- .../path-operation-advanced-configuration.md | 12 +- docs/en/docs/how-to/extending-openapi.md | 10 +- docs/en/docs/release-notes.md | 49 + docs/en/docs/tutorial/bigger-applications.md | 22 +- .../tutorial002_py310.py | 22 +- fastapi/applications.py | 5 +- fastapi/openapi/utils.py | 73 +- fastapi/routing.py | 1130 +++++++++++++---- tests/test_custom_route_class.py | 11 +- tests/test_router_include_context.py | 855 +++++++++++++ 11 files changed, 1877 insertions(+), 316 deletions(-) create mode 100644 tests/test_router_include_context.py diff --git a/docs/en/docs/advanced/openapi-callbacks.md b/docs/en/docs/advanced/openapi-callbacks.md index 40cf47956..819709110 100644 --- a/docs/en/docs/advanced/openapi-callbacks.md +++ b/docs/en/docs/advanced/openapi-callbacks.md @@ -167,13 +167,13 @@ Notice how the callback URL used contains the URL received as a query parameter At this point you have the *callback path operation(s)* needed (the one(s) that the *external developer* should implement in the *external API*) in the callback router you created above. -Now use the parameter `callbacks` in *your API's path operation decorator* to pass the attribute `.routes` (that's actually just a `list` of routes/*path operations*) from that callback router: +Now use the parameter `callbacks` in *your API's path operation decorator* to pass the attribute `.routes` from that callback router: {* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} /// tip -Notice that you are not passing the router itself (`invoices_callback_router`) to `callback=`, but the attribute `.routes`, as in `invoices_callback_router.routes`. +Notice that you are not passing the router itself (`invoices_callback_router`) to `callbacks=`, but its `.routes`, as in `invoices_callback_router.routes`. FastAPI will use those routes to generate the callback OpenAPI documentation. /// diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index 800bf305d..9dca4d712 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -16,17 +16,11 @@ You would have to make sure that it is unique for each operation. ### Using the *path operation function* name as the operationId { #using-the-path-operation-function-name-as-the-operationid } -If you want to use your APIs' function names as `operationId`s, you can iterate over all of them and override each *path operation's* `operation_id` using their `APIRoute.name`. +If you want to use your APIs' function names as `operationId`s, you can pass a custom `generate_unique_id_function` to `FastAPI`. -You should do it after adding all your *path operations*. +The function receives each `APIRoute` and returns the `operationId` to use for that path operation. -{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py310.py hl[2, 12:21, 24] *} - -/// tip - -If you manually call `app.openapi()`, you should update the `operationId`s before that. - -/// +{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py310.py hl[2,5:6,9] *} /// warning diff --git a/docs/en/docs/how-to/extending-openapi.md b/docs/en/docs/how-to/extending-openapi.md index 65f584438..8368eea50 100644 --- a/docs/en/docs/how-to/extending-openapi.md +++ b/docs/en/docs/how-to/extending-openapi.md @@ -25,7 +25,15 @@ And that function `get_openapi()` receives as parameters: * `openapi_version`: The version of the OpenAPI specification used. By default, the latest: `3.1.0`. * `summary`: A short summary of the API. * `description`: The description of your API, this can include markdown and will be shown in the docs. -* `routes`: A list of routes, these are each of the registered *path operations*. They are taken from `app.routes`. +* `routes`: The routes from the application, taken from `app.routes`. FastAPI uses them to collect the registered *path operations*, including those from included routers. + +/// tip | Technical Details + +`app.routes` is a lower-level route tree. It can include route candidates that FastAPI uses internally for included routers, not only final `APIRoute` objects. + +You can still pass `app.routes` to `get_openapi()`. FastAPI will traverse that route tree to collect the effective path operations. + +/// /// note diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5303b55df..101804507 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,55 @@ hide: ## Latest Changes +♻ Refactor internals to preserve `APIRouter` and `APIRoute` instances + +Unblocks ✹ SO MANY THINGS ✹ + +Before this, `router.include_router(other_router)` would take each path operation from `other_router` and "clone" it, or recreate it from scratch. + +This would mean that in the end there was only one top level router, part of the app. + +The way it is structured here is that there are a few additional classes to handle intermediate metadata for router and route inclusion. That way the information of "router X includes Y and Y includes Z" is stored somewhere, without affecting (recreating / clonning) the final route. + +#### Non Objectives + +Dependencies for 404: previously I intended to support dependencies that would be executed even for 404, but that would conflict with the fact that a router could _not_ find a match, but the next router _did_ find a match. Executing dependencies in the router that did not find a match would not make sense, they could consume the request, body, etc. This original idea was discarded. + +#### Breaking Changes + +Now `router.routes` is no longer a plain list of `APIRoute` objects, it can contain these intermediate objects that can contain additional routers, forming a tree. + +Any logic that depended on iterating on the `router.routes` directly would be affected, that logic cannot expect to be able to extract data from a plain list of routes, as it's no longer a plain list but a tree. + +Additionally, any logic that iterated on `router.routes` to modify them would now also see these new objects, and would not see all the routes in the app. + +`router.routes` should be considered an internal implementation detail, only passed around to the FastAPI functions that need it. + +#### Features + +* Adding routes (path operations) after a router is included now works, they are reflected as they are not copied. +* Including `subrouter` in `mainrouter` can be done before adding routes (path operations) to `subrouter`, because now the the entire object is stored instead of copying the routes. +* As routes are not copied, in some cases that might save some memory. + +#### Alpha Features + +This is not documented yet, so it's not officially supported yet and could change in the future. + +But, as `APIRoute` and `APIRouter` instances are now preserved, they could be customized. + +`APIRouter` has two new methods, `.matches()` and `.handle()`, counterpart to the existing ones in `APIRoute`. With this a router could customize how it matches and handles requests. For example, it could match only requests that include some specific header, for example for handling versions in headers. + +Still, for now, consider this very experimental and potentially changing and breaking in the future. + +#### Future Features Enabled + +* Custom `APIRoute` subclasses (undocumented, but alraedy works as desccribed above) +* Custom `APIRouter` subclasses (undocumented, but already works as described above) +* Dependencies per router +* Exception handlers per router +* Middleware per router +* Other features planned + ### Docs * 📝 Update FastAPI Cloud deployment instructions. PR [#15724](https://github.com/fastapi/fastapi/pull/15724) by [@alejsdev](https://github.com/alejsdev). diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index 8950d59b4..b1a6afb6a 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -396,9 +396,9 @@ It will include all the routes from that router as part of it. /// note | Technical Details -It will actually internally create a *path operation* for each *path operation* that was declared in the `APIRouter`. +FastAPI keeps the original `APIRouter` and its `APIRoute`s active when the router is included in the main application. -So, behind the scenes, it will actually work as if everything was the same single app. +That means custom `APIRouter` and `APIRoute` subclasses can still participate after the router is included. /// @@ -406,7 +406,7 @@ So, behind the scenes, it will actually work as if everything was the same singl You don't have to worry about performance when including routers. -This will take microseconds and will only happen at startup. +This is designed to be lightweight and to avoid adding overhead to each request. So it won't affect performance. ⚡ @@ -461,7 +461,7 @@ The `APIRouter`s are not "mounted", they are not isolated from the rest of the a This is because we want to include their *path operations* in the OpenAPI schema and the user interfaces. -As we cannot just isolate them and "mount" them independently of the rest, the *path operations* are "cloned" (re-created), not included directly. +FastAPI keeps the original routers and path operations active, and combines the router prefixes, dependencies, tags, responses, and other metadata when handling requests and generating OpenAPI. /// @@ -532,4 +532,16 @@ The same way you can include an `APIRouter` in a `FastAPI` application, you can router.include_router(other_router) ``` -Make sure you do it before including `router` in the `FastAPI` app, so that the *path operations* from `other_router` are also included. +You can do this before or after including `router` in the `FastAPI` app. FastAPI will still include the *path operations* from `other_router` in routing and OpenAPI. + +The same applies to *path operations* added later to the routers. They will be visible through the earlier inclusion too. + +/// warning | Technical Details + +Avoid directly mutating `router.routes` after including a router. FastAPI treats router inclusion as live, so the original router and its routes remain part of routing and OpenAPI generation. + +Use documented APIs such as path operation decorators and `.include_router()` to add routes and routers. + +Treat `router.routes` as a lower-level route tree that can contain route definitions and included routers, and avoid relying on it as a flat list of final path operations. + +/// diff --git a/docs_src/path_operation_advanced_configuration/tutorial002_py310.py b/docs_src/path_operation_advanced_configuration/tutorial002_py310.py index 3aaae9b37..5c2257ed6 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial002_py310.py +++ b/docs_src/path_operation_advanced_configuration/tutorial002_py310.py @@ -1,24 +1,14 @@ from fastapi import FastAPI from fastapi.routing import APIRoute -app = FastAPI() - -@app.get("/items/") -async def read_items(): - return [{"item_id": "Foo"}] +def custom_generate_unique_id(route: APIRoute) -> str: + return route.name -def use_route_names_as_operation_ids(app: FastAPI) -> None: - """ - Simplify operation IDs so that generated API clients have simpler function - names. +app = FastAPI(generate_unique_id_function=custom_generate_unique_id) - Should be called only after all routes have been added. - """ - for route in app.routes: - if isinstance(route, APIRoute): - route.operation_id = route.name # in this case, 'read_items' - -use_route_names_as_operation_ids(app) +@app.get("/items/") +async def read_items(): + return [{"item_id": "Foo"}] diff --git a/fastapi/applications.py b/fastapi/applications.py index faac6853f..c7c551e4e 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -921,6 +921,7 @@ class FastAPI(Starlette): ), ] = "3.1.0" self.openapi_schema: dict[str, Any] | None = None + self._openapi_routes_version: int | None = None if self.openapi_url: assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'" assert self.version, "A version must be provided for OpenAPI, e.g.: '2.1.0'" @@ -1079,7 +1080,8 @@ class FastAPI(Starlette): Read more in the [FastAPI docs for OpenAPI](https://fastapi.tiangolo.com/how-to/extending-openapi/). """ - if not self.openapi_schema: + routes_version = self.router._get_routes_version() + if not self.openapi_schema or self._openapi_routes_version != routes_version: self.openapi_schema = get_openapi( title=self.title, version=self.version, @@ -1096,6 +1098,7 @@ class FastAPI(Starlette): separate_input_output_schemas=self.separate_input_output_schemas, external_docs=self.openapi_external_docs, ) + self._openapi_routes_version = routes_version return self.openapi_schema def setup(self) -> None: diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 1c7a17c4c..ab4543d34 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -213,7 +213,7 @@ def get_openapi_operation_request_body( def generate_operation_id( - *, route: routing.APIRoute, method: str + *, route: routing._APIRouteLike, method: str ) -> str: # pragma: nocover warnings.warn( message="fastapi.openapi.utils.generate_operation_id() was deprecated, " @@ -227,14 +227,14 @@ def generate_operation_id( return generate_operation_id_for_path(name=route.name, path=path, method=method) -def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str: +def generate_operation_summary(*, route: routing._APIRouteLike, method: str) -> str: if route.summary: return route.summary return route.name.replace("_", " ").title() def get_openapi_operation_metadata( - *, route: routing.APIRoute, method: str, operation_ids: set[str] + *, route: routing._APIRouteLike, method: str, operation_ids: set[str] ) -> dict[str, Any]: operation: dict[str, Any] = {} if route.tags: @@ -259,7 +259,7 @@ def get_openapi_operation_metadata( def get_openapi_path( *, - route: routing.APIRoute, + route: routing._APIRouteLike, operation_ids: set[str], model_name_map: ModelNameMap, field_mapping: dict[ @@ -329,7 +329,7 @@ def get_openapi_path( cb_security_schemes, cb_definitions, ) = get_openapi_path( - route=callback, + route=cast(routing._APIRouteLike, callback), operation_ids=operation_ids, model_name_map=model_name_map, field_mapping=field_mapping, @@ -478,6 +478,18 @@ def get_openapi_path( return path, security_schemes, definitions +def _get_api_route_for_openapi( + route: BaseRoute, route_context: routing._EffectiveRouteContext | None +) -> routing._APIRouteLike | None: + if route_context is not None and isinstance( + route_context.original_route, routing.APIRoute + ): + return cast(routing._APIRouteLike, route_context) + if isinstance(route, routing.APIRoute): + return cast(routing._APIRouteLike, route) + return None + + def get_fields_from_routes( routes: Sequence[BaseRoute], ) -> list[ModelField]: @@ -485,24 +497,25 @@ def get_fields_from_routes( responses_from_routes: list[ModelField] = [] request_fields_from_routes: list[ModelField] = [] callback_flat_models: list[ModelField] = [] - for route in routes: - if not isinstance(route, routing.APIRoute): + for route, route_context in routing._iter_routes_with_context(routes): + api_route = _get_api_route_for_openapi(route, route_context) + if api_route is None: continue - if route.include_in_schema: - if route.body_field: - assert isinstance(route.body_field, ModelField), ( + if api_route.include_in_schema: + if api_route.body_field: + assert isinstance(api_route.body_field, ModelField), ( "A request body must be a Pydantic Field" ) - body_fields_from_routes.append(route.body_field) - if route.response_field: - responses_from_routes.append(route.response_field) - if route.response_fields: - responses_from_routes.extend(route.response_fields.values()) - if route.stream_item_field: - responses_from_routes.append(route.stream_item_field) - if route.callbacks: - callback_flat_models.extend(get_fields_from_routes(route.callbacks)) - params = get_flat_params(route.dependant) + body_fields_from_routes.append(api_route.body_field) + if api_route.response_field: + responses_from_routes.append(api_route.response_field) + if api_route.response_fields: + responses_from_routes.extend(api_route.response_fields.values()) + if api_route.stream_item_field: + responses_from_routes.append(api_route.stream_item_field) + if api_route.callbacks: + callback_flat_models.extend(get_fields_from_routes(api_route.callbacks)) + params = get_flat_params(api_route.dependant) request_fields_from_routes.extend(params) flat_models = callback_flat_models + list( @@ -546,7 +559,7 @@ def get_openapi( paths: dict[str, dict[str, Any]] = {} webhook_paths: dict[str, dict[str, Any]] = {} operation_ids: set[str] = set() - all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or [])) + all_fields = get_fields_from_routes(list(routes) + list(webhooks or [])) flat_models = get_flat_models_from_fields(all_fields, known_models=set()) model_name_map = get_model_name_map(flat_models) field_mapping, definitions = get_definitions( @@ -554,10 +567,11 @@ def get_openapi( model_name_map=model_name_map, separate_input_output_schemas=separate_input_output_schemas, ) - for route in routes or []: - if isinstance(route, routing.APIRoute): + for route, route_context in routing._iter_routes_with_context(routes): + api_route = _get_api_route_for_openapi(route, route_context) + if api_route is not None: result = get_openapi_path( - route=route, + route=api_route, operation_ids=operation_ids, model_name_map=model_name_map, field_mapping=field_mapping, @@ -566,17 +580,18 @@ def get_openapi( if result: path, security_schemes, path_definitions = result if path: - paths.setdefault(route.path_format, {}).update(path) + paths.setdefault(api_route.path_format, {}).update(path) if security_schemes: components.setdefault("securitySchemes", {}).update( security_schemes ) if path_definitions: definitions.update(path_definitions) - for webhook in webhooks or []: - if isinstance(webhook, routing.APIRoute): + for webhook, webhook_context in routing._iter_routes_with_context(webhooks or []): + api_webhook = _get_api_route_for_openapi(webhook, webhook_context) + if api_webhook is not None: result = get_openapi_path( - route=webhook, + route=api_webhook, operation_ids=operation_ids, model_name_map=model_name_map, field_mapping=field_mapping, @@ -585,7 +600,7 @@ def get_openapi( if result: path, security_schemes, path_definitions = result if path: - webhook_paths.setdefault(webhook.path_format, {}).update(path) + webhook_paths.setdefault(api_webhook.path_format, {}).update(path) if security_schemes: components.setdefault("securitySchemes", {}).update( security_schemes diff --git a/fastapi/routing.py b/fastapi/routing.py index 21a1385a2..fb4784309 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1,4 +1,5 @@ import contextlib +import copy import email.message import functools import inspect @@ -21,10 +22,13 @@ from contextlib import ( AsyncExitStack, asynccontextmanager, ) +from contextvars import ContextVar +from dataclasses import dataclass, field from enum import Enum, IntEnum from typing import ( Annotated, Any, + Protocol, TypeVar, cast, ) @@ -74,12 +78,17 @@ from fastapi.utils import ( ) from starlette import routing from starlette._exception_handler import wrap_app_handling_exceptions -from starlette._utils import is_async_callable +from starlette._utils import get_route_path, is_async_callable from starlette.concurrency import iterate_in_threadpool, run_in_threadpool -from starlette.datastructures import FormData +from starlette.datastructures import FormData, URLPath from starlette.exceptions import HTTPException from starlette.requests import Request -from starlette.responses import JSONResponse, Response, StreamingResponse +from starlette.responses import ( + JSONResponse, + PlainTextResponse, + Response, + StreamingResponse, +) from starlette.routing import ( BaseRoute, Match, @@ -808,6 +817,250 @@ class APIWebSocketRoute(routing.WebSocketRoute): return match, child_scope +_FASTAPI_SCOPE_KEY = "fastapi" +_FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY = "effective_route_context" +_FASTAPI_INCLUDED_ROUTER_KEY = "included_router" +_effective_route_context_var: ContextVar[Any | None] = ContextVar( + "fastapi_effective_route_context", default=None +) +_SCOPE_MISSING = object() + + +def _get_fastapi_scope(scope: Scope) -> dict[str, Any]: + fastapi_scope = scope.setdefault(_FASTAPI_SCOPE_KEY, {}) + assert isinstance(fastapi_scope, dict) + return fastapi_scope + + +def _get_scope_effective_route_context(scope: Scope) -> Any | None: + return scope.get(_FASTAPI_SCOPE_KEY, {}).get(_FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY) + + +def _get_scope_included_router(scope: Scope) -> Any | None: + return scope.get(_FASTAPI_SCOPE_KEY, {}).get(_FASTAPI_INCLUDED_ROUTER_KEY) + + +def _restore_fastapi_scope_key(scope: Scope, key: str, previous: Any) -> None: + fastapi_scope = scope.get(_FASTAPI_SCOPE_KEY) + if not isinstance(fastapi_scope, dict): + return + if previous is _SCOPE_MISSING: + fastapi_scope.pop(key, None) + else: + fastapi_scope[key] = previous + + +class _APIRouteLike(Protocol): + path: str + endpoint: Callable[..., Any] + stream_item_type: Any | None + response_model: Any + summary: str | None + response_description: str + deprecated: bool | None + operation_id: str | None + response_model_include: IncEx | None + response_model_exclude: IncEx | None + response_model_by_alias: bool + response_model_exclude_unset: bool + response_model_exclude_defaults: bool + response_model_exclude_none: bool + include_in_schema: bool + response_class: type[Response] | DefaultPlaceholder + dependency_overrides_provider: Any | None + callbacks: list[BaseRoute] | None + openapi_extra: dict[str, Any] | None + generate_unique_id_function: Callable[[Any], str] | DefaultPlaceholder + strict_content_type: bool | DefaultPlaceholder + tags: list[str | Enum] + responses: dict[int | str, dict[str, Any]] + name: str + path_regex: Any + path_format: str + param_convertors: dict[str, Any] + methods: set[str] + unique_id: str + status_code: int | None + response_field: ModelField | None + stream_item_field: ModelField | None + dependencies: list[params.Depends] + description: str + response_fields: dict[int | str, ModelField] + dependant: Dependant + _flat_dependant: Dependant + _embed_body_fields: bool + body_field: ModelField | None + is_sse_stream: bool + is_json_stream: bool + + +def _populate_api_route_state( + route: _APIRouteLike, + path: str, + endpoint: Callable[..., Any], + *, + response_model: Any = Default(None), + status_code: int | None = None, + tags: list[str | Enum] | None = None, + dependencies: Sequence[params.Depends] | None = None, + summary: str | None = None, + description: str | None = None, + response_description: str = "Successful Response", + responses: dict[int | str, dict[str, Any]] | None = None, + deprecated: bool | None = None, + name: str | None = None, + methods: set[str] | list[str] | None = None, + operation_id: str | None = None, + response_model_include: IncEx | None = None, + response_model_exclude: IncEx | None = None, + response_model_by_alias: bool = True, + response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, + include_in_schema: bool = True, + response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse), + dependency_overrides_provider: Any | None = None, + callbacks: list[BaseRoute] | None = None, + openapi_extra: dict[str, Any] | None = None, + generate_unique_id_function: Callable[[Any], str] | DefaultPlaceholder = Default( + generate_unique_id + ), + strict_content_type: bool | DefaultPlaceholder = Default(True), +) -> None: + route.path = path + route.endpoint = endpoint + route.stream_item_type = None + if isinstance(response_model, DefaultPlaceholder): + return_annotation = get_typed_return_annotation(endpoint) + if lenient_issubclass(return_annotation, Response): + response_model = None + else: + stream_item = get_stream_item_type(return_annotation) + if stream_item is not None: + # Extract item type for JSONL or SSE streaming when + # response_class is DefaultPlaceholder (JSONL) or + # EventSourceResponse (SSE). + # ServerSentEvent is excluded: it's a transport + # wrapper, not a data model, so it shouldn't feed + # into validation or OpenAPI schema generation. + if ( + isinstance(response_class, DefaultPlaceholder) + or lenient_issubclass(response_class, EventSourceResponse) + ) and not lenient_issubclass(stream_item, ServerSentEvent): + route.stream_item_type = stream_item + response_model = None + else: + response_model = return_annotation + route.response_model = response_model + route.summary = summary + route.response_description = response_description + route.deprecated = deprecated + route.operation_id = operation_id + route.response_model_include = response_model_include + route.response_model_exclude = response_model_exclude + route.response_model_by_alias = response_model_by_alias + route.response_model_exclude_unset = response_model_exclude_unset + route.response_model_exclude_defaults = response_model_exclude_defaults + route.response_model_exclude_none = response_model_exclude_none + route.include_in_schema = include_in_schema + route.response_class = response_class + route.dependency_overrides_provider = dependency_overrides_provider + route.callbacks = callbacks + route.openapi_extra = openapi_extra + route.generate_unique_id_function = generate_unique_id_function + route.strict_content_type = strict_content_type + route.tags = tags or [] + route.responses = responses or {} + route.name = get_name(endpoint) if name is None else name + route.path_regex, route.path_format, route.param_convertors = compile_path(path) + if methods is None: + methods = ["GET"] + route.methods = {method.upper() for method in methods} + if isinstance(generate_unique_id_function, DefaultPlaceholder): + current_generate_unique_id: Callable[[Any], str] = ( + generate_unique_id_function.value + ) + else: + current_generate_unique_id = generate_unique_id_function + route.unique_id = route.operation_id or current_generate_unique_id(route) + # normalize enums e.g. http.HTTPStatus + if isinstance(status_code, IntEnum): + status_code = int(status_code) + route.status_code = status_code + if route.response_model: + assert is_body_allowed_for_status_code(status_code), ( + f"Status code {status_code} must not have a response body" + ) + response_name = "Response_" + route.unique_id + route.response_field = create_model_field( + name=response_name, + type_=route.response_model, + mode="serialization", + ) + else: + route.response_field = None + if route.stream_item_type: + stream_item_name = "StreamItem_" + route.unique_id + route.stream_item_field = create_model_field( + name=stream_item_name, + type_=route.stream_item_type, + mode="serialization", + ) + else: + route.stream_item_field = None + route.dependencies = list(dependencies or []) + route.description = description or inspect.cleandoc(route.endpoint.__doc__ or "") + # if a "form feed" character (page break) is found in the description text, + # truncate description text to the content preceding the first "form feed" + route.description = route.description.split("\f")[0].strip() + response_fields = {} + for additional_status_code, response in route.responses.items(): + assert isinstance(response, dict), "An additional response must be a dict" + model = response.get("model") + if model: + assert is_body_allowed_for_status_code(additional_status_code), ( + f"Status code {additional_status_code} must not have a response body" + ) + response_name = f"Response_{additional_status_code}_{route.unique_id}" + response_field = create_model_field( + name=response_name, type_=model, mode="serialization" + ) + response_fields[additional_status_code] = response_field + if response_fields: + route.response_fields = response_fields + else: + route.response_fields = {} + + assert callable(endpoint), "An endpoint must be a callable" + route.dependant = get_dependant( + path=route.path_format, call=route.endpoint, scope="function" + ) + for depends in route.dependencies[::-1]: + route.dependant.dependencies.insert( + 0, + get_parameterless_sub_dependant(depends=depends, path=route.path_format), + ) + route._flat_dependant = get_flat_dependant(route.dependant) + route._embed_body_fields = _should_embed_body_fields( + route._flat_dependant.body_params + ) + route.body_field = get_body_field( + flat_dependant=route._flat_dependant, + name=route.unique_id, + embed_body_fields=route._embed_body_fields, + ) + # Detect generator endpoints that should stream as JSONL or SSE + is_generator = ( + route.dependant.is_async_gen_callable or route.dependant.is_gen_callable + ) + route.is_sse_stream = is_generator and lenient_issubclass( + response_class, EventSourceResponse + ) + route.is_json_stream = is_generator and isinstance( + response_class, DefaultPlaceholder + ) + + class APIRoute(routing.Route): def __init__( self, @@ -841,166 +1094,541 @@ class APIRoute(routing.Route): | DefaultPlaceholder = Default(generate_unique_id), strict_content_type: bool | DefaultPlaceholder = Default(True), ) -> None: - self.path = path - self.endpoint = endpoint - self.stream_item_type: Any | None = None - if isinstance(response_model, DefaultPlaceholder): - return_annotation = get_typed_return_annotation(endpoint) - if lenient_issubclass(return_annotation, Response): - response_model = None - else: - stream_item = get_stream_item_type(return_annotation) - if stream_item is not None: - # Extract item type for JSONL or SSE streaming when - # response_class is DefaultPlaceholder (JSONL) or - # EventSourceResponse (SSE). - # ServerSentEvent is excluded: it's a transport - # wrapper, not a data model, so it shouldn't feed - # into validation or OpenAPI schema generation. - if ( - isinstance(response_class, DefaultPlaceholder) - or lenient_issubclass(response_class, EventSourceResponse) - ) and not lenient_issubclass(stream_item, ServerSentEvent): - self.stream_item_type = stream_item - response_model = None - else: - response_model = return_annotation - self.response_model = response_model - self.summary = summary - self.response_description = response_description - self.deprecated = deprecated - self.operation_id = operation_id - self.response_model_include = response_model_include - self.response_model_exclude = response_model_exclude - self.response_model_by_alias = response_model_by_alias - self.response_model_exclude_unset = response_model_exclude_unset - self.response_model_exclude_defaults = response_model_exclude_defaults - self.response_model_exclude_none = response_model_exclude_none - self.include_in_schema = include_in_schema - self.response_class = response_class - self.dependency_overrides_provider = dependency_overrides_provider - self.callbacks = callbacks - self.openapi_extra = openapi_extra - self.generate_unique_id_function = generate_unique_id_function - self.strict_content_type = strict_content_type - self.tags = tags or [] - self.responses = responses or {} - self.name = get_name(endpoint) if name is None else name - self.path_regex, self.path_format, self.param_convertors = compile_path(path) - if methods is None: - methods = ["GET"] - self.methods: set[str] = {method.upper() for method in methods} - if isinstance(generate_unique_id_function, DefaultPlaceholder): - current_generate_unique_id: Callable[[APIRoute], str] = ( - generate_unique_id_function.value - ) - else: - current_generate_unique_id = generate_unique_id_function - self.unique_id = self.operation_id or current_generate_unique_id(self) - # normalize enums e.g. http.HTTPStatus - if isinstance(status_code, IntEnum): - status_code = int(status_code) - self.status_code = status_code - if self.response_model: - assert is_body_allowed_for_status_code(status_code), ( - f"Status code {status_code} must not have a response body" - ) - response_name = "Response_" + self.unique_id - self.response_field = create_model_field( - name=response_name, - type_=self.response_model, - mode="serialization", - ) - else: - self.response_field = None # type: ignore[assignment] - if self.stream_item_type: - stream_item_name = "StreamItem_" + self.unique_id - self.stream_item_field: ModelField | None = create_model_field( - name=stream_item_name, - type_=self.stream_item_type, - mode="serialization", - ) - else: - self.stream_item_field = None - self.dependencies = list(dependencies or []) - self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "") - # if a "form feed" character (page break) is found in the description text, - # truncate description text to the content preceding the first "form feed" - self.description = self.description.split("\f")[0].strip() - response_fields = {} - for additional_status_code, response in self.responses.items(): - assert isinstance(response, dict), "An additional response must be a dict" - model = response.get("model") - if model: - assert is_body_allowed_for_status_code(additional_status_code), ( - f"Status code {additional_status_code} must not have a response body" - ) - response_name = f"Response_{additional_status_code}_{self.unique_id}" - response_field = create_model_field( - name=response_name, type_=model, mode="serialization" - ) - response_fields[additional_status_code] = response_field - if response_fields: - self.response_fields: dict[int | str, ModelField] = response_fields - else: - self.response_fields = {} - - assert callable(endpoint), "An endpoint must be a callable" - self.dependant = get_dependant( - path=self.path_format, call=self.endpoint, scope="function" - ) - for depends in self.dependencies[::-1]: - self.dependant.dependencies.insert( - 0, - get_parameterless_sub_dependant(depends=depends, path=self.path_format), - ) - self._flat_dependant = get_flat_dependant(self.dependant) - self._embed_body_fields = _should_embed_body_fields( - self._flat_dependant.body_params - ) - self.body_field = get_body_field( - flat_dependant=self._flat_dependant, - name=self.unique_id, - embed_body_fields=self._embed_body_fields, - ) - # Detect generator endpoints that should stream as JSONL or SSE - is_generator = ( - self.dependant.is_async_gen_callable or self.dependant.is_gen_callable - ) - self.is_sse_stream = is_generator and lenient_issubclass( - response_class, EventSourceResponse - ) - self.is_json_stream = is_generator and isinstance( - response_class, DefaultPlaceholder + _populate_api_route_state( + cast(_APIRouteLike, self), + path, + endpoint, + response_model=response_model, + status_code=status_code, + tags=tags, + dependencies=dependencies, + summary=summary, + description=description, + response_description=response_description, + responses=responses, + deprecated=deprecated, + name=name, + methods=methods, + operation_id=operation_id, + response_model_include=response_model_include, + response_model_exclude=response_model_exclude, + response_model_by_alias=response_model_by_alias, + response_model_exclude_unset=response_model_exclude_unset, + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, + include_in_schema=include_in_schema, + response_class=response_class, + dependency_overrides_provider=dependency_overrides_provider, + callbacks=callbacks, + openapi_extra=openapi_extra, + generate_unique_id_function=generate_unique_id_function, + strict_content_type=strict_content_type, ) self.app = request_response(self.get_route_handler()) def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]: + route = cast(_APIRouteLike, self) + # TODO: Replace or deprecate this no-scope hook so included-route + # effective context can be passed explicitly instead of via ContextVar. + effective_context = _effective_route_context_var.get() + if effective_context is not None and effective_context.original_route is self: + route = cast(_APIRouteLike, effective_context) return get_request_handler( - dependant=self.dependant, - body_field=self.body_field, - status_code=self.status_code, - response_class=self.response_class, - response_field=self.response_field, - response_model_include=self.response_model_include, - response_model_exclude=self.response_model_exclude, - response_model_by_alias=self.response_model_by_alias, - response_model_exclude_unset=self.response_model_exclude_unset, - response_model_exclude_defaults=self.response_model_exclude_defaults, - response_model_exclude_none=self.response_model_exclude_none, - dependency_overrides_provider=self.dependency_overrides_provider, - embed_body_fields=self._embed_body_fields, - strict_content_type=self.strict_content_type, - stream_item_field=self.stream_item_field, - is_json_stream=self.is_json_stream, + dependant=route.dependant, + body_field=route.body_field, + status_code=route.status_code, + response_class=route.response_class, + response_field=route.response_field, + response_model_include=route.response_model_include, + response_model_exclude=route.response_model_exclude, + response_model_by_alias=route.response_model_by_alias, + response_model_exclude_unset=route.response_model_exclude_unset, + response_model_exclude_defaults=route.response_model_exclude_defaults, + response_model_exclude_none=route.response_model_exclude_none, + dependency_overrides_provider=route.dependency_overrides_provider, + embed_body_fields=route._embed_body_fields, + strict_content_type=route.strict_content_type, + stream_item_field=route.stream_item_field, + is_json_stream=route.is_json_stream, ) def matches(self, scope: Scope) -> tuple[Match, Scope]: - match, child_scope = super().matches(scope) + effective_context = _get_scope_effective_route_context(scope) + if effective_context is not None and effective_context.original_route is self: + match, child_scope = effective_context.matches(scope) + else: + match, child_scope = super().matches(scope) if match != Match.NONE: child_scope["route"] = self return match, child_scope + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + effective_context = _get_scope_effective_route_context(scope) + if effective_context is not None and effective_context.original_route is self: + methods = effective_context.methods + if methods and scope["method"] not in methods: + headers = {"Allow": ", ".join(methods)} + if "app" in scope: + raise HTTPException(status_code=405, headers=headers) + response = PlainTextResponse( + "Method Not Allowed", status_code=405, headers=headers + ) + await response(scope, receive, send) + return + token = _effective_route_context_var.set(effective_context) + try: + app = request_response(self.get_route_handler()) + finally: + _effective_route_context_var.reset(token) + await app(scope, receive, send) + return + await super().handle(scope, receive, send) + + +@dataclass +class _RouterIncludeContext: + included_router: "APIRouter" + prefix: str = "" + tags: list[str | Enum] = field(default_factory=list) + dependencies: list[params.Depends] = field(default_factory=list) + default_response_class: type[Response] | DefaultPlaceholder = field( + default_factory=lambda: Default(JSONResponse) + ) + responses: dict[int | str, dict[str, Any]] = field(default_factory=dict) + callbacks: list[BaseRoute] = field(default_factory=list) + deprecated: bool | None = None + include_in_schema: bool = True + generate_unique_id_function: Callable[[APIRoute], str] | DefaultPlaceholder = field( + default_factory=lambda: Default(generate_unique_id) + ) + strict_content_type: bool | DefaultPlaceholder = field( + default_factory=lambda: Default(True) + ) + dependency_overrides_provider: Any | None = None + + @classmethod + def for_include( + cls, + *, + parent_router: "APIRouter", + included_router: "APIRouter", + prefix: str = "", + tags: list[str | Enum] | None = None, + dependencies: Sequence[params.Depends] | None = None, + default_response_class: type[Response] | DefaultPlaceholder = Default( + JSONResponse + ), + responses: dict[int | str, dict[str, Any]] | None = None, + callbacks: list[BaseRoute] | None = None, + deprecated: bool | None = None, + include_in_schema: bool = True, + generate_unique_id_function: Callable[[APIRoute], str] + | DefaultPlaceholder = Default(generate_unique_id), + ) -> "_RouterIncludeContext": + return cls( + included_router=included_router, + prefix=parent_router.prefix + prefix, + tags=[*parent_router.tags, *(tags or [])], + dependencies=[*parent_router.dependencies, *(dependencies or [])], + default_response_class=get_value_or_default( + default_response_class, parent_router.default_response_class + ), + responses={**parent_router.responses, **(responses or {})}, + callbacks=[*parent_router.callbacks, *(callbacks or [])], + deprecated=deprecated or parent_router.deprecated, + include_in_schema=parent_router.include_in_schema and include_in_schema, + generate_unique_id_function=get_value_or_default( + generate_unique_id_function, parent_router.generate_unique_id_function + ), + strict_content_type=parent_router.strict_content_type, + dependency_overrides_provider=parent_router.dependency_overrides_provider, + ) + + def combine( + self, child_context: "_RouterIncludeContext" + ) -> "_RouterIncludeContext": + return _RouterIncludeContext( + included_router=child_context.included_router, + prefix=self.prefix + child_context.prefix, + tags=[*self.tags, *child_context.tags], + dependencies=[*self.dependencies, *child_context.dependencies], + default_response_class=get_value_or_default( + child_context.default_response_class, self.default_response_class + ), + responses={**self.responses, **child_context.responses}, + callbacks=[*self.callbacks, *child_context.callbacks], + deprecated=self.deprecated or child_context.deprecated, + include_in_schema=self.include_in_schema + and child_context.include_in_schema, + generate_unique_id_function=get_value_or_default( + child_context.generate_unique_id_function, + self.generate_unique_id_function, + ), + strict_content_type=get_value_or_default( + child_context.strict_content_type, self.strict_content_type + ), + dependency_overrides_provider=self.dependency_overrides_provider, + ) + + def path_for( + self, route: APIRoute | routing.Route | routing.WebSocketRoute | routing.Mount + ) -> str: + return self.prefix + route.path + + +@dataclass +class _EffectiveRouteContext: + original_route: BaseRoute + starlette_route: BaseRoute | None = None + path: str = "" + endpoint: Callable[..., Any] | None = None + stream_item_type: Any | None = None + response_model: Any = None + summary: str | None = None + response_description: str = "Successful Response" + deprecated: bool | None = None + operation_id: str | None = None + response_model_include: IncEx | None = None + response_model_exclude: IncEx | None = None + response_model_by_alias: bool = True + response_model_exclude_unset: bool = False + response_model_exclude_defaults: bool = False + response_model_exclude_none: bool = False + include_in_schema: bool = True + response_class: type[Response] | DefaultPlaceholder = field( + default_factory=lambda: Default(JSONResponse) + ) + dependency_overrides_provider: Any | None = None + callbacks: list[BaseRoute] | None = None + openapi_extra: dict[str, Any] | None = None + generate_unique_id_function: Callable[[Any], str] | DefaultPlaceholder = field( + default_factory=lambda: Default(generate_unique_id) + ) + strict_content_type: bool | DefaultPlaceholder = field( + default_factory=lambda: Default(True) + ) + tags: list[str | Enum] = field(default_factory=list) + responses: dict[int | str, dict[str, Any]] = field(default_factory=dict) + name: str = "" + path_regex: Any = None + path_format: str = "" + param_convertors: dict[str, Any] = field(default_factory=dict) + methods: set[str] = field(default_factory=set) + unique_id: str = "" + status_code: int | None = None + response_field: ModelField | None = None + stream_item_field: ModelField | None = None + dependencies: list[params.Depends] = field(default_factory=list) + description: str = "" + response_fields: dict[int | str, ModelField] = field(default_factory=dict) + dependant: Dependant | None = None + _flat_dependant: Dependant | None = None + _embed_body_fields: bool = False + body_field: ModelField | None = None + is_sse_stream: bool = False + is_json_stream: bool = False + + @classmethod + def from_api_route( + cls, + *, + original_route: APIRoute, + include_context: _RouterIncludeContext, + ) -> "_EffectiveRouteContext": + route = cast(_APIRouteLike, original_route) + context = cls(original_route=original_route) + _populate_api_route_state( + cast(_APIRouteLike, context), + include_context.path_for(original_route), + route.endpoint, + response_model=route.response_model, + status_code=route.status_code, + tags=[*include_context.tags, *route.tags], + dependencies=[*include_context.dependencies, *route.dependencies], + summary=route.summary, + description=route.description, + response_description=route.response_description, + responses={**include_context.responses, **route.responses}, + deprecated=route.deprecated or include_context.deprecated, + methods=route.methods, + operation_id=route.operation_id, + response_model_include=route.response_model_include, + response_model_exclude=route.response_model_exclude, + response_model_by_alias=route.response_model_by_alias, + response_model_exclude_unset=route.response_model_exclude_unset, + response_model_exclude_defaults=route.response_model_exclude_defaults, + response_model_exclude_none=route.response_model_exclude_none, + include_in_schema=route.include_in_schema + and include_context.include_in_schema, + response_class=get_value_or_default( + route.response_class, + include_context.included_router.default_response_class, + include_context.default_response_class, + ), + name=route.name, + dependency_overrides_provider=include_context.dependency_overrides_provider, + callbacks=[*include_context.callbacks, *(route.callbacks or [])], + openapi_extra=route.openapi_extra, + generate_unique_id_function=get_value_or_default( + route.generate_unique_id_function, + include_context.included_router.generate_unique_id_function, + include_context.generate_unique_id_function, + ), + strict_content_type=get_value_or_default( + route.strict_content_type, + include_context.included_router.strict_content_type, + include_context.strict_content_type, + ), + ) + return context + + def matches(self, scope: Scope) -> tuple[Match, Scope]: + if not isinstance(self.original_route, APIRoute): + assert self.starlette_route is not None + return self.starlette_route.matches(scope) + if scope["type"] != "http": + return Match.NONE, {} + route_path = get_route_path(scope) + match = self.path_regex.match(route_path) + if not match: + return Match.NONE, {} + matched_params = match.groupdict() + for key, value in matched_params.items(): + matched_params[key] = self.param_convertors[key].convert(value) + path_params = dict(scope.get("path_params", {})) + path_params.update(matched_params) + child_scope = {"endpoint": self.endpoint, "path_params": path_params} + methods = self.methods + if methods and scope["method"] not in methods: + return Match.PARTIAL, child_scope + return Match.FULL, child_scope + + def url_path_for(self, name: str, /, **path_params: Any) -> Any: + if not isinstance(self.original_route, APIRoute): + assert self.starlette_route is not None + return self.starlette_route.url_path_for(name, **path_params) + seen_params = set(path_params.keys()) + param_convertors = self.param_convertors + expected_params = set(param_convertors.keys()) + if name != self.name or seen_params != expected_params: + raise routing.NoMatchFound(name, path_params) + path, remaining_params = routing.replace_params( + self.path_format, param_convertors, path_params + ) + assert not remaining_params + return URLPath(path=path, protocol="http") + + +@dataclass +class _IncludedRouter(BaseRoute): + original_router: "APIRouter" + include_context: _RouterIncludeContext + _effective_candidates: list["_EffectiveRouteContext | _IncludedRouter"] = field( + default_factory=list + ) + _effective_candidates_version: int | None = None + + def effective_candidates(self) -> list["_EffectiveRouteContext | _IncludedRouter"]: + routes_version = self.original_router._get_routes_version() + if routes_version == self._effective_candidates_version: + return self._effective_candidates + self._effective_candidates = [] + candidates = self.original_router.routes + for route in candidates: + if isinstance(route, _IncludedRouter): + child_context = self.include_context.combine(route.include_context) + child_branch = _IncludedRouter( + original_router=route.original_router, + include_context=child_context, + ) + self._effective_candidates.append(child_branch) + continue + route_context = self._build_effective_context(route) + if route_context is not None: + self._effective_candidates.append(route_context) + self._effective_candidates_version = routes_version + return self._effective_candidates + + def _build_effective_context( + self, route: BaseRoute + ) -> _EffectiveRouteContext | None: + if isinstance(route, APIRoute): + return _EffectiveRouteContext.from_api_route( + original_route=route, + include_context=self.include_context, + ) + if isinstance(route, routing.Route): + starlette_route: BaseRoute = routing.Route( + self.include_context.path_for(route), + endpoint=route.endpoint, + methods=list(route.methods or []), + name=route.name, + include_in_schema=route.include_in_schema, + ) + return _EffectiveRouteContext( + original_route=route, + starlette_route=starlette_route, + ) + if isinstance(route, APIWebSocketRoute): + starlette_route = APIWebSocketRoute( + self.include_context.path_for(route), + endpoint=route.endpoint, + name=route.name, + dependencies=[*self.include_context.dependencies, *route.dependencies], + dependency_overrides_provider=( + self.include_context.dependency_overrides_provider + ), + ) + return _EffectiveRouteContext( + original_route=route, + starlette_route=starlette_route, + ) + if isinstance(route, routing.WebSocketRoute): + starlette_route = routing.WebSocketRoute( + self.include_context.path_for(route), route.endpoint, name=route.name + ) + return _EffectiveRouteContext( + original_route=route, + starlette_route=starlette_route, + ) + if isinstance(route, routing.Mount): + starlette_route = copy.copy(route) + starlette_route.path = self.include_context.path_for(route).rstrip("/") + ( + starlette_route.path_regex, + starlette_route.path_format, + starlette_route.param_convertors, + ) = compile_path(starlette_route.path + "/{path:path}") + return _EffectiveRouteContext( + original_route=route, + starlette_route=starlette_route, + ) + if isinstance(route, routing.Host): + if self.include_context.prefix: + prefixed_app: ASGIApp = routing.Router( + routes=[routing.Mount(self.include_context.prefix, app=route.app)] + ) + else: + prefixed_app = route.app + starlette_route = routing.Host( + route.host, app=prefixed_app, name=route.name + ) + return _EffectiveRouteContext( + original_route=route, + starlette_route=starlette_route, + ) + return None + + def _match( + self, scope: Scope + ) -> tuple[Match, Scope, BaseRoute | None, _EffectiveRouteContext | None]: + partial: tuple[Scope, BaseRoute, _EffectiveRouteContext | None] | None = None + for candidate in self.effective_candidates(): + if isinstance(candidate, _IncludedRouter): + match, child_scope = candidate.matches(scope) + route: BaseRoute = candidate + route_context = None + elif isinstance(candidate.original_route, APIRoute): + route_context = candidate + fastapi_scope = _get_fastapi_scope(scope) + previous_context = fastapi_scope.get( + _FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY, _SCOPE_MISSING + ) + fastapi_scope[_FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY] = route_context + try: + match, child_scope = candidate.original_route.matches(scope) + finally: + _restore_fastapi_scope_key( + scope, _FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY, previous_context + ) + route = candidate.original_route + else: + route_context = candidate + match, child_scope = candidate.matches(scope) + route = candidate.starlette_route or candidate.original_route + if match == Match.FULL: + return match, child_scope, route, route_context + if match == Match.PARTIAL and partial is None: + partial = (child_scope, route, route_context) + if partial is not None: + child_scope, route, route_context = partial + return Match.PARTIAL, child_scope, route, route_context + return Match.NONE, {}, None, None + + def matches(self, scope: Scope) -> tuple[Match, Scope]: + fastapi_scope = _get_fastapi_scope(scope) + previous_router = fastapi_scope.get( + _FASTAPI_INCLUDED_ROUTER_KEY, _SCOPE_MISSING + ) + fastapi_scope[_FASTAPI_INCLUDED_ROUTER_KEY] = self + try: + match, _ = self.original_router.matches(scope) + return match, {} + finally: + _restore_fastapi_scope_key( + scope, _FASTAPI_INCLUDED_ROUTER_KEY, previous_router + ) + + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + _get_fastapi_scope(scope)[_FASTAPI_INCLUDED_ROUTER_KEY] = self + await self.original_router.handle(scope, receive, send) + + async def _handle_selected( + self, scope: Scope, receive: Receive, send: Send + ) -> None: + match, child_scope, route, effective_context = self._match(scope) + if match == Match.NONE or route is None: + await self.original_router.default(scope, receive, send) + return + scope.update(child_scope) + if isinstance(route, _IncludedRouter): + await route.handle(scope, receive, send) + return + if effective_context is not None: + _get_fastapi_scope(scope)[_FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY] = ( + effective_context + ) + original_route = effective_context.original_route + if isinstance(original_route, APIRoute): + scope["route"] = original_route + await original_route.handle(scope, receive, send) + return + await route.handle(scope, receive, send) + + def effective_route_contexts(self) -> Iterator[_EffectiveRouteContext]: + for candidate in self.effective_candidates(): + if isinstance(candidate, _IncludedRouter): + yield from candidate.effective_route_contexts() + else: + yield candidate + + def url_path_for(self, name: str, /, **path_params: Any) -> Any: + for route_context in self.effective_route_contexts(): + try: + return route_context.url_path_for(name, **path_params) + except routing.NoMatchFound: + pass + raise routing.NoMatchFound(name, path_params) + + +def _iter_included_route_candidates(routes: Sequence[BaseRoute]) -> Iterator[BaseRoute]: + for route, route_context in _iter_routes_with_context(routes): + if route_context is not None and route_context.starlette_route is not None: + yield route_context.starlette_route + else: + yield route + + +def _iter_routes_with_context( + routes: Sequence[BaseRoute], +) -> Iterator[tuple[BaseRoute, _EffectiveRouteContext | None]]: + for route in routes: + if isinstance(route, _IncludedRouter): + for route_context in route.effective_route_contexts(): + yield route_context.original_route, route_context + else: + yield route, None + class APIRouter(routing.Router): """ @@ -1313,6 +1941,87 @@ class APIRouter(routing.Router): self.default_response_class = default_response_class self.generate_unique_id_function = generate_unique_id_function self.strict_content_type = strict_content_type + self._routes_version = 0 + + def _mark_routes_changed(self) -> None: + self._routes_version += 1 + + def _get_routes_version(self, seen: set[int] | None = None) -> int: + if seen is None: + seen = set() + router_id = id(self) + if router_id in seen: + return self._routes_version + seen.add(router_id) + version = self._routes_version + for route in self.routes: + if isinstance(route, _IncludedRouter): + version += route.original_router._get_routes_version(seen) + return version + + def _contains_router( + self, router: "APIRouter", seen: set[int] | None = None + ) -> bool: + if seen is None: + seen = set() + router_id = id(self) + if router_id in seen: + return False + seen.add(router_id) + for route in self.routes: + if not isinstance(route, _IncludedRouter): + continue + if route.original_router is router: + return True + if route.original_router._contains_router(router, seen): + return True + return False + + def add_route( + self, + path: str, + endpoint: Callable[[Request], Awaitable[Response] | Response], + methods: Collection[str] | None = None, + name: str | None = None, + include_in_schema: bool = True, + ) -> None: + super().add_route( + path, + endpoint, + methods=methods, + name=name, + include_in_schema=include_in_schema, + ) + self._mark_routes_changed() + + def add_websocket_route( + self, + path: str, + endpoint: Callable[[WebSocket], Awaitable[None]], + name: str | None = None, + ) -> None: + super().add_websocket_route(path, endpoint, name=name) + self._mark_routes_changed() + + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + included_router = _get_scope_included_router(scope) + if ( + isinstance(included_router, _IncludedRouter) + and included_router.original_router is self + ): + await included_router._handle_selected(scope, receive, send) + return + await self.app(scope, receive, send) + + def matches(self, scope: Scope) -> tuple[Match, Scope]: + included_router = _get_scope_included_router(scope) + if ( + isinstance(included_router, _IncludedRouter) + and included_router.original_router is self + ): + match, child_scope, _, _ = included_router._match(scope) + return match, child_scope + return Match.NONE, {} def route( self, @@ -1415,6 +2124,7 @@ class APIRouter(routing.Router): ), ) self.routes.append(route) + self._mark_routes_changed() def api_route( self, @@ -1498,6 +2208,7 @@ class APIRouter(routing.Router): dependency_overrides_provider=self.dependency_overrides_provider, ) self.routes.append(route) + self._mark_routes_changed() def websocket( self, @@ -1714,111 +2425,40 @@ class APIRouter(routing.Router): "Cannot include the same APIRouter instance into itself. " "Did you mean to include a different router?" ) + assert not router._contains_router(self), ( + "Cannot include an APIRouter instance that already includes this router. " + "Did you mean to include a different router?" + ) if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" assert not prefix.endswith("/"), ( "A path prefix must not end with '/', as the routes will start with '/'" ) else: - for r in router.routes: - path = getattr(r, "path") # noqa: B009 + for r in _iter_included_route_candidates(router.routes): + path = getattr(r, "path", None) name = getattr(r, "name", "unknown") if path is not None and not path: raise FastAPIError( f"Prefix and path cannot be both empty (path operation: {name})" ) - if responses is None: - responses = {} - for route in router.routes: - if isinstance(route, APIRoute): - combined_responses = {**responses, **route.responses} - use_response_class = get_value_or_default( - route.response_class, - router.default_response_class, - default_response_class, - self.default_response_class, - ) - current_tags = [] - if tags: - current_tags.extend(tags) - if route.tags: - current_tags.extend(route.tags) - current_dependencies: list[params.Depends] = [] - if dependencies: - current_dependencies.extend(dependencies) - if route.dependencies: - current_dependencies.extend(route.dependencies) - current_callbacks = [] - if callbacks: - current_callbacks.extend(callbacks) - if route.callbacks: - current_callbacks.extend(route.callbacks) - current_generate_unique_id = get_value_or_default( - route.generate_unique_id_function, - router.generate_unique_id_function, - generate_unique_id_function, - self.generate_unique_id_function, - ) - self.add_api_route( - prefix + route.path, - route.endpoint, - response_model=route.response_model, - status_code=route.status_code, - tags=current_tags, - dependencies=current_dependencies, - summary=route.summary, - description=route.description, - response_description=route.response_description, - responses=combined_responses, - deprecated=route.deprecated or deprecated or self.deprecated, - methods=route.methods, - operation_id=route.operation_id, - response_model_include=route.response_model_include, - response_model_exclude=route.response_model_exclude, - response_model_by_alias=route.response_model_by_alias, - response_model_exclude_unset=route.response_model_exclude_unset, - response_model_exclude_defaults=route.response_model_exclude_defaults, - response_model_exclude_none=route.response_model_exclude_none, - include_in_schema=route.include_in_schema - and self.include_in_schema - and include_in_schema, - response_class=use_response_class, - name=route.name, - route_class_override=type(route), - callbacks=current_callbacks, - openapi_extra=route.openapi_extra, - generate_unique_id_function=current_generate_unique_id, - strict_content_type=get_value_or_default( - route.strict_content_type, - router.strict_content_type, - self.strict_content_type, - ), - ) - elif isinstance(route, routing.Route): - methods = list(route.methods or []) - self.add_route( - prefix + route.path, - route.endpoint, - methods=methods, - include_in_schema=route.include_in_schema, - name=route.name, - ) - elif isinstance(route, APIWebSocketRoute): - current_dependencies = [] - if dependencies: - current_dependencies.extend(dependencies) - if route.dependencies: - current_dependencies.extend(route.dependencies) - self.add_api_websocket_route( - prefix + route.path, - route.endpoint, - dependencies=current_dependencies, - name=route.name, - ) - elif isinstance(route, routing.WebSocketRoute): - self.add_websocket_route( - prefix + route.path, route.endpoint, name=route.name - ) + include_context = _RouterIncludeContext.for_include( + parent_router=self, + included_router=router, + prefix=prefix, + tags=tags, + dependencies=dependencies, + default_response_class=default_response_class, + responses=responses, + callbacks=callbacks, + deprecated=deprecated, + include_in_schema=include_in_schema, + generate_unique_id_function=generate_unique_id_function, + ) + self.routes.append( + _IncludedRouter(original_router=router, include_context=include_context) + ) + self._mark_routes_changed() for handler in router.on_startup: self.add_event_handler("startup", handler) for handler in router.on_shutdown: diff --git a/tests/test_custom_route_class.py b/tests/test_custom_route_class.py index 786c1efc3..de5e1da90 100644 --- a/tests/test_custom_route_class.py +++ b/tests/test_custom_route_class.py @@ -3,7 +3,6 @@ from fastapi import APIRouter, FastAPI from fastapi.routing import APIRoute from fastapi.testclient import TestClient from inline_snapshot import snapshot -from starlette.routing import Route app = FastAPI() @@ -63,13 +62,9 @@ def test_get_path(path, expected_status, expected_response): def test_route_classes(): - routes = {} - for r in app.router.routes: - assert isinstance(r, Route) - routes[r.path] = r - assert getattr(routes["/a/"], "x_type") == "A" # noqa: B009 - assert getattr(routes["/a/b/"], "x_type") == "B" # noqa: B009 - assert getattr(routes["/a/b/c/"], "x_type") == "C" # noqa: B009 + assert isinstance(router_a.routes[0], APIRouteA) + assert isinstance(router_b.routes[0], APIRouteB) + assert isinstance(router_c.routes[0], APIRouteC) def test_openapi_schema(): diff --git a/tests/test_router_include_context.py b/tests/test_router_include_context.py new file mode 100644 index 000000000..408cdd3f1 --- /dev/null +++ b/tests/test_router_include_context.py @@ -0,0 +1,855 @@ +from typing import Annotated, cast + +import pytest +from fastapi import APIRouter, Body, Depends, FastAPI, Request +from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse +from fastapi.routing import ( + APIRoute, + _IncludedRouter, + _iter_included_route_candidates, + _restore_fastapi_scope_key, +) +from fastapi.testclient import TestClient +from starlette.routing import BaseRoute, Host, Match, Mount, NoMatchFound, Route, Router + + +def dependency_a(): + return "a" + + +def dependency_b(): + return "b" + + +def dependency_c(): + return "c" + + +def unique_id_b(route: APIRoute) -> str: + return f"b_{route.name}" + + +def test_router_include_context_matches_flattened_include_metadata(): + callback_router = APIRouter() + + @callback_router.post("/callback") + def callback(): # pragma: no cover + return {"ok": True} + + callback_route = callback_router.routes[0] + + parent_router = APIRouter() + included_router = APIRouter( + prefix="/items", + tags=["router"], + dependencies=[Depends(dependency_a)], + responses={401: {"description": "Unauthorized"}}, + callbacks=[callback_route], + default_response_class=HTMLResponse, + strict_content_type=False, + ) + + @included_router.get( + "/{item_id}", + tags=["route"], + dependencies=[Depends(dependency_b)], + responses={404: {"description": "Missing"}}, + callbacks=[callback_route], + generate_unique_id_function=unique_id_b, + ) + def read_item(item_id: str, request: Request): + context = request.scope["fastapi"]["effective_route_context"] + return JSONResponse( + { + "path": context.path, + "tags": context.tags, + "dependency_count": len(context.dependencies), + "response_codes": sorted(context.responses), + "callback_count": len(context.callbacks or []), + "deprecated": context.deprecated, + "include_in_schema": context.include_in_schema, + "response_class": context.response_class.__name__, + "generate_unique_id": context.generate_unique_id_function(context), + "strict_content_type": context.strict_content_type, + "has_dependency_overrides_provider": ( + context.dependency_overrides_provider + is app.router.dependency_overrides_provider + ), + } + ) + + parent_router.include_router( + included_router, + prefix="/api", + tags=["include"], + dependencies=[Depends(dependency_c)], + responses={400: {"description": "Bad request"}}, + callbacks=[callback_route], + deprecated=True, + include_in_schema=False, + ) + + app = FastAPI() + app.include_router(parent_router) + response = TestClient(app).get("/api/items/foo") + + assert response.status_code == 200 + assert response.json() == { + "path": "/api/items/{item_id}", + "tags": ["include", "router", "route"], + "dependency_count": 3, + "response_codes": [400, 401, 404], + "callback_count": 3, + "deprecated": True, + "include_in_schema": False, + "response_class": "HTMLResponse", + "generate_unique_id": "b_read_item", + "strict_content_type": False, + "has_dependency_overrides_provider": True, + } + + +def test_live_route_addition_uses_include_metadata_for_runtime_and_openapi(): + calls: list[str] = [] + + def included_dependency(): + calls.append("dependency") + + router = APIRouter() + app = FastAPI() + app.include_router( + router, + prefix="/api", + tags=["included"], + dependencies=[Depends(included_dependency)], + responses={418: {"description": "Teapot"}}, + ) + + @router.get("/later") + def read_later(): + return {"later": True} + + client = TestClient(app) + response = client.get("/api/later") + + assert response.status_code == 200 + assert response.json() == {"later": True} + assert calls == ["dependency"] + operation = client.get("/openapi.json").json()["paths"]["/api/later"]["get"] + assert operation["tags"] == ["included"] + assert operation["responses"]["418"] == {"description": "Teapot"} + + +def test_openapi_cache_updates_after_live_route_addition(): + router = APIRouter() + app = FastAPI() + app.include_router(router, prefix="/api") + client = TestClient(app) + + first_schema = client.get("/openapi.json").json() + assert "/api/later" not in first_schema["paths"] + + @router.get("/later") + def read_later(): # pragma: no cover + return {"later": True} + + second_schema = client.get("/openapi.json").json() + assert "/api/later" in second_schema["paths"] + + +def test_nested_router_added_after_parent_inclusion_is_live(): + parent_router = APIRouter() + child_router = APIRouter() + app = FastAPI() + app.include_router(parent_router, prefix="/api") + parent_router.include_router(child_router, prefix="/child", tags=["child"]) + + @child_router.get("/items") + def read_items(): + return ["item"] + + client = TestClient(app) + response = client.get("/api/child/items") + + assert response.status_code == 200 + assert response.json() == ["item"] + operation = client.get("/openapi.json").json()["paths"]["/api/child/items"]["get"] + assert operation["tags"] == ["child"] + + +def test_repeated_deep_inclusions_handle_all_concrete_paths(): + shared_router = APIRouter() + + @shared_router.get("/items") + def read_items(): + return [] + + parent_router = APIRouter() + parent_router.include_router(shared_router, prefix="/a") + parent_router.include_router(shared_router, prefix="/b") + + app = FastAPI() + app.include_router(parent_router, prefix="/v1") + app.include_router(parent_router, prefix="/v2") + + client = TestClient(app) + paths = ["/v1/a/items", "/v1/b/items", "/v2/a/items", "/v2/b/items"] + for path in paths: + response = client.get(path) + assert response.status_code == 200 + assert response.json() == [] + assert set(client.get("/openapi.json").json()["paths"]) == set(paths) + + +def test_url_path_for_uses_effective_context_for_live_included_route(): + router = APIRouter() + app = FastAPI() + app.include_router(router, prefix="/api") + + @router.get("/items/{item_id}", name="read_item") + def read_item(item_id: str): # pragma: no cover + return {"item_id": item_id} + + assert app.url_path_for("read_item", item_id="abc") == "/api/items/abc" + + +def test_url_path_for_uses_distinct_repeated_inclusion_contexts(): + router = APIRouter() + + @router.get("/items/{item_id}", name="read_item") + def read_item(item_id: str): # pragma: no cover + return {"item_id": item_id} + + parent_router = APIRouter() + parent_router.include_router(router, prefix="/v1") + parent_router.include_router(router, prefix="/v2") + + assert parent_router.url_path_for("read_item", item_id="abc") == "/v1/items/abc" + assert ( + parent_router.routes[1].url_path_for("read_item", item_id="abc") + == "/v2/items/abc" + ) + + +def test_indirect_router_inclusion_cycles_are_rejected(): + parent_router = APIRouter() + child_router = APIRouter() + + parent_router.include_router(child_router, prefix="/child") + + with pytest.raises(AssertionError, match="already includes this router"): + child_router.include_router(parent_router, prefix="/parent") + + parent_router = APIRouter() + child_router = APIRouter() + grandchild_router = APIRouter() + + parent_router.include_router(child_router, prefix="/child") + child_router.include_router(grandchild_router, prefix="/grandchild") + + with pytest.raises(AssertionError, match="already includes this router"): + grandchild_router.include_router(parent_router, prefix="/parent") + + +def test_original_api_route_subclass_instance_is_called_after_inclusion(): + class TrackingRoute(APIRoute): + calls = 0 + + async def handle(self, scope, receive, send): + self.calls += 1 + await super().handle(scope, receive, send) + + router = APIRouter(route_class=TrackingRoute) + + @router.get("/items") + def read_items(): + return [] + + original_route = router.routes[0] + assert isinstance(original_route, TrackingRoute) + + app = FastAPI() + app.include_router(router, prefix="/api") + + response = TestClient(app).get("/api/items") + + assert response.status_code == 200 + assert original_route.calls == 1 + + +def test_original_api_route_get_route_handler_is_called_after_inclusion(): + class TrackingRoute(APIRoute): + calls = 0 + + def get_route_handler(self): + handler = super().get_route_handler() + + async def custom_handler(request): + self.calls += 1 + return await handler(request) + + return custom_handler + + router = APIRouter(route_class=TrackingRoute) + + @router.get("/items") + def read_items(): + return [] + + original_route = router.routes[0] + assert isinstance(original_route, TrackingRoute) + original_route.calls = 0 + + app = FastAPI() + app.include_router(router, prefix="/api") + + response = TestClient(app).get("/api/items") + + assert response.status_code == 200 + assert original_route.calls == 1 + + +def test_original_api_route_matches_is_called_after_inclusion(): + class HeaderRoute(APIRoute): + calls = 0 + + def matches(self, scope): + self.calls += 1 + headers = dict(scope.get("headers", [])) + if headers.get(b"x-match") != b"yes": + return Match.NONE, {} + return super().matches(scope) + + router = APIRouter(route_class=HeaderRoute) + + @router.get("/items") + def read_items(): + return [] + + original_route = router.routes[0] + assert isinstance(original_route, HeaderRoute) + original_route.calls = 0 + + app = FastAPI() + app.include_router(router, prefix="/api") + client = TestClient(app) + + assert client.get("/api/items").status_code == 404 + assert client.get("/api/items", headers={"x-match": "yes"}).status_code == 200 + assert original_route.calls >= 2 + + +def test_effective_route_context_is_available_in_scope_during_request(): + router = APIRouter() + + @router.get("/items") + def read_items(request: Request): + fastapi_scope = request.scope.get("fastapi") + assert isinstance(fastapi_scope, dict) + return { + "has_context": "effective_route_context" in fastapi_scope, + "path": fastapi_scope["effective_route_context"].path, + } + + app = FastAPI() + app.include_router(router, prefix="/api") + + response = TestClient(app).get("/api/items") + + assert response.status_code == 200 + assert response.json() == {"has_context": True, "path": "/api/items"} + + +def test_original_api_router_matches_is_called_after_inclusion(): + class HeaderRouter(APIRouter): + calls = 0 + + def matches(self, scope): + self.calls += 1 + headers = dict(scope.get("headers", [])) + if headers.get(b"x-router-match") != b"yes": + return Match.NONE, {} + return super().matches(scope) + + router = HeaderRouter() + + @router.get("/items") + def read_items(): + return [] + + app = FastAPI() + app.include_router(router, prefix="/api") + client = TestClient(app) + + assert client.get("/api/items").status_code == 404 + assert ( + client.get("/api/items", headers={"x-router-match": "yes"}).status_code == 200 + ) + assert router.calls >= 2 + + +def test_original_nested_api_router_subclasses_are_called_after_inclusion(): + class TrackingRouter(APIRouter): + calls = 0 + + async def handle(self, scope, receive, send): + self.calls += 1 + await super().handle(scope, receive, send) + + parent_router = TrackingRouter() + child_router = TrackingRouter() + + @child_router.get("/items") + def read_items(): + return [] + + parent_router.include_router(child_router, prefix="/child") + app = FastAPI() + app.include_router(parent_router, prefix="/api") + + response = TestClient(app).get("/api/child/items") + + assert response.status_code == 200 + assert parent_router.calls == 1 + assert child_router.calls == 1 + + +def test_router_and_include_prefix_path_params_reach_endpoint_and_openapi(): + router = APIRouter(prefix="/tenants/{tenant_id}") + + @router.get("/items/{item_id}") + def read_item(version: int, tenant_id: int, item_id: int): + return {"version": version, "tenant_id": tenant_id, "item_id": item_id} + + app = FastAPI() + app.include_router(router, prefix="/api/{version}") + + client = TestClient(app) + response = client.get("/api/1/tenants/2/items/3") + + assert response.status_code == 200 + assert response.json() == {"version": 1, "tenant_id": 2, "item_id": 3} + + operation = client.get("/openapi.json").json()["paths"][ + "/api/{version}/tenants/{tenant_id}/items/{item_id}" + ]["get"] + assert {parameter["name"] for parameter in operation["parameters"]} == { + "version", + "tenant_id", + "item_id", + } + + +def test_effective_body_fields_from_app_router_include_and_route_match_openapi(): + def app_body_dependency(app_body: Annotated[str, Body()]): + return app_body + + def router_body_dependency(router_body: Annotated[int, Body()]): + return router_body + + def include_body_dependency(include_body: Annotated[bool, Body()]): + return include_body + + app = FastAPI(dependencies=[Depends(app_body_dependency)]) + router = APIRouter(dependencies=[Depends(router_body_dependency)]) + + @router.post("/items") + def create_item(route_body: Annotated[float, Body()]): + return {"route_body": route_body} + + app.include_router( + router, + prefix="/api", + dependencies=[Depends(include_body_dependency)], + ) + + client = TestClient(app) + response = client.post( + "/api/items", + json={ + "app_body": "app", + "router_body": 1, + "include_body": True, + "route_body": 2.5, + }, + ) + + assert response.status_code == 200 + assert response.json() == {"route_body": 2.5} + + schema = client.get("/openapi.json").json() + request_body_schema = schema["paths"]["/api/items"]["post"]["requestBody"][ + "content" + ]["application/json"]["schema"] + body_ref = request_body_schema["$ref"].removeprefix("#/components/schemas/") + body_schema = schema["components"]["schemas"][body_ref] + assert set(body_schema["required"]) == { + "app_body", + "router_body", + "include_body", + "route_body", + } + assert set(body_schema["properties"]) == { + "app_body", + "router_body", + "include_body", + "route_body", + } + + +def test_later_full_match_wins_over_earlier_included_partial_match(): + get_router = APIRouter() + post_router = APIRouter() + + @get_router.get("/items") + def read_items(): # pragma: no cover + return {"method": "get"} + + @post_router.post("/items") + def create_item(): + return {"method": "post"} + + app = FastAPI() + app.include_router(get_router, prefix="/api") + app.include_router(post_router, prefix="/api") + + response = TestClient(app).post("/api/items") + + assert response.status_code == 200 + assert response.json() == {"method": "post"} + + +def test_included_partial_match_returns_405_when_no_later_full_match_exists(): + router = APIRouter() + + @router.get("/items") + def read_items(): # pragma: no cover + return [] + + app = FastAPI() + app.include_router(router, prefix="/api") + + response = TestClient(app).post("/api/items") + + assert response.status_code == 405 + assert response.headers["allow"] == "GET" + + +def test_included_slash_redirect_does_not_block_later_exact_match(): + redirect_router = APIRouter() + exact_router = APIRouter() + + @redirect_router.get("/items/") + def read_items_with_slash(): # pragma: no cover + return {"path": "slash"} + + @exact_router.get("/items") + def read_items_without_slash(): + return {"path": "exact"} + + app = FastAPI() + app.include_router(redirect_router, prefix="/api") + app.include_router(exact_router, prefix="/api") + + response = TestClient(app).get("/api/items", follow_redirects=False) + + assert response.status_code == 200 + assert response.json() == {"path": "exact"} + + +def test_failed_included_match_does_not_leak_effective_context_to_later_route(): + class RejectingRoute(APIRoute): + def matches(self, scope): + return Match.NONE, {} + + rejecting_router = APIRouter(route_class=RejectingRoute) + fallback_router = APIRouter() + + @rejecting_router.get("/items") + def rejected_item(): # pragma: no cover + return {"source": "rejected"} + + @fallback_router.get("/items") + def fallback_item(request: Request): + fastapi_scope = request.scope.get("fastapi", {}) + context = fastapi_scope.get("effective_route_context") + return { + "source": "fallback", + "context_path": getattr(context, "path", None), + } + + app = FastAPI() + app.include_router(rejecting_router, prefix="/api") + app.include_router(fallback_router, prefix="/api") + + response = TestClient(app).get("/api/items") + + assert response.status_code == 200 + assert response.json() == {"source": "fallback", "context_path": "/api/items"} + + +def test_included_starlette_mount_keeps_prefix_runtime_and_url_path_for(): + def mounted_endpoint(request): + return PlainTextResponse("mounted") + + router = APIRouter( + routes=[ + Mount( + "/mounted", + routes=[Route("/items/{item_id}", mounted_endpoint, name="read_item")], + name="mounted", + ) + ] + ) + app = FastAPI() + app.include_router(router, prefix="/api") + + client = TestClient(app) + response = client.get("/api/mounted/items/abc") + + assert response.status_code == 200 + assert response.text == "mounted" + assert ( + app.url_path_for("mounted:read_item", item_id="abc") == "/api/mounted/items/abc" + ) + + +def test_included_starlette_host_keeps_prefix_runtime_and_url_path_for(): + def hosted_endpoint(request): + return PlainTextResponse("hosted") + + hosted_app = Router( + routes=[Route("/items/{item_id}", hosted_endpoint, name="read_item")] + ) + router = APIRouter( + routes=[Host("{subdomain}.example.com", hosted_app, name="hosted")] + ) + app = FastAPI() + app.include_router(router, prefix="/api") + + client = TestClient(app, base_url="http://api.example.com") + response = client.get("/api/items/abc") + + assert response.status_code == 200 + assert response.text == "hosted" + url = app.url_path_for("hosted:read_item", subdomain="api", item_id="abc") + assert str(url) == "/api/items/abc" + assert url.host == "api.example.com" + + +def test_restore_fastapi_scope_key_ignores_non_dict_fastapi_scope(): + scope = {"fastapi": "not-a-dict"} + + _restore_fastapi_scope_key(scope, "effective_route_context", object()) + + assert scope == {"fastapi": "not-a-dict"} + + +@pytest.mark.anyio +async def test_included_api_route_without_app_scope_returns_405_response(): + router = APIRouter() + + @router.get("/items") + def read_items(): # pragma: no cover + return {"items": []} + + app = FastAPI() + app.include_router(router, prefix="/api") + included_router = cast(_IncludedRouter, app.router.routes[-1]) + effective_context = next(included_router.effective_route_contexts()) + route = effective_context.original_route + messages = [] + + async def receive(): # pragma: no cover + return {"type": "http.request", "body": b"", "more_body": False} + + async def send(message): + messages.append(message) + + scope = { + "type": "http", + "method": "POST", + "path": "/api/items", + "raw_path": b"/api/items", + "root_path": "", + "scheme": "http", + "query_string": b"", + "headers": [], + "fastapi": {"effective_route_context": effective_context}, + } + + await route.handle(scope, receive, send) + + assert messages[0]["type"] == "http.response.start" + assert messages[0]["status"] == 405 + assert dict(messages[0]["headers"])[b"allow"] == b"GET" + + +def test_effective_api_route_context_does_not_match_websocket_scope(): + router = APIRouter() + + @router.get("/items") + def read_items(): # pragma: no cover + return {"items": []} + + app = FastAPI() + app.include_router(router, prefix="/api") + included_router = cast(_IncludedRouter, app.router.routes[-1]) + effective_context = next(included_router.effective_route_contexts()) + + match, child_scope = effective_context.matches( + { + "type": "websocket", + "path": "/api/items", + "root_path": "", + } + ) + + assert match == Match.NONE + assert child_scope == {} + + +def test_effective_api_route_context_url_path_for_no_match(): + router = APIRouter() + + @router.get("/items/{item_id}") + def read_item(item_id: str): # pragma: no cover + return {"item_id": item_id} + + app = FastAPI() + app.include_router(router, prefix="/api") + included_router = cast(_IncludedRouter, app.router.routes[-1]) + effective_context = next(included_router.effective_route_contexts()) + + with pytest.raises(NoMatchFound): + effective_context.url_path_for("missing", item_id="abc") + + with pytest.raises(NoMatchFound): + included_router.url_path_for("missing", item_id="abc") + + +def test_included_starlette_host_without_prefix_keeps_original_app(): + def hosted_endpoint(request): + return PlainTextResponse("hosted") + + hosted_app = Router( + routes=[Route("/items/{item_id}", hosted_endpoint, name="read_item")] + ) + router = APIRouter( + routes=[Host("{subdomain}.example.com", hosted_app, name="hosted")] + ) + app = FastAPI() + app.include_router(router) + + client = TestClient(app, base_url="http://api.example.com") + response = client.get("/items/abc") + + assert response.status_code == 200 + assert response.text == "hosted" + + +class UnknownRoute(BaseRoute): + def matches(self, scope): # pragma: no cover + return Match.NONE, {} + + async def handle(self, scope, receive, send): # pragma: no cover + raise AssertionError("UnknownRoute should not be handled") + + def url_path_for(self, name, /, **path_params): # pragma: no cover + raise NoMatchFound(name, path_params) + + +@pytest.mark.anyio +async def test_included_unknown_route_is_ignored_and_can_return_default_404(): + router = APIRouter(routes=[UnknownRoute()]) + app = FastAPI() + app.include_router(router, prefix="/api") + included_router = cast(_IncludedRouter, app.router.routes[-1]) + + assert included_router.effective_candidates() == [] + + messages = [] + + async def receive(): # pragma: no cover + return {"type": "http.request", "body": b"", "more_body": False} + + async def send(message): + messages.append(message) + + scope = { + "type": "http", + "method": "GET", + "path": "/api/missing", + "raw_path": b"/api/missing", + "root_path": "", + "scheme": "http", + "query_string": b"", + "headers": [], + "fastapi": {}, + } + + await included_router._handle_selected(scope, receive, send) + + assert messages[0]["type"] == "http.response.start" + assert messages[0]["status"] == 404 + + +def test_no_prefix_include_validation_sees_effective_starlette_route_candidates(): + def endpoint(request): # pragma: no cover + return PlainTextResponse("ok") + + child_router = APIRouter(routes=[Route("/items", endpoint, name="read_items")]) + parent_router = APIRouter() + parent_router.include_router(child_router, prefix="/child") + + candidates = list(_iter_included_route_candidates(parent_router.routes)) + + assert cast(Route, candidates[0]).path == "/child/items" + + +def test_apirouter_matches_fallback_without_include_context(): + router = APIRouter() + + def read_items(request): # pragma: no cover + return PlainTextResponse("items") + + router.add_route("/items", read_items) + + assert router.matches({"type": "http", "path": "/items", "root_path": ""}) == ( + Match.NONE, + {}, + ) + + +@pytest.mark.anyio +async def test_apirouter_handle_fallback_without_include_context(): + router = APIRouter() + + def read_items(request): + return PlainTextResponse("items") + + router.add_route("/items", read_items) + messages = [] + + async def receive(): # pragma: no cover + return {"type": "http.request", "body": b"", "more_body": False} + + async def send(message): + messages.append(message) + + scope = { + "type": "http", + "method": "GET", + "path": "/items", + "raw_path": b"/items", + "root_path": "", + "scheme": "http", + "query_string": b"", + "headers": [], + } + + await router.handle(scope, receive, send) + + assert messages[0]["type"] == "http.response.start" + assert messages[0]["status"] == 200 + assert messages[1]["body"] == b"items" From ba609a8946e9054c050ca4cf7afa2a222694bfc9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 14 Jun 2026 12:36:16 +0000 Subject: [PATCH 024/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 101804507..60c2b61c6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -56,6 +56,10 @@ Still, for now, consider this very experimental and potentially changing and bre * Middleware per router * Other features planned +### Breaking Changes + +* ♻ Refactor internals to preserve `APIRouter` and `APIRoute` instances. PR [#15745](https://github.com/fastapi/fastapi/pull/15745) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Update FastAPI Cloud deployment instructions. PR [#15724](https://github.com/fastapi/fastapi/pull/15724) by [@alejsdev](https://github.com/alejsdev). From 31d097f286b63c2c7bc9954aa605571e0f3d1e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 14 Jun 2026 14:39:49 +0200 Subject: [PATCH 025/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= =?UTF-8?q?=20(#15747)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 60c2b61c6..c7376e527 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,7 +7,9 @@ hide: ## Latest Changes -♻ Refactor internals to preserve `APIRouter` and `APIRoute` instances +### Breaking Changes + +* ♻ Refactor internals to preserve `APIRouter` and `APIRoute` instances. PR [#15745](https://github.com/fastapi/fastapi/pull/15745) by [@tiangolo](https://github.com/tiangolo). Unblocks ✹ SO MANY THINGS ✹ @@ -21,7 +23,7 @@ The way it is structured here is that there are a few additional classes to hand Dependencies for 404: previously I intended to support dependencies that would be executed even for 404, but that would conflict with the fact that a router could _not_ find a match, but the next router _did_ find a match. Executing dependencies in the router that did not find a match would not make sense, they could consume the request, body, etc. This original idea was discarded. -#### Breaking Changes +#### Specific Breaking Changes Now `router.routes` is no longer a plain list of `APIRoute` objects, it can contain these intermediate objects that can contain additional routers, forming a tree. @@ -56,10 +58,6 @@ Still, for now, consider this very experimental and potentially changing and bre * Middleware per router * Other features planned -### Breaking Changes - -* ♻ Refactor internals to preserve `APIRouter` and `APIRoute` instances. PR [#15745](https://github.com/fastapi/fastapi/pull/15745) by [@tiangolo](https://github.com/tiangolo). - ### Docs * 📝 Update FastAPI Cloud deployment instructions. PR [#15724](https://github.com/fastapi/fastapi/pull/15724) by [@alejsdev](https://github.com/alejsdev). From c6d58978269b51304ccd8f0fb644f35ecbed94ce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 14 Jun 2026 12:40:20 +0000 Subject: [PATCH 026/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c7376e527..88bd14374 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -60,6 +60,7 @@ Still, for now, consider this very experimental and potentially changing and bre ### Docs +* 📝 Update release notes. PR [#15747](https://github.com/fastapi/fastapi/pull/15747) by [@tiangolo](https://github.com/tiangolo). * 📝 Update FastAPI Cloud deployment instructions. PR [#15724](https://github.com/fastapi/fastapi/pull/15724) by [@alejsdev](https://github.com/alejsdev). * ✏ Use `Annotated` in inline example in `docs/en/docs/tutorial/body-multiple-params.md`. PR [#15591](https://github.com/fastapi/fastapi/pull/15591) by [@TheArchons](https://github.com/TheArchons). * 📝 Remove "NGINX Unit" from the list of ASGI-servers in docs. PR [#15475](https://github.com/fastapi/fastapi/pull/15475) by [@angryfoxx](https://github.com/angryfoxx). From 9a9c4ad5d06f5fe8ee6775a5aeaa2f83c854f263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 14 Jun 2026 14:47:28 +0200 Subject: [PATCH 027/188] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.13?= =?UTF-8?q?7.0=20(#15748)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 88bd14374..704c5599f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.137.0 (2026-06-14) + ### Breaking Changes * ♻ Refactor internals to preserve `APIRouter` and `APIRoute` instances. PR [#15745](https://github.com/fastapi/fastapi/pull/15745) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 38e747232..808f6b294 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.136.3" +__version__ = "0.137.0" from starlette import status as status From d8aad201eba558b20419a832e51a026b0e6ede0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 15 Jun 2026 12:55:06 +0200 Subject: [PATCH 028/188] =?UTF-8?q?=F0=9F=90=9B=20Fix=20bug,=20allow=20emp?= =?UTF-8?q?ty=20path=20in=20path=20operation=20in=20prefixless=20router=20?= =?UTF-8?q?(#15763)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/routing.py | 13 +++++-- tests/test_router_include_context.py | 55 ++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index fb4784309..f33b96479 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -2435,9 +2435,16 @@ class APIRouter(routing.Router): "A path prefix must not end with '/', as the routes will start with '/'" ) else: - for r in _iter_included_route_candidates(router.routes): - path = getattr(r, "path", None) - name = getattr(r, "name", "unknown") + for route, route_context in _iter_routes_with_context(router.routes): + if route_context is None: + path = getattr(route, "path", None) + name = getattr(route, "name", "unknown") + elif route_context.starlette_route is not None: + path = getattr(route_context.starlette_route, "path", None) + name = getattr(route_context.starlette_route, "name", "unknown") + else: + path = route_context.path + name = route_context.name if path is not None and not path: raise FastAPIError( f"Prefix and path cannot be both empty (path operation: {name})" diff --git a/tests/test_router_include_context.py b/tests/test_router_include_context.py index 408cdd3f1..c2679aa11 100644 --- a/tests/test_router_include_context.py +++ b/tests/test_router_include_context.py @@ -2,6 +2,7 @@ from typing import Annotated, cast import pytest from fastapi import APIRouter, Body, Depends, FastAPI, Request +from fastapi.exceptions import FastAPIError from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse from fastapi.routing import ( APIRoute, @@ -807,6 +808,60 @@ def test_no_prefix_include_validation_sees_effective_starlette_route_candidates( assert cast(Route, candidates[0]).path == "/child/items" +def test_no_prefix_include_validation_sees_effective_api_route_path(): + leaf_router = APIRouter() + + @leaf_router.get("") + def read_items(): + return [] + + parent_router = APIRouter() + parent_router.include_router(leaf_router, prefix="/items") + + # for coverage + candidates = list(_iter_included_route_candidates(parent_router.routes)) + assert cast(APIRoute, candidates[0]).path == "" + + app = FastAPI() + app.include_router(parent_router) + client = TestClient(app) + + response = client.get("/items") + + assert response.status_code == 200, response.text + assert response.json() == [] + + +def test_no_prefix_include_validation_sees_effective_starlette_route_path(): + def endpoint(request): + return PlainTextResponse("ok") + + child_router = APIRouter(routes=[Route("/items", endpoint, name="read_items")]) + parent_router = APIRouter() + parent_router.include_router(child_router, prefix="/child") + + app = FastAPI() + app.include_router(parent_router) + client = TestClient(app) + + response = client.get("/child/items") + + assert response.status_code == 200, response.text + assert response.text == "ok" + + +def test_no_prefix_include_validation_rejects_empty_effective_api_route_path(): + router = APIRouter() + + @router.get("") + def read_items(): # pragma: no cover + return [] + + app = FastAPI() + with pytest.raises(FastAPIError): + app.include_router(router) + + def test_apirouter_matches_fallback_without_include_context(): router = APIRouter() From e0f8cadf094bb4cb7a21e7757333d5dfbae46712 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Jun 2026 10:55:32 +0000 Subject: [PATCH 029/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 704c5599f..991f62c91 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix bug, allow empty path in path operation in prefixless router. PR [#15763](https://github.com/fastapi/fastapi/pull/15763) by [@tiangolo](https://github.com/tiangolo). + ## 0.137.0 (2026-06-14) ### Breaking Changes From b78c82262f5170831ebd3f27873b09d8e579bac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 15 Jun 2026 13:19:51 +0200 Subject: [PATCH 030/188] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20typing=20checks=20?= =?UTF-8?q?for=20APIRoute=20(#15765)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/routing.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/fastapi/routing.py b/fastapi/routing.py index f33b96479..48c0c2153 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1062,6 +1062,41 @@ def _populate_api_route_state( class APIRoute(routing.Route): + stream_item_type: Any | None + response_model: Any + summary: str | None + response_description: str + deprecated: bool | None + operation_id: str | None + response_model_include: IncEx | None + response_model_exclude: IncEx | None + response_model_by_alias: bool + response_model_exclude_unset: bool + response_model_exclude_defaults: bool + response_model_exclude_none: bool + include_in_schema: bool + response_class: type[Response] | DefaultPlaceholder + dependency_overrides_provider: Any | None + callbacks: list[BaseRoute] | None + openapi_extra: dict[str, Any] | None + generate_unique_id_function: Callable[[Any], str] | DefaultPlaceholder + strict_content_type: bool | DefaultPlaceholder + tags: list[str | Enum] + responses: dict[int | str, dict[str, Any]] + unique_id: str + status_code: int | None + response_field: ModelField | None + stream_item_field: ModelField | None + dependencies: list[params.Depends] + description: str + response_fields: dict[int | str, ModelField] + dependant: Dependant + _flat_dependant: Dependant + _embed_body_fields: bool + body_field: ModelField | None + is_sse_stream: bool + is_json_stream: bool + def __init__( self, path: str, From edd1461589287bb432c8eb81ace92d000fd5c1b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Jun 2026 11:20:24 +0000 Subject: [PATCH 031/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 991f62c91..e2a29ea8a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Fixes +* 🚹 Fix typing checks for APIRoute. PR [#15765](https://github.com/fastapi/fastapi/pull/15765) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix bug, allow empty path in path operation in prefixless router. PR [#15763](https://github.com/fastapi/fastapi/pull/15763) by [@tiangolo](https://github.com/tiangolo). ## 0.137.0 (2026-06-14) From a82e5f2fac102a8b12c0795da80b2cb1d2a8b016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 15 Jun 2026 13:26:48 +0200 Subject: [PATCH 032/188] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.13?= =?UTF-8?q?7.1=20(#15766)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e2a29ea8a..9effecd9e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.137.1 (2026-06-15) + ### Fixes * 🚹 Fix typing checks for APIRoute. PR [#15765](https://github.com/fastapi/fastapi/pull/15765) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 808f6b294..fdd584b40 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.137.0" +__version__ = "0.137.1" from starlette import status as status From 76876e5a812268860148ba3a2059795b4d65ae55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 15 Jun 2026 16:31:14 +0200 Subject: [PATCH 033/188] =?UTF-8?q?=F0=9F=94=A7=20Add=20ty=20configs=20to?= =?UTF-8?q?=20check=20docs=20sources=20(#15769)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- pyproject.toml | 36 ++++++++++++++++++++++++++++++++++++ scripts/lint.sh | 2 +- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0cb5b86e3..de6f56a53 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - id: local-ty name: ty check - entry: uv run ty check fastapi + entry: uv run ty check fastapi docs_src --force-exclude require_serial: true language: unsupported pass_filenames: false diff --git a/pyproject.toml b/pyproject.toml index daa523ce2..8b633a928 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -349,5 +349,41 @@ havin = "havin" Ines = "Ines" ser = "ser" +[tool.ty.src] +exclude = [ + # These docs examples are intentionally partial, dynamic, environment-driven, + # deprecated, or currently require broader tutorial rewrites to satisfy ty. + "docs_src/additional_status_codes/", + "docs_src/app_testing/tutorial003_py310.py", + "docs_src/body_multiple_params/", + "docs_src/body_updates/tutorial002_py310.py", + "docs_src/custom_docs_ui/", + "docs_src/custom_response/tutorial001_py310.py", + "docs_src/custom_response/tutorial001b_py310.py", + "docs_src/custom_response/tutorial009c_py310.py", + "docs_src/dependencies/tutorial007_py310.py", + "docs_src/dependencies/tutorial008_an_py310.py", + "docs_src/dependencies/tutorial008_py310.py", + "docs_src/dependencies/tutorial010_py310.py", + "docs_src/events/", + "docs_src/extending_openapi/tutorial001_py310.py", + "docs_src/path_params_numeric_validations/", + "docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py", + "docs_src/python_types/tutorial003_py310.py", + "docs_src/python_types/tutorial011_py310.py", + "docs_src/query_params_str_validations/", + "docs_src/response_model/tutorial006_py310.py", + "docs_src/security/tutorial003_an_py310.py", + "docs_src/security/tutorial003_py310.py", + "docs_src/security/tutorial004_an_py310.py", + "docs_src/security/tutorial004_py310.py", + "docs_src/security/tutorial005_an_py310.py", + "docs_src/security/tutorial005_py310.py", + "docs_src/settings/", + "docs_src/sql_databases/", + "docs_src/using_request_directly/tutorial001_py310.py", + "docs_src/wsgi/tutorial001_py310.py", +] + [tool.ty.terminal] error-on-warning = true diff --git a/scripts/lint.sh b/scripts/lint.sh index a4d3422d3..291674e32 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -4,6 +4,6 @@ set -e set -x mypy fastapi -ty check fastapi +ty check fastapi docs_src --force-exclude ruff check fastapi tests docs_src scripts ruff format fastapi tests --check From 4473a0cd9129f9623f8d23d1a3bc86914967d9c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Jun 2026 14:31:50 +0000 Subject: [PATCH 034/188] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9effecd9e..979c1f9a0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 🔧 Add ty configs to check docs sources. PR [#15769](https://github.com/fastapi/fastapi/pull/15769) by [@tiangolo](https://github.com/tiangolo). + ## 0.137.1 (2026-06-15) ### Fixes From b7de2b7feb207b0a157b81c237b5a0cba6e21cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 15 Jun 2026 17:53:46 +0200 Subject: [PATCH 035/188] =?UTF-8?q?=F0=9F=94=A7=20Add=20ty=20configs=20to?= =?UTF-8?q?=20check=20docs=20sources=20(#15770)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- scripts/contributors.py | 2 +- scripts/deploy_docs_status.py | 2 +- scripts/doc_parsing_utils.py | 4 +-- scripts/docs.py | 4 +-- scripts/label_approved.py | 2 +- scripts/lint.sh | 2 +- scripts/notify_translations.py | 3 ++- scripts/people.py | 2 +- scripts/sponsors.py | 2 +- scripts/topic_repos.py | 2 +- tests/test_compat.py | 18 +++++++++---- tests/test_custom_middleware_exception.py | 4 ++- tests/test_datastructures.py | 7 ++--- tests/test_default_response_class.py | 2 +- tests/test_deprecated_responses.py | 10 +++---- tests/test_inherited_custom_class.py | 2 +- tests/test_jsonable_encoder.py | 10 +++---- tests/test_local_docs.py | 20 +++++++------- tests/test_openapi_schema_type.py | 2 +- tests/test_orjson_response_class.py | 4 +-- ...est_response_model_as_return_annotation.py | 26 +++++++++---------- tests/test_router_events.py | 18 ++++++------- tests/test_schema_compat_pydantic_v2.py | 2 +- tests/test_serialize_response_model.py | 4 +-- tests/test_skip_defaults.py | 2 +- tests/test_sse.py | 4 +-- tests/test_starlette_urlconvertors.py | 4 +-- tests/test_stream_cancellation.py | 9 ++++--- tests/test_swagger_ui_escape.py | 6 ++--- .../test_body/test_tutorial001.py | 2 +- ...est_tutorial001_tutorial002_tutorial003.py | 3 ++- .../test_custom_response/test_tutorial008.py | 3 ++- .../test_custom_response/test_tutorial009.py | 3 ++- .../test_custom_response/test_tutorial009b.py | 3 ++- .../test_debugging/test_tutorial001.py | 10 +++---- .../test_openapi_webhooks/test_tutorial001.py | 5 +++- .../test_python_types/test_tutorial003.py | 2 +- .../test_python_types/test_tutorial005.py | 8 +++--- .../test_security/test_tutorial005.py | 8 +++--- .../test_sql_databases/test_tutorial001.py | 14 ++++++---- .../test_sql_databases/test_tutorial002.py | 14 ++++++---- tests/test_webhooks_security.py | 5 +++- tests/utils.py | 6 ++--- 44 files changed, 149 insertions(+), 118 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de6f56a53..e5c359eeb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - id: local-ty name: ty check - entry: uv run ty check fastapi docs_src --force-exclude + entry: uv run ty check require_serial: true language: unsupported pass_filenames: false diff --git a/scripts/contributors.py b/scripts/contributors.py index af1434d79..5e9d72d64 100644 --- a/scripts/contributors.py +++ b/scripts/contributors.py @@ -237,7 +237,7 @@ def update_content(*, content_path: Path, new_content: Any) -> bool: def main() -> None: logging.basicConfig(level=logging.INFO) - settings = Settings() + settings = Settings() # ty: ignore[missing-argument] logging.info(f"Using config: {settings.model_dump_json()}") g = Github(settings.github_token.get_secret_value()) repo = g.get_repo(settings.github_repository) diff --git a/scripts/deploy_docs_status.py b/scripts/deploy_docs_status.py index e620b15ba..8e5a2e38b 100644 --- a/scripts/deploy_docs_status.py +++ b/scripts/deploy_docs_status.py @@ -24,7 +24,7 @@ class LinkData(BaseModel): def main() -> None: logging.basicConfig(level=logging.INFO) - settings = Settings() + settings = Settings() # ty: ignore[missing-argument] logging.info(f"Using config: {settings.model_dump_json()}") g = Github(auth=Auth.Token(settings.github_token.get_secret_value())) diff --git a/scripts/doc_parsing_utils.py b/scripts/doc_parsing_utils.py index 88ff2c50b..f2f047c3d 100644 --- a/scripts/doc_parsing_utils.py +++ b/scripts/doc_parsing_utils.py @@ -625,14 +625,14 @@ def replace_multiline_code_block( _line_b_code, line_b_comment = _split_hash_comment(line_b) res_line = line_b if line_b_comment: - res_line = res_line.replace(line_b_comment, line_a_comment, 1) + res_line = res_line.replace(line_b_comment, line_a_comment or "", 1) code_block.append(res_line) elif block_language in {"console", "json", "slash-style-comments"}: _line_a_code, line_a_comment = _split_slashes_comment(line_a) _line_b_code, line_b_comment = _split_slashes_comment(line_b) res_line = line_b if line_b_comment: - res_line = res_line.replace(line_b_comment, line_a_comment, 1) + res_line = res_line.replace(line_b_comment, line_a_comment or "", 1) code_block.append(res_line) else: code_block.append(line_b) diff --git a/scripts/docs.py b/scripts/docs.py index a478d59a0..07c951cc7 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -155,7 +155,7 @@ def build_lang( """ build_zensical_lang_to_stage(lang) copy_zensical_stage_to_site(lang) - typer.secho(f"Successfully built docs for: {lang}", color=typer.colors.GREEN) + typer.secho(f"Successfully built docs for: {lang}", fg=typer.colors.GREEN) def split_markdown_header(markdown: str) -> tuple[str, str]: @@ -408,7 +408,7 @@ def build_all() -> None: for lang in langs: if lang != "en": copy_zensical_stage_to_site(lang) - typer.secho("Successfully built all docs", color=typer.colors.GREEN) + typer.secho("Successfully built all docs", fg=typer.colors.GREEN) @app.command() diff --git a/scripts/label_approved.py b/scripts/label_approved.py index 81de92efb..397a79663 100644 --- a/scripts/label_approved.py +++ b/scripts/label_approved.py @@ -22,7 +22,7 @@ class Settings(BaseSettings): config: dict[str, LabelSettings] | Literal[""] = default_config -settings = Settings() +settings = Settings() # ty: ignore[missing-argument] if settings.debug: logging.basicConfig(level=logging.DEBUG) else: diff --git a/scripts/lint.sh b/scripts/lint.sh index 291674e32..a7d1f2f66 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -4,6 +4,6 @@ set -e set -x mypy fastapi -ty check fastapi docs_src --force-exclude +ty check ruff check fastapi tests docs_src scripts ruff format fastapi tests --check diff --git a/scripts/notify_translations.py b/scripts/notify_translations.py index 3484b69c7..22fa633f4 100644 --- a/scripts/notify_translations.py +++ b/scripts/notify_translations.py @@ -304,7 +304,7 @@ def update_comment(*, settings: Settings, comment_id: str, body: str) -> Comment def main() -> None: - settings = Settings() + settings = Settings() # ty: ignore[missing-argument] if settings.debug: logging.basicConfig(level=logging.DEBUG) else: @@ -324,6 +324,7 @@ def main() -> None: ) or settings.number if number is None: raise RuntimeError("No PR number available") + number = cast(int, number) # Avoid race conditions with multiple labels sleep_time = random.random() * 10 # random number between 0 and 10 seconds diff --git a/scripts/people.py b/scripts/people.py index 5718d65da..72b591367 100644 --- a/scripts/people.py +++ b/scripts/people.py @@ -394,7 +394,7 @@ def update_content(*, content_path: Path, new_content: Any) -> bool: def main() -> None: logging.basicConfig(level=logging.INFO) - settings = Settings() + settings = Settings() # ty: ignore[missing-argument] logging.info(f"Using config: {settings.model_dump_json()}") rate_limiter.speed_multiplier = settings.speed_multiplier g = Github(settings.github_token.get_secret_value()) diff --git a/scripts/sponsors.py b/scripts/sponsors.py index fdcabc737..38d8cbfa4 100644 --- a/scripts/sponsors.py +++ b/scripts/sponsors.py @@ -158,7 +158,7 @@ def update_content(*, content_path: Path, new_content: Any) -> bool: def main() -> None: logging.basicConfig(level=logging.INFO) - settings = Settings() + settings = Settings() # ty: ignore[missing-argument] logging.info(f"Using config: {settings.model_dump_json()}") g = Github(settings.pr_token.get_secret_value()) repo = g.get_repo(settings.github_repository) diff --git a/scripts/topic_repos.py b/scripts/topic_repos.py index b7afc0864..94379d384 100644 --- a/scripts/topic_repos.py +++ b/scripts/topic_repos.py @@ -24,7 +24,7 @@ class Repo(BaseModel): def main() -> None: logging.basicConfig(level=logging.INFO) - settings = Settings() + settings = Settings() # ty: ignore[missing-argument] logging.info(f"Using config: {settings.model_dump_json()}") g = Github(settings.github_token.get_secret_value(), per_page=100) diff --git a/tests/test_compat.py b/tests/test_compat.py index 772bd305e..76151fac9 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,3 +1,5 @@ +from typing import Any, cast + from fastapi import FastAPI, UploadFile from fastapi._compat import ( Undefined, @@ -56,9 +58,15 @@ def test_propagates_pydantic2_model_config(): @app.post("/") def foo(req: Model) -> dict[str, str | None]: + value = req.value + if isinstance(value, Missing): + value = None + embedded_value = req.embedded_model.value + if isinstance(embedded_value, Missing): + embedded_value = None return { - "value": req.value or None, - "embedded_value": req.embedded_model.value or None, + "value": value, + "embedded_value": embedded_value, } client = TestClient(app) @@ -100,7 +108,7 @@ def test_serialize_sequence_value_with_optional_list(): """Test that serialize_sequence_value handles optional lists correctly.""" from fastapi._compat import v2 - field_info = FieldInfo(annotation=list[str] | None) + field_info = FieldInfo(annotation=cast(Any, list[str] | None)) field = v2.ModelField(name="items", field_info=field_info) result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"]) assert result == ["a", "b", "c"] @@ -111,7 +119,7 @@ def test_serialize_sequence_value_with_optional_list_pipe_union(): """Test that serialize_sequence_value handles optional lists correctly (with new syntax).""" from fastapi._compat import v2 - field_info = FieldInfo(annotation=list[str] | None) + field_info = FieldInfo(annotation=cast(Any, list[str] | None)) field = v2.ModelField(name="items", field_info=field_info) result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"]) assert result == ["a", "b", "c"] @@ -125,7 +133,7 @@ def test_serialize_sequence_value_with_none_first_in_union(): from fastapi._compat import v2 # Use Union[None, list[str]] to ensure None comes first in the union args - field_info = FieldInfo(annotation=Union[None, list[str]]) # noqa: UP007 + field_info = FieldInfo(annotation=cast(Any, Union[None, list[str]])) # noqa: UP007 field = v2.ModelField(name="items", field_info=field_info) result = v2.serialize_sequence_value(field=field, value=["x", "y"]) assert result == ["x", "y"] diff --git a/tests/test_custom_middleware_exception.py b/tests/test_custom_middleware_exception.py index cf548f4ae..989ab58bc 100644 --- a/tests/test_custom_middleware_exception.py +++ b/tests/test_custom_middleware_exception.py @@ -3,6 +3,7 @@ from pathlib import Path from fastapi import APIRouter, FastAPI, File, UploadFile from fastapi.exceptions import HTTPException from fastapi.testclient import TestClient +from starlette.types import ASGIApp app = FastAPI() @@ -16,7 +17,7 @@ class ContentSizeLimitMiddleware: max_content_size (optional): the maximum content size allowed in bytes, None for no limit """ - def __init__(self, app: APIRouter, max_content_size: int | None = None): + def __init__(self, app: ASGIApp, max_content_size: int | None = None): self.app = app self.max_content_size = max_content_size @@ -31,6 +32,7 @@ class ContentSizeLimitMiddleware: body_len = len(message.get("body", b"")) received += body_len + assert self.max_content_size is not None if received > self.max_content_size: raise HTTPException( 422, diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 29a70cae0..1b5335ea9 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -1,9 +1,10 @@ import io from pathlib import Path +from typing import cast import pytest from fastapi import FastAPI, UploadFile -from fastapi.datastructures import Default +from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.testclient import TestClient @@ -13,8 +14,8 @@ def test_upload_file_invalid_pydantic_v2(): def test_default_placeholder_equals(): - placeholder_1 = Default("a") - placeholder_2 = Default("a") + placeholder_1 = cast(DefaultPlaceholder, Default("a")) + placeholder_2 = cast(DefaultPlaceholder, Default("a")) assert placeholder_1 == placeholder_2 assert placeholder_1.value == placeholder_2.value diff --git a/tests/test_default_response_class.py b/tests/test_default_response_class.py index 88498e560..bc60e0b14 100644 --- a/tests/test_default_response_class.py +++ b/tests/test_default_response_class.py @@ -11,7 +11,7 @@ class ORJSONResponse(JSONResponse): media_type = "application/x-orjson" def render(self, content: Any) -> bytes: - import orjson + import orjson # ty: ignore[unresolved-import] return orjson.dumps(content) diff --git a/tests/test_deprecated_responses.py b/tests/test_deprecated_responses.py index 8cbd9c11f..8a5663744 100644 --- a/tests/test_deprecated_responses.py +++ b/tests/test_deprecated_responses.py @@ -3,7 +3,7 @@ import warnings import pytest from fastapi import FastAPI from fastapi.exceptions import FastAPIDeprecationWarning -from fastapi.responses import ORJSONResponse, UJSONResponse +from fastapi.responses import ORJSONResponse, UJSONResponse # ty: ignore[deprecated] from fastapi.testclient import TestClient from pydantic import BaseModel @@ -21,7 +21,7 @@ class Item(BaseModel): def _make_orjson_app() -> FastAPI: with warnings.catch_warnings(): warnings.simplefilter("ignore", FastAPIDeprecationWarning) - app = FastAPI(default_response_class=ORJSONResponse) + app = FastAPI(default_response_class=ORJSONResponse) # ty: ignore[deprecated] @app.get("/items") def get_items() -> Item: @@ -44,7 +44,7 @@ def test_orjson_response_returns_correct_data(): @needs_orjson def test_orjson_response_emits_deprecation_warning(): with pytest.warns(FastAPIDeprecationWarning, match="ORJSONResponse is deprecated"): - ORJSONResponse(content={"hello": "world"}) + ORJSONResponse(content={"hello": "world"}) # ty: ignore[deprecated] # UJSON @@ -53,7 +53,7 @@ def test_orjson_response_emits_deprecation_warning(): def _make_ujson_app() -> FastAPI: with warnings.catch_warnings(): warnings.simplefilter("ignore", FastAPIDeprecationWarning) - app = FastAPI(default_response_class=UJSONResponse) + app = FastAPI(default_response_class=UJSONResponse) # ty: ignore[deprecated] @app.get("/items") def get_items() -> Item: @@ -76,4 +76,4 @@ def test_ujson_response_returns_correct_data(): @needs_ujson def test_ujson_response_emits_deprecation_warning(): with pytest.warns(FastAPIDeprecationWarning, match="UJSONResponse is deprecated"): - UJSONResponse(content={"hello": "world"}) + UJSONResponse(content={"hello": "world"}) # ty: ignore[deprecated] diff --git a/tests/test_inherited_custom_class.py b/tests/test_inherited_custom_class.py index 8cf8952f9..54c6566a0 100644 --- a/tests/test_inherited_custom_class.py +++ b/tests/test_inherited_custom_class.py @@ -13,7 +13,7 @@ class MyUuid: def __str__(self): return self.uuid - @property # type: ignore + @property def __class__(self): return uuid.UUID diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index c23a9e5d7..8f8bd3fcb 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -87,10 +87,10 @@ def test_encode_dict(): def test_encode_dict_include_exclude_list(): pet = {"name": "Firulais", "owner": {"name": "Foo"}} assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} - assert jsonable_encoder(pet, include=["name"]) == {"name": "Firulais"} - assert jsonable_encoder(pet, exclude=["owner"]) == {"name": "Firulais"} - assert jsonable_encoder(pet, include=[]) == {} - assert jsonable_encoder(pet, exclude=[]) == { + assert jsonable_encoder(pet, include=["name"]) == {"name": "Firulais"} # ty: ignore[invalid-argument-type] + assert jsonable_encoder(pet, exclude=["owner"]) == {"name": "Firulais"} # ty: ignore[invalid-argument-type] + assert jsonable_encoder(pet, include=[]) == {} # ty: ignore[invalid-argument-type] + assert jsonable_encoder(pet, exclude=[]) == { # ty: ignore[invalid-argument-type] "name": "Firulais", "owner": {"name": "Foo"}, } @@ -176,7 +176,7 @@ def test_encode_model_with_config(): def test_encode_model_with_alias_raises(): with pytest.raises(ValidationError): - ModelWithAlias(foo="Bar") + ModelWithAlias(foo="Bar") # ty: ignore[missing-argument, unknown-argument] def test_encode_model_with_alias(): diff --git a/tests/test_local_docs.py b/tests/test_local_docs.py index 5f102edf1..351161182 100644 --- a/tests/test_local_docs.py +++ b/tests/test_local_docs.py @@ -9,7 +9,7 @@ def test_strings_in_generated_swagger(): swagger_css_url = sig.parameters.get("swagger_css_url").default # type: ignore swagger_favicon_url = sig.parameters.get("swagger_favicon_url").default # type: ignore html = get_swagger_ui_html(openapi_url="/docs", title="title") - body_content = html.body.decode() + body_content = bytes(html.body).decode() assert swagger_js_url in body_content assert swagger_css_url in body_content assert swagger_favicon_url in body_content @@ -26,7 +26,7 @@ def test_strings_in_custom_swagger(): swagger_css_url=swagger_css_url, swagger_favicon_url=swagger_favicon_url, ) - body_content = html.body.decode() + body_content = bytes(html.body).decode() assert swagger_js_url in body_content assert swagger_css_url in body_content assert swagger_favicon_url in body_content @@ -37,7 +37,7 @@ def test_strings_in_generated_redoc(): redoc_js_url = sig.parameters.get("redoc_js_url").default # type: ignore redoc_favicon_url = sig.parameters.get("redoc_favicon_url").default # type: ignore html = get_redoc_html(openapi_url="/docs", title="title") - body_content = html.body.decode() + body_content = bytes(html.body).decode() assert redoc_js_url in body_content assert redoc_favicon_url in body_content @@ -51,17 +51,17 @@ def test_strings_in_custom_redoc(): redoc_js_url=redoc_js_url, redoc_favicon_url=redoc_favicon_url, ) - body_content = html.body.decode() + body_content = bytes(html.body).decode() assert redoc_js_url in body_content assert redoc_favicon_url in body_content def test_google_fonts_in_generated_redoc(): - body_with_google_fonts = get_redoc_html( - openapi_url="/docs", title="title" - ).body.decode() + body_with_google_fonts = bytes( + get_redoc_html(openapi_url="/docs", title="title").body + ).decode() assert "fonts.googleapis.com" in body_with_google_fonts - body_without_google_fonts = get_redoc_html( - openapi_url="/docs", title="title", with_google_fonts=False - ).body.decode() + body_without_google_fonts = bytes( + get_redoc_html(openapi_url="/docs", title="title", with_google_fonts=False).body + ).decode() assert "fonts.googleapis.com" not in body_without_google_fonts diff --git a/tests/test_openapi_schema_type.py b/tests/test_openapi_schema_type.py index e8166d2fb..610375b77 100644 --- a/tests/test_openapi_schema_type.py +++ b/tests/test_openapi_schema_type.py @@ -21,4 +21,4 @@ def test_allowed_schema_type( def test_invalid_type_value() -> None: """Test that Schema raises ValueError for invalid type values.""" with pytest.raises(ValueError, match="2 validation errors for Schema"): - Schema(type=True) # type: ignore[arg-type] + Schema(type=True) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] diff --git a/tests/test_orjson_response_class.py b/tests/test_orjson_response_class.py index 3e34041dc..499b3e585 100644 --- a/tests/test_orjson_response_class.py +++ b/tests/test_orjson_response_class.py @@ -6,13 +6,13 @@ pytest.importorskip("orjson") from fastapi import FastAPI from fastapi.exceptions import FastAPIDeprecationWarning -from fastapi.responses import ORJSONResponse +from fastapi.responses import ORJSONResponse # ty: ignore[deprecated] from fastapi.testclient import TestClient from sqlalchemy.sql.elements import quoted_name with warnings.catch_warnings(): warnings.simplefilter("ignore", FastAPIDeprecationWarning) - app = FastAPI(default_response_class=ORJSONResponse) + app = FastAPI(default_response_class=ORJSONResponse) # ty: ignore[deprecated] @app.get("/orjson_non_str_keys") diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 7be7902ad..36d50afa9 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -78,22 +78,22 @@ def no_response_model_annotation_return_same_model() -> User: @app.get("/no_response_model-annotation-return_exact_dict") def no_response_model_annotation_return_exact_dict() -> User: - return {"name": "John", "surname": "Doe"} + return {"name": "John", "surname": "Doe"} # ty: ignore[invalid-return-type] @app.get("/no_response_model-annotation-return_invalid_dict") def no_response_model_annotation_return_invalid_dict() -> User: - return {"name": "John"} + return {"name": "John"} # ty: ignore[invalid-return-type] @app.get("/no_response_model-annotation-return_invalid_model") def no_response_model_annotation_return_invalid_model() -> User: - return Item(name="Foo", price=42.0) + return Item(name="Foo", price=42.0) # ty: ignore[invalid-return-type] @app.get("/no_response_model-annotation-return_dict_with_extra_data") def no_response_model_annotation_return_dict_with_extra_data() -> User: - return {"name": "John", "surname": "Doe", "password_hash": "secret"} + return {"name": "John", "surname": "Doe", "password_hash": "secret"} # ty: ignore[invalid-return-type] @app.get("/no_response_model-annotation-return_submodel_with_extra_data") @@ -108,24 +108,24 @@ def response_model_none_annotation_return_same_model() -> User: @app.get("/response_model_none-annotation-return_exact_dict", response_model=None) def response_model_none_annotation_return_exact_dict() -> User: - return {"name": "John", "surname": "Doe"} + return {"name": "John", "surname": "Doe"} # ty: ignore[invalid-return-type] @app.get("/response_model_none-annotation-return_invalid_dict", response_model=None) def response_model_none_annotation_return_invalid_dict() -> User: - return {"name": "John"} + return {"name": "John"} # ty: ignore[invalid-return-type] @app.get("/response_model_none-annotation-return_invalid_model", response_model=None) def response_model_none_annotation_return_invalid_model() -> User: - return Item(name="Foo", price=42.0) + return Item(name="Foo", price=42.0) # ty: ignore[invalid-return-type] @app.get( "/response_model_none-annotation-return_dict_with_extra_data", response_model=None ) def response_model_none_annotation_return_dict_with_extra_data() -> User: - return {"name": "John", "surname": "Doe", "password_hash": "secret"} + return {"name": "John", "surname": "Doe", "password_hash": "secret"} # ty: ignore[invalid-return-type] @app.get( @@ -140,21 +140,21 @@ def response_model_none_annotation_return_submodel_with_extra_data() -> User: "/response_model_model1-annotation_model2-return_same_model", response_model=User ) def response_model_model1_annotation_model2_return_same_model() -> Item: - return User(name="John", surname="Doe") + return User(name="John", surname="Doe") # ty: ignore[invalid-return-type] @app.get( "/response_model_model1-annotation_model2-return_exact_dict", response_model=User ) def response_model_model1_annotation_model2_return_exact_dict() -> Item: - return {"name": "John", "surname": "Doe"} + return {"name": "John", "surname": "Doe"} # ty: ignore[invalid-return-type] @app.get( "/response_model_model1-annotation_model2-return_invalid_dict", response_model=User ) def response_model_model1_annotation_model2_return_invalid_dict() -> Item: - return {"name": "John"} + return {"name": "John"} # ty: ignore[invalid-return-type] @app.get( @@ -169,7 +169,7 @@ def response_model_model1_annotation_model2_return_invalid_model() -> Item: response_model=User, ) def response_model_model1_annotation_model2_return_dict_with_extra_data() -> Item: - return {"name": "John", "surname": "Doe", "password_hash": "secret"} + return {"name": "John", "surname": "Doe", "password_hash": "secret"} # ty: ignore[invalid-return-type] @app.get( @@ -177,7 +177,7 @@ def response_model_model1_annotation_model2_return_dict_with_extra_data() -> Ite response_model=User, ) def response_model_model1_annotation_model2_return_submodel_with_extra_data() -> Item: - return DBUser(name="John", surname="Doe", password_hash="secret") + return DBUser(name="John", surname="Doe", password_hash="secret") # ty: ignore[invalid-return-type] @app.get( diff --git a/tests/test_router_events.py b/tests/test_router_events.py index 7869a7afc..e4f5a58f0 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -31,31 +31,31 @@ def test_router_events(state: State) -> None: def main() -> dict[str, str]: return {"message": "Hello World"} - @app.on_event("startup") + @app.on_event("startup") # ty: ignore[deprecated] def app_startup() -> None: state.app_startup = True - @app.on_event("shutdown") + @app.on_event("shutdown") # ty: ignore[deprecated] def app_shutdown() -> None: state.app_shutdown = True router = APIRouter() - @router.on_event("startup") + @router.on_event("startup") # ty: ignore[deprecated] def router_startup() -> None: state.router_startup = True - @router.on_event("shutdown") + @router.on_event("shutdown") # ty: ignore[deprecated] def router_shutdown() -> None: state.router_shutdown = True sub_router = APIRouter() - @sub_router.on_event("startup") + @sub_router.on_event("startup") # ty: ignore[deprecated] def sub_router_startup() -> None: state.sub_router_startup = True - @sub_router.on_event("shutdown") + @sub_router.on_event("shutdown") # ty: ignore[deprecated] def sub_router_shutdown() -> None: state.sub_router_shutdown = True @@ -253,7 +253,7 @@ def test_router_async_shutdown_handler(state: State) -> None: def main() -> dict[str, str]: return {"message": "Hello World"} - @app.on_event("shutdown") + @app.on_event("shutdown") # ty: ignore[deprecated] async def app_shutdown() -> None: state.app_shutdown = True @@ -274,7 +274,7 @@ def test_router_sync_generator_lifespan(state: State) -> None: yield state.app_shutdown = True - app = FastAPI(lifespan=lifespan) # type: ignore[arg-type] + app = FastAPI(lifespan=lifespan) # type: ignore[invalid-argument-type] # ty: ignore[invalid-argument-type] @app.get("/") def main() -> dict[str, str]: @@ -300,7 +300,7 @@ def test_router_async_generator_lifespan(state: State) -> None: yield state.app_shutdown = True - app = FastAPI(lifespan=lifespan) # type: ignore[arg-type] + app = FastAPI(lifespan=lifespan) # type: ignore[invalid-argument-type] # ty: ignore[invalid-argument-type] @app.get("/") def main() -> dict[str, str]: diff --git a/tests/test_schema_compat_pydantic_v2.py b/tests/test_schema_compat_pydantic_v2.py index 7612c6ab5..bf47e62b2 100644 --- a/tests/test_schema_compat_pydantic_v2.py +++ b/tests/test_schema_compat_pydantic_v2.py @@ -26,7 +26,7 @@ def get_client(): @app.get("/users") async def get_user() -> User: - return {"username": "alice", "role": "admin"} + return {"username": "alice", "role": "admin"} # ty: ignore[invalid-return-type] client = TestClient(app) return client diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index bb05f7bc4..6ee55ead8 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -18,7 +18,7 @@ def get_valid(): @app.get("/items/coerce", response_model=Item) def get_coerce(): - return Item(aliased_name="coerce", price="1.0") + return Item(aliased_name="coerce", price="1.0") # ty: ignore[invalid-argument-type] @app.get("/items/validlist", response_model=list[Item]) @@ -52,7 +52,7 @@ def get_valid_exclude_unset(): response_model_exclude_unset=True, ) def get_coerce_exclude_unset(): - return Item(aliased_name="coerce", price="1.0") + return Item(aliased_name="coerce", price="1.0") # ty: ignore[invalid-argument-type] @app.get( diff --git a/tests/test_skip_defaults.py b/tests/test_skip_defaults.py index 238da7392..170cf21e3 100644 --- a/tests/test_skip_defaults.py +++ b/tests/test_skip_defaults.py @@ -29,7 +29,7 @@ class ModelDefaults(BaseModel): @app.get("/", response_model=Model, response_model_exclude_unset=True) def get_root() -> ModelSubclass: - return ModelSubclass(sub={}, y=1, z=0) + return ModelSubclass(sub={}, y=1, z=0) # ty: ignore[invalid-argument-type] @app.get( diff --git a/tests/test_sse.py b/tests/test_sse.py index 86a67f8f9..6a9d669fe 100644 --- a/tests/test_sse.py +++ b/tests/test_sse.py @@ -227,7 +227,7 @@ def test_server_sent_event_single_line_fields_reject_newlines( field_name: str, value: str ): with pytest.raises(ValueError, match=f"SSE '{field_name}' must be a single line"): - ServerSentEvent(data="test", **{field_name: value}) + ServerSentEvent(data="test", **{field_name: value}) # ty: ignore[invalid-argument-type] def test_server_sent_event_negative_retry_rejected(): @@ -237,7 +237,7 @@ def test_server_sent_event_negative_retry_rejected(): def test_server_sent_event_float_retry_rejected(): with pytest.raises(ValueError): - ServerSentEvent(data="test", retry=1.5) # type: ignore[arg-type] + ServerSentEvent(data="test", retry=1.5) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_raw_data_sent_without_json_encoding(client: TestClient): diff --git a/tests/test_starlette_urlconvertors.py b/tests/test_starlette_urlconvertors.py index 5ef1b819c..cebe3dbe8 100644 --- a/tests/test_starlette_urlconvertors.py +++ b/tests/test_starlette_urlconvertors.py @@ -32,7 +32,7 @@ def test_route_converters_int(): response = client.get("/int/5") assert response.status_code == 200, response.text assert response.json() == {"int": 5} - assert app.url_path_for("int_convertor", param=5) == "/int/5" # type: ignore + assert app.url_path_for("int_convertor", param=5) == "/int/5" def test_route_converters_float(): @@ -40,7 +40,7 @@ def test_route_converters_float(): response = client.get("/float/25.5") assert response.status_code == 200, response.text assert response.json() == {"float": 25.5} - assert app.url_path_for("float_convertor", param=25.5) == "/float/25.5" # type: ignore + assert app.url_path_for("float_convertor", param=25.5) == "/float/25.5" def test_route_converters_path(): diff --git a/tests/test_stream_cancellation.py b/tests/test_stream_cancellation.py index 20069c5f6..18e6d67d5 100644 --- a/tests/test_stream_cancellation.py +++ b/tests/test_stream_cancellation.py @@ -10,6 +10,7 @@ import anyio import pytest from fastapi import FastAPI from fastapi.responses import StreamingResponse +from starlette.types import Message, Scope pytestmark = [ pytest.mark.anyio, @@ -45,16 +46,16 @@ async def _run_asgi_and_cancel(app: FastAPI, path: str, timeout: float) -> bool: """ chunks: list[bytes] = [] - async def receive(): # type: ignore[no-untyped-def] + async def receive() -> Message: # Simulate a client that never disconnects, rely on cancellation await anyio.sleep(float("inf")) return {"type": "http.disconnect"} # pragma: no cover - async def send(message: dict) -> None: # type: ignore[type-arg] + async def send(message: Message) -> None: if message["type"] == "http.response.body": chunks.append(message.get("body", b"")) - scope = { + scope: Scope = { "type": "http", "asgi": {"version": "3.0", "spec_version": "2.0"}, "http_version": "1.1", @@ -67,7 +68,7 @@ async def _run_asgi_and_cancel(app: FastAPI, path: str, timeout: float) -> bool: } with anyio.move_on_after(timeout) as cancel_scope: - await app(scope, receive, send) # type: ignore[arg-type] + await app(scope, receive, send) # If we got here within the timeout the generator was cancellable. # cancel_scope.cancelled_caught is True when move_on_after fired. diff --git a/tests/test_swagger_ui_escape.py b/tests/test_swagger_ui_escape.py index 072d21952..6b9851abd 100644 --- a/tests/test_swagger_ui_escape.py +++ b/tests/test_swagger_ui_escape.py @@ -8,7 +8,7 @@ def test_init_oauth_html_chars_are_escaped(): title="Test", init_oauth={"appName": xss_payload}, ) - body = html.body.decode() + body = bytes(html.body).decode() assert "