Compare commits

...

28 Commits

Author SHA1 Message Date
github-actions[bot]
479d699ab7 🌐 Update translations for de (update-outdated) 2026-06-15 06:58:28 +00:00
Sebastián Ramírez
9a9c4ad5d0 🔖 Release version 0.137.0 (#15748)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-14 12:47:28 +00:00
github-actions[bot]
c6d5897826 📝 Update release notes
[skip ci]
2026-06-14 12:40:20 +00:00
Sebastián Ramírez
31d097f286 📝 Update release notes (#15747) 2026-06-14 12:39:49 +00:00
github-actions[bot]
ba609a8946 📝 Update release notes
[skip ci]
2026-06-14 12:36:16 +00:00
Sebastián Ramírez
8e1d774cef ♻️ Refactor internals to preserve APIRouter and APIRoute instances (#15745) 2026-06-14 14:35:48 +02:00
github-actions[bot]
016ab760fa 📝 Update release notes
[skip ci]
2026-06-13 18:22:48 +00:00
Sebastián Ramírez
e2fcd5562f 🔧 Update sponsors: remove TalorData (#15744) 2026-06-13 18:22:22 +00:00
github-actions[bot]
d3e6a2931f 📝 Update release notes
[skip ci]
2026-06-10 23:14:26 +00:00
Sebastián Ramírez
e4b6a36c4f 🔧 Update sponsors: remove ExoFlare (#15736) 2026-06-10 23:13:56 +00:00
github-actions[bot]
944fb70622 📝 Update release notes
[skip ci]
2026-06-10 23:05:07 +00:00
Sebastián Ramírez
5d2dae82c4 🔧 Update sponsors: remove InterviewPal (#15735) 2026-06-10 23:04:41 +00:00
github-actions[bot]
e680fd6c8c 📝 Update release notes
[skip ci]
2026-06-10 22:59:54 +00:00
Sebastián Ramírez
ee036a111e 🔧 Update sponsors: remove Liblab (#15731) 2026-06-11 00:59:29 +02:00
github-actions[bot]
b98fb552e6 📝 Update release notes
[skip ci]
2026-06-10 22:44:58 +00:00
Sebastián Ramírez
e52919282a 🔧 Update sponsors: remove Scalar (#15730) 2026-06-10 22:44:29 +00:00
github-actions[bot]
b31720337a 📝 Update release notes
[skip ci]
2026-06-09 22:07:36 +00:00
dependabot[bot]
e133ffbc1b ⬆ Bump the python-packages group across 1 directory with 6 updates (#15721)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 00:05:55 +02:00
github-actions[bot]
4be770779e 📝 Update release notes
[skip ci]
2026-06-09 22:04:54 +00:00
dependabot[bot]
2015b63561 ⬆ Bump python-multipart from 0.0.29 to 0.0.30 (#15723)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <109919500+YuriiMotov@users.noreply.github.com>
2026-06-10 00:04:26 +02:00
github-actions[bot]
3c5d5bb497 📝 Update release notes
[skip ci]
2026-06-09 21:55:01 +00:00
dependabot[bot]
13b2b4cb34 ⬆ Bump the github-actions group with 3 updates (#15720)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <109919500+YuriiMotov@users.noreply.github.com>
2026-06-09 23:54:29 +02:00
github-actions[bot]
b6ed4666fe 📝 Update release notes
[skip ci]
2026-06-09 21:52:25 +00:00
dependabot[bot]
8b80abfbf3 ⬆ Bump starlette from 1.1.0 to 1.2.1 (#15722)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 23:51:55 +02:00
github-actions[bot]
e42d913c60 📝 Update release notes
[skip ci]
2026-06-09 21:50:10 +00:00
dependabot[bot]
0cf3f76e7c ⬆ Bump https://github.com/crate-ci/typos from v1.46.0 to v1.47.1 in the pre-commit group (#15719)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <109919500+YuriiMotov@users.noreply.github.com>
2026-06-09 23:49:44 +02:00
github-actions[bot]
706e20da5e 📝 Update release notes
[skip ci]
2026-06-09 12:05:54 +00:00
Alejandra
71ea74fb8f 📝 Update FastAPI Cloud deployment instructions (#15724)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-09 14:05:27 +02:00
50 changed files with 2044 additions and 532 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -52,9 +52,7 @@ The key features are:
### Gold Sponsors
<a href="https://blockbee.io?ref=fastapi" target="_blank" title="BlockBee Cryptocurrency Payment Gateway"><img src="https://fastapi.tiangolo.com/img/sponsors/blockbee.png"></a>
<a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a>
<a href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge" target="_blank" title="Auth, user management and more for your B2B product"><img src="https://fastapi.tiangolo.com/img/sponsors/propelauth.png"></a>
<a href="https://liblab.com?utm_source=fastapi" target="_blank" title="liblab - Generate SDKs from FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/liblab.png"></a>
<a href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" target="_blank" title="Deploy & scale any full-stack web app on Render. Focus on building apps, not infra."><img src="https://fastapi.tiangolo.com/img/sponsors/render.svg"></a>
<a href="https://www.coderabbit.ai/?utm_source=fastapi&utm_medium=badge&utm_campaign=fastapi" target="_blank" title="Cut Code Review Time & Bugs in Half with CodeRabbit"><img src="https://fastapi.tiangolo.com/img/sponsors/coderabbit.png"></a>
<a href="https://subtotal.com/?utm_source=fastapi&utm_medium=sponsorship&utm_campaign=open-source" target="_blank" title="The Gold Standard in Retail Account Linking"><img src="https://fastapi.tiangolo.com/img/sponsors/subtotal.svg"></a>
@@ -68,9 +66,7 @@ The key features are:
<a href="https://www.svix.com/" target="_blank" title="Svix - Webhooks as a service"><img src="https://fastapi.tiangolo.com/img/sponsors/svix.svg"></a>
<a href="https://www.stainlessapi.com/?utm_source=fastapi&utm_medium=referral" target="_blank" title="Stainless | Generate best-in-class SDKs"><img src="https://fastapi.tiangolo.com/img/sponsors/stainless.png"></a>
<a href="https://www.permit.io/blog/implement-authorization-in-fastapi?utm_source=github&utm_medium=referral&utm_campaign=fastapi" target="_blank" title="Fine-Grained Authorization for FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/permit.png"></a>
<a href="https://www.interviewpal.com/?utm_source=fastapi&utm_medium=open-source&utm_campaign=dev-hiring" target="_blank" title="InterviewPal - AI Interview Coach for Engineers and Devs"><img src="https://fastapi.tiangolo.com/img/sponsors/interviewpal.png"></a>
<a href="https://dribia.com/en/" target="_blank" title="Dribia - Data Science within your reach"><img src="https://fastapi.tiangolo.com/img/sponsors/dribia.png"></a>
<a href="https://talordata.com/?campaignid=oh5dVZ3Zc3YGiAI2&utm_source=fastapi&utm_term=fastapi" target="_blank" title="TalorData SERP API - Multi-Engine Search Results Data"><img src="https://fastapi.tiangolo.com/img/sponsors/talordata.png"></a>
<a href="https://www.rapidproxy.io/?ref=fastapi" target="_blank" title="Try RapidProxy for free - Residential Proxies with 90M+ Global IPs. Starting from $0.65/GB for web scraping, automation, and data collection."><img src="https://fastapi.tiangolo.com/img/sponsors/rapidproxy.png"></a>
<!-- /sponsors -->
@@ -450,9 +446,7 @@ For a more complete example including more features, see the <a href="https://fa
### 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.
You can optionally deploy your FastAPI app to [FastAPI Cloud](https://fastapicloud.com) with a single command. 🚀
<div class="termy">
@@ -468,6 +462,8 @@ Deploying to FastAPI Cloud...
</div>
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

View File

@@ -192,7 +192,7 @@ $ pip install "fastapi[standard]"
</div>
**Hinweis**: Stellen Sie sicher, dass Sie `"fastapi[standard]"` in Anführungszeichen setzen, damit es in allen Terminals funktioniert.
**Hinweis**: Stellen Sie sicher, dass Sie fastapi[standard] in Anführungszeichen setzen, damit es in allen Terminals funktioniert.
## Beispiel { #example }
@@ -492,9 +492,7 @@ Für ein vollständigeres Beispiel, mit weiteren Funktionen, siehe das <a href="
### Ihre App deployen (optional) { #deploy-your-app-optional }
Optional können Sie Ihre FastAPI-App in die [FastAPI Cloud](https://fastapicloud.com) deployen, gehen Sie und treten Sie der Warteliste bei, falls noch nicht geschehen. 🚀
Wenn Sie bereits ein **FastAPI Cloud**-Konto haben (wir haben Sie von der Warteliste eingeladen 😉), können Sie Ihre Anwendung mit einem einzigen Befehl deployen.
Optional können Sie Ihre FastAPI-App mit einem einzigen Befehl in die [FastAPI Cloud](https://fastapicloud.com) deployen. 🚀
<div class="termy">
@@ -510,6 +508,8 @@ Deploying to FastAPI Cloud...
</div>
Das CLI erkennt Ihre FastAPI-Anwendung automatisch und deployt sie in die Cloud. Wenn Sie nicht eingeloggt sind, wird Ihr Browser geöffnet, um den Authentifizierungsprozess abzuschließen.
Das wars! Jetzt können Sie unter dieser URL auf Ihre App zugreifen. ✨
#### Über FastAPI Cloud { #about-fastapi-cloud }

View File

@@ -108,7 +108,7 @@ Zum Beispiel:
{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *}
/// info | Info
/// note | Hinweis
`Body` hat die gleichen zusätzlichen Validierungs- und Metadaten-Parameter wie `Query`, `Path` und andere, die Sie später kennenlernen werden.
@@ -123,7 +123,7 @@ Standardmäßig wird **FastAPI** dann seinen Body direkt erwarten.
Aber wenn Sie möchten, dass es einen JSON-Body mit einem Schlüssel `item` erwartet, und darin den Inhalt des Modells, so wie es das tut, wenn Sie mehrere Body-Parameter deklarieren, dann können Sie den speziellen `Body`-Parameter `embed` setzen:
```Python
item: Item = Body(embed=True)
item: Annotated[Item, Body(embed=True)]
```
so wie in:

View File

@@ -8,7 +8,7 @@ Ihre API muss fast immer einen **Response**body senden. Aber Clients müssen nic
Um einen **Request**body zu deklarieren, verwenden Sie [Pydantic](https://docs.pydantic.dev/)-Modelle mit all deren Fähigkeiten und Vorzügen.
/// info | Info
/// note | Hinweis
Um Daten zu senden, sollten Sie eines von: `POST` (meistverwendet), `PUT`, `DELETE` oder `PATCH` verwenden.

View File

@@ -24,13 +24,13 @@ Aber denken Sie daran, dass, wenn Sie `Query`, `Path`, `Cookie` und andere von `
///
/// info | Info
/// note | Hinweis
Um Cookies zu deklarieren, müssen Sie `Cookie` verwenden, da die Parameter sonst als Query-Parameter interpretiert würden.
///
/// info | Info
/// note | Hinweis
Beachten Sie, dass **Browser Cookies auf besondere Weise und hinter den Kulissen handhaben** und **JavaScript** **nicht** ohne Weiteres erlauben, auf sie zuzugreifen.

View File

@@ -72,13 +72,13 @@ Sie können die Response mit dem Parameter `response_description` beschreiben:
{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *}
/// info | Info
/// note | Hinweis
Beachten Sie, dass sich `response_description` speziell auf die Response bezieht, während `description` sich generell auf die *Pfadoperation* bezieht.
///
/// check | Testen
/// tip | Tipp
OpenAPI verlangt, dass jede *Pfadoperation* über eine Beschreibung der Response verfügt.

View File

@@ -8,7 +8,7 @@ Importieren Sie zuerst `Path` von `fastapi`, und importieren Sie `Annotated`:
{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *}
/// info | Info
/// note | Hinweis
FastAPI hat in Version 0.95.0 Unterstützung für `Annotated` hinzugefügt und es zur Verwendung empfohlen.
@@ -131,7 +131,7 @@ Und Sie können auch Zahlenvalidierungen deklarieren:
* `lt`: `l`ess `t`han (kleiner als)
* `le`: `l`ess than or `e`qual (kleiner oder gleich)
/// info | Info
/// note | Hinweis
`Query`, `Path`, und andere Klassen, die Sie später sehen werden, sind Unterklassen einer gemeinsamen `Param`-Klasse.

View File

@@ -2,7 +2,7 @@
Sie können Dateien, die vom Client hochgeladen werden, mithilfe von `File` definieren.
/// info | Info
/// note | Hinweis
Um hochgeladene Dateien zu empfangen, installieren Sie zuerst [`python-multipart`](https://github.com/Kludex/python-multipart).
@@ -28,7 +28,7 @@ Erstellen Sie Datei-Parameter, so wie Sie es auch mit `Body` und `Form` machen w
{* ../../docs_src/request_files/tutorial001_an_py310.py hl[9] *}
/// info | Info
/// note | Hinweis
`File` ist eine Klasse, die direkt von `Form` erbt.

View File

@@ -2,7 +2,7 @@
Sie können gleichzeitig Dateien und Formulardaten mit `File` und `Form` definieren.
/// info | Info
/// note | Hinweis
Um hochgeladene Dateien und/oder Formulardaten zu empfangen, installieren Sie zuerst [`python-multipart`](https://github.com/Kludex/python-multipart).

View File

@@ -18,7 +18,7 @@ Beachten Sie, dass `status_code` ein Parameter der „Dekorator“-Methode ist (
Dem `status_code`-Parameter wird eine Zahl mit dem HTTP-Statuscode übergeben.
/// info | Info
/// note | Hinweis
Alternativ kann `status_code` auch ein `IntEnum` erhalten, wie etwa Pythons [`http.HTTPStatus`](https://docs.python.org/3/library/http.html#http.HTTPStatus).

View File

@@ -8,7 +8,7 @@ Damit können Sie [pytest](https://docs.pytest.org/) direkt mit **FastAPI** verw
## `TestClient` verwenden { #using-testclient }
/// info | Info
/// note | Hinweis
Um `TestClient` zu verwenden, installieren Sie zunächst [`httpx`](https://www.python-httpx.org).
@@ -145,7 +145,7 @@ Z. B.:
Weitere Informationen zum Übergeben von Daten an das Backend (mithilfe von `httpx` oder dem `TestClient`) finden Sie in der [HTTPX-Dokumentation](https://www.python-httpx.org).
/// info | Info
/// note | Hinweis
Beachten Sie, dass der `TestClient` Daten empfängt, die nach JSON konvertiert werden können, keine Pydantic-Modelle.

View File

@@ -6,15 +6,9 @@ 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
- 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
@@ -46,22 +40,13 @@ 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
- 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
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

View File

@@ -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. 🤓

View File

@@ -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.
///

View File

@@ -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

View File

@@ -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:
<div class="termy">
```console
$ fastapi login
You are logged in to FastAPI Cloud 🚀
```
</div>
## 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**. 🚀
<div class="termy">
@@ -36,6 +16,8 @@ Deploying to FastAPI Cloud...
</div>
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 }

View File

@@ -250,16 +250,6 @@ They are supporting my work with **FastAPI** (and others), mainly through [GitHu
<a href="{{ sponsor.url }}" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
{% endfor %}
{% endif %}
{% if sponsors.bronze %}
### Bronze Sponsors
{% for sponsor in sponsors.bronze -%}
<a href="{{ sponsor.url }}" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
{% endfor %}
{% endif %}
{% endif %}
### Individual Sponsors

View File

@@ -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

View File

@@ -492,9 +492,7 @@ For a more complete example including more features, see the <a href="https://fa
### 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.
You can optionally deploy your FastAPI app to [FastAPI Cloud](https://fastapicloud.com) with a single command. 🚀
<div class="termy">
@@ -510,6 +508,8 @@ Deploying to FastAPI Cloud...
</div>
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 }

View File

@@ -7,8 +7,63 @@ 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).
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.
#### 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.
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 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).
* 📝 Update `docs/en/docs/tutorial/security/oauth2-jwt.md`. PR [#14781](https://github.com/fastapi/fastapi/pull/14781) by [@zadevhub](https://github.com/zadevhub).
@@ -29,6 +84,16 @@ 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).
* 🔧 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).
* ⬆ 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).
* ⬆ 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).

View File

@@ -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.
///

View File

@@ -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:
<div class="termy">
```console
$ fastapi login
You are logged in to FastAPI Cloud 🚀
```
</div>
Then deploy your app:
You can optionally deploy your FastAPI app to [FastAPI Cloud](https://fastapicloud.com) with a single command. 🚀
<div class="termy">
@@ -232,6 +216,8 @@ Deploying to FastAPI Cloud...
</div>
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 }

View File

@@ -46,24 +46,12 @@
<img class="sponsor-image" src="/img/sponsors/blockbee-banner.png" />
</a>
</div>
<div class="item">
<a title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files" style="display: block; position: relative;" href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=top-banner" target="_blank">
<span class="sponsor-badge">sponsor</span>
<img class="sponsor-image" src="/img/sponsors/scalar-banner.svg" />
</a>
</div>
<div class="item">
<a title="Auth, user management and more for your B2B product" style="display: block; position: relative;" href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=topbanner" target="_blank">
<span class="sponsor-badge">sponsor</span>
<img class="sponsor-image" src="/img/sponsors/propelauth-banner.png" />
</a>
</div>
<div class="item">
<a title="liblab - Generate SDKs from FastAPI" style="display: block; position: relative;" href="https://liblab.com?utm_source=fastapi" target="_blank">
<span class="sponsor-badge">sponsor</span>
<img class="sponsor-image" src="/img/sponsors/liblab-banner.png" />
</a>
</div>
<div class="item">
<a title="Deploy & scale any full-stack web app on Render. Focus on building apps, not infra." style="display: block; position: relative;" href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" target="_blank">
<span class="sponsor-badge">sponsor</span>

View File

@@ -1,24 +1,14 @@
from fastapi import FastAPI
from fastapi.routing import APIRoute
app = FastAPI()
def custom_generate_unique_id(route: APIRoute) -> str:
return route.name
app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
@app.get("/items/")
async def read_items():
return [{"item_id": "Foo"}]
def use_route_names_as_operation_ids(app: FastAPI) -> None:
"""
Simplify operation IDs so that generated API clients have simpler function
names.
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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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():

View File

@@ -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"

163
uv.lock generated
View File

@@ -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]]
@@ -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]]
@@ -4970,27 +4970,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.15.14"
version = "0.15.15"
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" }
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/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" },
{ 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]]
@@ -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]]
@@ -5436,33 +5436,42 @@ wheels = [
]
[[package]]
name = "ty"
version = "0.0.40"
name = "truststore"
version = "0.10.4"
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" }
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/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" },
{ 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.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]]