mirror of
https://github.com/fastapi/fastapi.git
synced 2026-06-15 11:01:13 -04:00
Compare commits
28 Commits
check-zh-t
...
translate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479d699ab7 | ||
|
|
9a9c4ad5d0 | ||
|
|
c6d5897826 | ||
|
|
31d097f286 | ||
|
|
ba609a8946 | ||
|
|
8e1d774cef | ||
|
|
016ab760fa | ||
|
|
e2fcd5562f | ||
|
|
d3e6a2931f | ||
|
|
e4b6a36c4f | ||
|
|
944fb70622 | ||
|
|
5d2dae82c4 | ||
|
|
e680fd6c8c | ||
|
|
ee036a111e | ||
|
|
b98fb552e6 | ||
|
|
e52919282a | ||
|
|
b31720337a | ||
|
|
e133ffbc1b | ||
|
|
4be770779e | ||
|
|
2015b63561 | ||
|
|
3c5d5bb497 | ||
|
|
13b2b4cb34 | ||
|
|
b6ed4666fe | ||
|
|
8b80abfbf3 | ||
|
|
e42d913c60 | ||
|
|
0cf3f76e7c | ||
|
|
706e20da5e | ||
|
|
71ea74fb8f |
6
.github/workflows/build-docs.yml
vendored
6
.github/workflows/build-docs.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/contributors.yml
vendored
4
.github/workflows/contributors.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/create-draft-release.yml
vendored
2
.github/workflows/create-draft-release.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/deploy-docs.yml
vendored
2
.github/workflows/deploy-docs.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/label-approved.yml
vendored
2
.github/workflows/label-approved.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/latest-changes.yml
vendored
4
.github/workflows/latest-changes.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/notify-translations.yml
vendored
4
.github/workflows/notify-translations.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/people.yml
vendored
4
.github/workflows/people.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/pre-commit.yml
vendored
4
.github/workflows/pre-commit.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/prepare-release.yml
vendored
2
.github/workflows/prepare-release.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/smokeshow.yml
vendored
2
.github/workflows/smokeshow.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/sponsors.yml
vendored
4
.github/workflows/sponsors.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/test-redistribute.yml
vendored
2
.github/workflows/test-redistribute.yml
vendored
@@ -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
|
||||
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/topic-repos.yml
vendored
2
.github/workflows/topic-repos.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/translate.yml
vendored
6
.github/workflows/translate.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/zizmor.yml
vendored
2
.github/workflows/zizmor.yml
vendored
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
10
README.md
10
README.md
@@ -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
|
||||
|
||||
@@ -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 war’s! Jetzt können Sie unter dieser URL auf Ihre App zugreifen. ✨
|
||||
|
||||
#### Über FastAPI Cloud { #about-fastapi-cloud }
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. 🤓
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
///
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
///
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
1130
fastapi/routing.py
1130
fastapi/routing.py
File diff suppressed because it is too large
Load Diff
@@ -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():
|
||||
|
||||
855
tests/test_router_include_context.py
Normal file
855
tests/test_router_include_context.py
Normal 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
163
uv.lock
generated
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user