mirror of
https://github.com/fastapi/fastapi.git
synced 2026-02-23 18:26:42 -05:00
Compare commits
22 Commits
0.130.0
...
rename-web
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01c0e11700 | ||
|
|
2f9c914d44 | ||
|
|
0cf27ecf88 | ||
|
|
3f30ca1a5e | ||
|
|
6af3832126 | ||
|
|
acdf52e0c8 | ||
|
|
5c863d0718 | ||
|
|
ac8621a76e | ||
|
|
22354a2530 | ||
|
|
94a1ee749e | ||
|
|
248d7fb9f5 | ||
|
|
da1937443d | ||
|
|
5161f7b42b | ||
|
|
fef2ce70d9 | ||
|
|
a3c8c37272 | ||
|
|
2826124378 | ||
|
|
4da264f0f3 | ||
|
|
c5559a66dd | ||
|
|
1cea8f659c | ||
|
|
b423b73c35 | ||
|
|
70e8558352 | ||
|
|
48e9835732 |
48
.github/workflows/test.yml
vendored
48
.github/workflows/test.yml
vendored
@@ -68,10 +68,8 @@ jobs:
|
||||
python-version: "3.13"
|
||||
coverage: coverage
|
||||
uv-resolution: highest
|
||||
# Ubuntu with 3.13 needs coverage for CodSpeed benchmarks
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.13"
|
||||
coverage: coverage
|
||||
uv-resolution: highest
|
||||
codspeed: codspeed
|
||||
- os: ubuntu-latest
|
||||
@@ -109,20 +107,10 @@ jobs:
|
||||
run: uv pip install "git+https://github.com/Kludex/starlette@main"
|
||||
- run: mkdir coverage
|
||||
- name: Test
|
||||
if: matrix.codspeed != 'codspeed'
|
||||
run: uv run --no-sync bash scripts/test.sh
|
||||
env:
|
||||
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
|
||||
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
|
||||
- name: CodSpeed benchmarks
|
||||
if: matrix.codspeed == 'codspeed'
|
||||
uses: CodSpeedHQ/action@v4
|
||||
env:
|
||||
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
|
||||
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
|
||||
with:
|
||||
mode: simulation
|
||||
run: uv run --no-sync coverage run -m pytest tests/ --codspeed
|
||||
# Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow
|
||||
- name: Store coverage files
|
||||
if: matrix.coverage == 'coverage'
|
||||
@@ -132,6 +120,39 @@ jobs:
|
||||
path: coverage
|
||||
include-hidden-files: true
|
||||
|
||||
benchmark:
|
||||
needs:
|
||||
- changes
|
||||
if: needs.changes.outputs.src == 'true' || github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
UV_PYTHON: "3.13"
|
||||
UV_RESOLUTION: highest
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
- name: Install Dependencies
|
||||
run: uv sync --no-dev --group tests --extra all
|
||||
- name: CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@v4
|
||||
with:
|
||||
mode: simulation
|
||||
run: uv run --no-sync pytest tests/benchmarks --codspeed
|
||||
|
||||
coverage-combine:
|
||||
needs:
|
||||
- test
|
||||
@@ -176,6 +197,7 @@ jobs:
|
||||
if: always()
|
||||
needs:
|
||||
- coverage-combine
|
||||
- benchmark
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
@@ -186,4 +208,4 @@ jobs:
|
||||
uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
allowed-skips: coverage-combine,test
|
||||
allowed-skips: coverage-combine,test,benchmark
|
||||
|
||||
@@ -38,13 +38,13 @@ In der Produktion hätten Sie eine der oben genannten Optionen.
|
||||
|
||||
Aber es ist der einfachste Weg, sich auf die Serverseite von WebSockets zu konzentrieren und ein funktionierendes Beispiel zu haben:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
|
||||
## Einen `websocket` erstellen { #create-a-websocket }
|
||||
|
||||
Erstellen Sie in Ihrer **FastAPI**-Anwendung einen `websocket`:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
|
||||
|
||||
/// note | Technische Details
|
||||
|
||||
@@ -58,7 +58,7 @@ Sie könnten auch `from starlette.websockets import WebSocket` verwenden.
|
||||
|
||||
In Ihrer WebSocket-Route können Sie Nachrichten `await`en und Nachrichten senden.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
|
||||
|
||||
Sie können Binär-, Text- und JSON-Daten empfangen und senden.
|
||||
|
||||
@@ -109,7 +109,7 @@ In WebSocket-Endpunkten können Sie Folgendes aus `fastapi` importieren und verw
|
||||
|
||||
Diese funktionieren auf die gleiche Weise wie für andere FastAPI-Endpunkte/*Pfadoperationen*:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info | Info
|
||||
|
||||
@@ -154,7 +154,7 @@ Damit können Sie den WebSocket verbinden und dann Nachrichten senden und empfan
|
||||
|
||||
Wenn eine WebSocket-Verbindung geschlossen wird, löst `await websocket.receive_text()` eine `WebSocketDisconnect`-Exception aus, die Sie dann wie in folgendem Beispiel abfangen und behandeln können.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
|
||||
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
|
||||
|
||||
Zum Ausprobieren:
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
88
docs/en/docs/advanced/strict-content-type.md
Normal file
88
docs/en/docs/advanced/strict-content-type.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Strict Content-Type Checking { #strict-content-type-checking }
|
||||
|
||||
By default, **FastAPI** uses strict `Content-Type` header checking for JSON request bodies, this means that JSON requests **must** include a valid `Content-Type` header (e.g. `application/json`) in order for the body to be parsed as JSON.
|
||||
|
||||
## CSRF Risk { #csrf-risk }
|
||||
|
||||
This default behavior provides protection against a class of **Cross-Site Request Forgery (CSRF)** attacks in a very specific scenario.
|
||||
|
||||
These attacks exploit the fact that browsers allow scripts to send requests without doing any CORS preflight check when they:
|
||||
|
||||
* don't have a `Content-Type` header (e.g. using `fetch()` with a `Blob` body)
|
||||
* and don't send any authentication credentials.
|
||||
|
||||
This type of attack is mainly relevant when:
|
||||
|
||||
* the application is running locally (e.g. on `localhost`) or in an internal network
|
||||
* and the application doesn't have any authentication, it expects that any request from the same network can be trusted.
|
||||
|
||||
## Example Attack { #example-attack }
|
||||
|
||||
Imagine you build a way to run a local AI agent.
|
||||
|
||||
It provides an API at
|
||||
|
||||
```
|
||||
http://localhost:8000/v1/agents/multivac
|
||||
```
|
||||
|
||||
There's also a frontend at
|
||||
|
||||
```
|
||||
http://localhost:8000
|
||||
```
|
||||
|
||||
/// tip
|
||||
|
||||
Note that both have the same host.
|
||||
|
||||
///
|
||||
|
||||
Then using the frontend you can make the AI agent do things on your behalf.
|
||||
|
||||
As it's running **locally**, and not in the open internet, you decide to **not have any authentication** set up, just trusting the access to the local network.
|
||||
|
||||
Then one of your users could install it and run it locally.
|
||||
|
||||
Then they could open a malicious website, e.g. something like
|
||||
|
||||
```
|
||||
https://evilhackers.example.com
|
||||
```
|
||||
|
||||
And that malicious website sends requests using `fetch()` with a `Blob` body to the local API at
|
||||
|
||||
```
|
||||
http://localhost:8000/v1/agents/multivac
|
||||
```
|
||||
|
||||
Even though the host of the malicious website and the local app is different, the browser won't trigger a CORS preflight request because:
|
||||
|
||||
* It's running without any authentication, it doesn't have to send any credentials.
|
||||
* The browser thinks it's not sending JSON (because of the missing `Content-Type` header).
|
||||
|
||||
Then the malicious website could make the local AI agent send angry messages to the user's ex-boss... or worse. 😅
|
||||
|
||||
## Open Internet { #open-internet }
|
||||
|
||||
If your app is in the open internet, you wouldn't "trust the network" and let anyone send privileged requests without authentication.
|
||||
|
||||
Attackers could simply run a script to send requests to your API, no need for browser interaction, so you are probably already securing any privileged endpoints.
|
||||
|
||||
In that case **this attack / risk doesn't apply to you**.
|
||||
|
||||
This risk and attack is mainly relevant when the app runs on the **local network** and that is the **only assumed protection**.
|
||||
|
||||
## Allowing Requests Without Content-Type { #allowing-requests-without-content-type }
|
||||
|
||||
If you need to support clients that don't send a `Content-Type` header, you can disable strict checking by setting `strict_content_type=False`:
|
||||
|
||||
{* ../../docs_src/strict_content_type/tutorial001_py310.py hl[4] *}
|
||||
|
||||
With this setting, requests without a `Content-Type` header will have their body parsed as JSON, which is the same behavior as older versions of FastAPI.
|
||||
|
||||
/// info
|
||||
|
||||
This behavior and configuration was added in FastAPI 0.132.0.
|
||||
|
||||
///
|
||||
@@ -38,13 +38,13 @@ In production you would have one of the options above.
|
||||
|
||||
But it's the simplest way to focus on the server-side of WebSockets and have a working example:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
|
||||
## Create a `websocket` { #create-a-websocket }
|
||||
|
||||
In your **FastAPI** application, create a `websocket`:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
|
||||
|
||||
/// note | Technical Details
|
||||
|
||||
@@ -58,7 +58,7 @@ You could also use `from starlette.websockets import WebSocket`.
|
||||
|
||||
In your WebSocket route you can `await` for messages and send messages.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
|
||||
|
||||
You can receive and send binary, text, and JSON data.
|
||||
|
||||
@@ -109,7 +109,7 @@ In WebSocket endpoints you can import from `fastapi` and use:
|
||||
|
||||
They work the same way as for other FastAPI endpoints/*path operations*:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info
|
||||
|
||||
@@ -154,7 +154,7 @@ With that you can connect the WebSocket and then send and receive messages:
|
||||
|
||||
When a WebSocket connection is closed, the `await websocket.receive_text()` will raise a `WebSocketDisconnect` exception, which you can then catch and handle like in this example.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
|
||||
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
|
||||
|
||||
To try it out:
|
||||
|
||||
|
||||
@@ -22,7 +22,13 @@ from fastapi.responses import (
|
||||
|
||||
## FastAPI Responses
|
||||
|
||||
There are a couple of custom FastAPI response classes, you can use them to optimize JSON performance.
|
||||
There were a couple of custom FastAPI response classes that were intended to optimize JSON performance.
|
||||
|
||||
However, they are now deprecated as you will now get better performance by using a [Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/).
|
||||
|
||||
That way, Pydantic will serialize the data into JSON bytes on the Rust side, which will achieve better performance than these custom JSON responses.
|
||||
|
||||
Read more about it in [Custom Response - HTML, Stream, File, others - `orjson` or Response Model](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model).
|
||||
|
||||
::: fastapi.responses.UJSONResponse
|
||||
options:
|
||||
|
||||
@@ -7,6 +7,34 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
### Internal
|
||||
|
||||
* 👥 Update FastAPI People - Experts. PR [#14972](https://github.com/fastapi/fastapi/pull/14972) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Allow skipping `benchmark` job in `test` workflow. PR [#14974](https://github.com/fastapi/fastapi/pull/14974) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
|
||||
## 0.132.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* 🔒️ Add `strict_content_type` checking for JSON requests. PR [#14978](https://github.com/fastapi/fastapi/pull/14978) by [@tiangolo](https://github.com/tiangolo).
|
||||
* Now FastAPI checks, by default, that JSON requests have a `Content-Type` header with a valid JSON value, like `application/json`, and rejects requests that don't.
|
||||
* If the clients for your app don't send a valid `Content-Type` header you can disable this with `strict_content_type=False`.
|
||||
* Check the new docs: [Strict Content-Type Checking](https://fastapi.tiangolo.com/advanced/strict-content-type/).
|
||||
|
||||
### Internal
|
||||
|
||||
* ⬆ Bump flask from 3.1.2 to 3.1.3. PR [#14949](https://github.com/fastapi/fastapi/pull/14949) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
* ⬆ Update all dependencies to use `griffelib` instead of `griffe`. PR [#14973](https://github.com/fastapi/fastapi/pull/14973) by [@svlandeg](https://github.com/svlandeg).
|
||||
* 🔨 Fix `FastAPI People` workflow. PR [#14951](https://github.com/fastapi/fastapi/pull/14951) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
* 👷 Do not run codspeed with coverage as it's not tracked. PR [#14966](https://github.com/fastapi/fastapi/pull/14966) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Do not include benchmark tests in coverage to speed up coverage processing. PR [#14965](https://github.com/fastapi/fastapi/pull/14965) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.131.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* 🗑️ Deprecate `ORJSONResponse` and `UJSONResponse`. PR [#14964](https://github.com/fastapi/fastapi/pull/14964) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.130.0
|
||||
|
||||
### Features
|
||||
|
||||
@@ -193,6 +193,7 @@ nav:
|
||||
- advanced/generate-clients.md
|
||||
- advanced/advanced-python-types.md
|
||||
- advanced/json-base64-bytes.md
|
||||
- advanced/strict-content-type.md
|
||||
- fastapi-cli.md
|
||||
- Deployment:
|
||||
- deployment/index.md
|
||||
|
||||
@@ -38,13 +38,13 @@ En producción tendrías una de las opciones anteriores.
|
||||
|
||||
Pero es la forma más sencilla de enfocarse en el lado del servidor de WebSockets y tener un ejemplo funcional:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
|
||||
## Crear un `websocket` { #create-a-websocket }
|
||||
|
||||
En tu aplicación de **FastAPI**, crea un `websocket`:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
|
||||
|
||||
/// note | Detalles Técnicos
|
||||
|
||||
@@ -58,7 +58,7 @@ También podrías usar `from starlette.websockets import WebSocket`.
|
||||
|
||||
En tu ruta de WebSocket puedes `await` para recibir mensajes y enviar mensajes.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
|
||||
|
||||
Puedes recibir y enviar datos binarios, de texto y JSON.
|
||||
|
||||
@@ -109,7 +109,7 @@ En endpoints de WebSocket puedes importar desde `fastapi` y usar:
|
||||
|
||||
Funcionan de la misma manera que para otros endpoints de FastAPI/*path operations*:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info | Información
|
||||
|
||||
@@ -154,7 +154,7 @@ Con eso puedes conectar el WebSocket y luego enviar y recibir mensajes:
|
||||
|
||||
Cuando una conexión de WebSocket se cierra, el `await websocket.receive_text()` lanzará una excepción `WebSocketDisconnect`, que puedes capturar y manejar como en este ejemplo.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
|
||||
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
|
||||
|
||||
Para probarlo:
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ En production, vous auriez l'une des options ci-dessus.
|
||||
|
||||
Mais c'est la façon la plus simple de se concentrer sur la partie serveur des WebSockets et d'avoir un exemple fonctionnel :
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
|
||||
## Créer un `websocket` { #create-a-websocket }
|
||||
|
||||
Dans votre application **FastAPI**, créez un `websocket` :
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
|
||||
|
||||
/// note | Détails techniques
|
||||
|
||||
@@ -58,7 +58,7 @@ Vous pourriez aussi utiliser `from starlette.websockets import WebSocket`.
|
||||
|
||||
Dans votre route WebSocket, vous pouvez `await` des messages et envoyer des messages.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
|
||||
|
||||
Vous pouvez recevoir et envoyer des données binaires, texte et JSON.
|
||||
|
||||
@@ -109,7 +109,7 @@ Dans les endpoints WebSocket, vous pouvez importer depuis `fastapi` et utiliser
|
||||
|
||||
Ils fonctionnent de la même manière que pour les autres endpoints/*chemins d'accès* FastAPI :
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info
|
||||
|
||||
@@ -154,7 +154,7 @@ Avec cela, vous pouvez connecter le WebSocket puis envoyer et recevoir des messa
|
||||
|
||||
Lorsqu'une connexion WebSocket est fermée, l'instruction `await websocket.receive_text()` lèvera une exception `WebSocketDisconnect`, que vous pouvez ensuite intercepter et gérer comme dans cet exemple.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
|
||||
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
|
||||
|
||||
Pour l'essayer :
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ $ pip install websockets
|
||||
|
||||
しかし、これはWebSocketsのサーバーサイドに焦点を当て、動作する例を示す最も簡単な方法です。
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
|
||||
## `websocket` を作成する { #create-a-websocket }
|
||||
|
||||
**FastAPI** アプリケーションで、`websocket` を作成します。
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
|
||||
|
||||
/// note | 技術詳細
|
||||
|
||||
@@ -58,7 +58,7 @@ $ pip install websockets
|
||||
|
||||
WebSocketルートでは、メッセージを待機して送信するために `await` を使用できます。
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
|
||||
|
||||
バイナリやテキストデータ、JSONデータを送受信できます。
|
||||
|
||||
@@ -109,7 +109,7 @@ WebSocketエンドポイントでは、`fastapi` から以下をインポート
|
||||
|
||||
これらは、他のFastAPI エンドポイント/*path operations* の場合と同じように機能します。
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info | 情報
|
||||
|
||||
@@ -154,7 +154,7 @@ $ fastapi dev main.py
|
||||
|
||||
WebSocket接続が閉じられると、 `await websocket.receive_text()` は例外 `WebSocketDisconnect` を発生させ、この例のようにキャッチして処理することができます。
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
|
||||
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
|
||||
|
||||
試してみるには、
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ $ pip install websockets
|
||||
|
||||
그러나 이는 WebSockets의 서버 측에 집중하고 동작하는 예제를 제공하는 가장 간단한 방법입니다:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
|
||||
## `websocket` 생성하기 { #create-a-websocket }
|
||||
|
||||
**FastAPI** 애플리케이션에서 `websocket`을 생성합니다:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
|
||||
|
||||
/// note | 기술 세부사항
|
||||
|
||||
@@ -58,7 +58,7 @@ $ pip install websockets
|
||||
|
||||
WebSocket 경로에서 메시지를 대기(`await`)하고 전송할 수 있습니다.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
|
||||
|
||||
여러분은 이진 데이터, 텍스트, JSON 데이터를 받을 수 있고 전송할 수 있습니다.
|
||||
|
||||
@@ -109,7 +109,7 @@ WebSocket 엔드포인트에서 `fastapi`에서 다음을 가져와 사용할
|
||||
|
||||
이들은 다른 FastAPI 엔드포인트/*경로 처리*와 동일하게 동작합니다:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info | 정보
|
||||
|
||||
@@ -154,7 +154,7 @@ $ fastapi dev main.py
|
||||
|
||||
WebSocket 연결이 닫히면, `await websocket.receive_text()`가 `WebSocketDisconnect` 예외를 발생시킵니다. 그러면 이 예제처럼 이를 잡아 처리할 수 있습니다.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
|
||||
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
|
||||
|
||||
테스트해보기:
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ Na produção, você teria uma das opções acima.
|
||||
|
||||
Mas é a maneira mais simples de focar no lado do servidor de WebSockets e ter um exemplo funcional:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
|
||||
## Crie um `websocket` { #create-a-websocket }
|
||||
|
||||
Em sua aplicação **FastAPI**, crie um `websocket`:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
|
||||
|
||||
/// note | Detalhes Técnicos
|
||||
|
||||
@@ -58,7 +58,7 @@ A **FastAPI** fornece o mesmo `WebSocket` diretamente apenas como uma conveniên
|
||||
|
||||
Em sua rota WebSocket você pode esperar (`await`) por mensagens e enviar mensagens.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
|
||||
|
||||
Você pode receber e enviar dados binários, de texto e JSON.
|
||||
|
||||
@@ -109,7 +109,7 @@ Nos endpoints WebSocket você pode importar do `fastapi` e usar:
|
||||
|
||||
Eles funcionam da mesma forma que para outros endpoints FastAPI/*operações de rota*:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info | Informação
|
||||
|
||||
@@ -154,7 +154,7 @@ Com isso você pode conectar o WebSocket e então enviar e receber mensagens:
|
||||
|
||||
Quando uma conexão WebSocket é fechada, o `await websocket.receive_text()` levantará uma exceção `WebSocketDisconnect`, que você pode então capturar e lidar como neste exemplo.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
|
||||
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
|
||||
|
||||
Para testar:
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ $ pip install websockets
|
||||
|
||||
Для примера нам нужен наиболее простой способ, который позволит сосредоточиться на серверной части веб‑сокетов и получить рабочий код:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
|
||||
## Создание `websocket` { #create-a-websocket }
|
||||
|
||||
Создайте `websocket` в своем **FastAPI** приложении:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
|
||||
|
||||
/// note | Технические детали
|
||||
|
||||
@@ -58,7 +58,7 @@ $ pip install websockets
|
||||
|
||||
Через эндпоинт веб-сокета вы можете получать и отправлять сообщения.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
|
||||
|
||||
Вы можете получать и отправлять двоичные, текстовые и JSON данные.
|
||||
|
||||
@@ -109,7 +109,7 @@ $ fastapi dev main.py
|
||||
|
||||
Они работают так же, как и в других FastAPI эндпоинтах/*операциях пути*:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info | Примечание
|
||||
|
||||
@@ -154,7 +154,7 @@ $ fastapi dev main.py
|
||||
|
||||
Если веб-сокет соединение закрыто, то `await websocket.receive_text()` вызовет исключение `WebSocketDisconnect`, которое можно поймать и обработать как в этом примере:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
|
||||
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
|
||||
|
||||
Чтобы воспроизвести пример:
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ Production'da yukarıdaki seçeneklerden birini kullanırsınız.
|
||||
|
||||
Ama WebSockets'in server tarafına odaklanmak ve çalışan bir örnek görmek için en basit yol bu:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
|
||||
## Bir `websocket` Oluşturun { #create-a-websocket }
|
||||
|
||||
**FastAPI** uygulamanızda bir `websocket` oluşturun:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
|
||||
|
||||
/// note | Teknik Detaylar
|
||||
|
||||
@@ -58,7 +58,7 @@ Ama WebSockets'in server tarafına odaklanmak ve çalışan bir örnek görmek i
|
||||
|
||||
WebSocket route'unuzda mesajları `await` edebilir ve mesaj gönderebilirsiniz.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
|
||||
|
||||
Binary, text ve JSON verisi alıp gönderebilirsiniz.
|
||||
|
||||
@@ -109,7 +109,7 @@ WebSocket endpoint'lerinde `fastapi` içinden import edip şunları kullanabilir
|
||||
|
||||
Diğer FastAPI endpoint'leri/*path operations* ile aynı şekilde çalışırlar:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info | Bilgi
|
||||
|
||||
@@ -154,7 +154,7 @@ Bununla WebSocket'e bağlanabilir, ardından mesaj gönderip alabilirsiniz:
|
||||
|
||||
Bir WebSocket bağlantısı kapandığında, `await websocket.receive_text()` bir `WebSocketDisconnect` exception'ı raise eder; ardından bunu bu örnekteki gibi yakalayıp (catch) yönetebilirsiniz.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
|
||||
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
|
||||
|
||||
Denemek için:
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ $ pip install websockets
|
||||
|
||||
Але це найпростіший спосіб зосередитися на серверній частині WebSockets і мати робочий приклад:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
|
||||
## Створіть `websocket` { #create-a-websocket }
|
||||
|
||||
У вашому застосунку **FastAPI** створіть `websocket`:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
@@ -58,7 +58,7 @@ $ pip install websockets
|
||||
|
||||
У вашому маршруті WebSocket ви можете `await` повідомлення і надсилати повідомлення.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
|
||||
|
||||
Ви можете отримувати та надсилати бінарні, текстові та JSON-дані.
|
||||
|
||||
@@ -109,7 +109,7 @@ $ fastapi dev main.py
|
||||
|
||||
Вони працюють так само, як для інших ендпойнтів FastAPI/*операцій шляху*:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info
|
||||
|
||||
@@ -154,7 +154,7 @@ $ fastapi dev main.py
|
||||
|
||||
Коли з'єднання WebSocket закривається, `await websocket.receive_text()` підніме виняток `WebSocketDisconnect`, який ви можете перехопити й обробити, як у цьому прикладі.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
|
||||
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
|
||||
|
||||
Щоб спробувати:
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ $ pip install websockets
|
||||
|
||||
但這是能讓我們專注於 WebSocket 伺服端並跑起一個可運作範例的最簡單方式:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
|
||||
## 建立一個 `websocket` { #create-a-websocket }
|
||||
|
||||
在你的 **FastAPI** 應用中,建立一個 `websocket`:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
|
||||
|
||||
/// note | 技術細節
|
||||
|
||||
@@ -58,7 +58,7 @@ $ pip install websockets
|
||||
|
||||
在你的 WebSocket 路由中,你可以 `await` 接收訊息並傳送訊息。
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
|
||||
|
||||
你可以接收與傳送二進位、文字與 JSON 資料。
|
||||
|
||||
@@ -109,7 +109,7 @@ $ fastapi dev main.py
|
||||
|
||||
它們的運作方式與其他 FastAPI 端點/*路徑操作* 相同:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info
|
||||
|
||||
@@ -154,7 +154,7 @@ $ fastapi dev main.py
|
||||
|
||||
當 WebSocket 連線關閉時,`await websocket.receive_text()` 會拋出 `WebSocketDisconnect` 例外,你可以像範例中那樣捕捉並處理。
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
|
||||
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
|
||||
|
||||
試用方式:
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ $ pip install websockets
|
||||
|
||||
但这是一种专注于 WebSockets 的服务器端并提供一个工作示例的最简单方式:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
|
||||
|
||||
## 创建 `websocket` { #create-a-websocket }
|
||||
|
||||
在您的 **FastAPI** 应用程序中,创建一个 `websocket`:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
@@ -58,7 +58,7 @@ $ pip install websockets
|
||||
|
||||
在您的 WebSocket 路由中,您可以使用 `await` 等待消息并发送消息。
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
|
||||
|
||||
您可以接收和发送二进制、文本和 JSON 数据。
|
||||
|
||||
@@ -109,7 +109,7 @@ $ fastapi dev main.py
|
||||
|
||||
它们的工作方式与其他 FastAPI 端点/*路径操作* 相同:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info
|
||||
|
||||
@@ -154,7 +154,7 @@ $ fastapi dev main.py
|
||||
|
||||
当 WebSocket 连接关闭时,`await websocket.receive_text()` 将引发 `WebSocketDisconnect` 异常,您可以捕获并处理该异常,就像本示例中的示例一样。
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
|
||||
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
|
||||
|
||||
尝试以下操作:
|
||||
|
||||
|
||||
14
docs_src/strict_content_type/tutorial001_py310.py
Normal file
14
docs_src/strict_content_type/tutorial001_py310.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI(strict_content_type=False)
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
@app.post("/items/")
|
||||
async def create_item(item: Item):
|
||||
return item
|
||||
0
docs_src/websockets_/__init__.py
Normal file
0
docs_src/websockets_/__init__.py
Normal file
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.130.0"
|
||||
__version__ = "0.132.0"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -840,6 +840,29 @@ class FastAPI(Starlette):
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
strict_content_type: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
Enable strict checking for request Content-Type headers.
|
||||
|
||||
When `True` (the default), requests with a body that do not include
|
||||
a `Content-Type` header will **not** be parsed as JSON.
|
||||
|
||||
This prevents potential cross-site request forgery (CSRF) attacks
|
||||
that exploit the browser's ability to send requests without a
|
||||
Content-Type header, bypassing CORS preflight checks. In particular
|
||||
applicable for apps that need to be run locally (in localhost).
|
||||
|
||||
When `False`, requests without a `Content-Type` header will have
|
||||
their body parsed as JSON, which maintains compatibility with
|
||||
certain clients that don't send `Content-Type` headers.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/).
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
**extra: Annotated[
|
||||
Any,
|
||||
Doc(
|
||||
@@ -974,6 +997,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema=include_in_schema,
|
||||
responses=responses,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
strict_content_type=strict_content_type,
|
||||
)
|
||||
self.exception_handlers: dict[
|
||||
Any, Callable[[Request, Any], Response | Awaitable[Response]]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from starlette.responses import FileResponse as FileResponse # noqa
|
||||
from starlette.responses import HTMLResponse as HTMLResponse # noqa
|
||||
from starlette.responses import JSONResponse as JSONResponse # noqa
|
||||
@@ -7,6 +8,7 @@ from starlette.responses import PlainTextResponse as PlainTextResponse # noqa
|
||||
from starlette.responses import RedirectResponse as RedirectResponse # noqa
|
||||
from starlette.responses import Response as Response # noqa
|
||||
from starlette.responses import StreamingResponse as StreamingResponse # noqa
|
||||
from typing_extensions import deprecated
|
||||
|
||||
try:
|
||||
import ujson
|
||||
@@ -20,12 +22,29 @@ except ImportError: # pragma: nocover
|
||||
orjson = None # type: ignore
|
||||
|
||||
|
||||
@deprecated(
|
||||
"UJSONResponse is deprecated, FastAPI now serializes data directly to JSON "
|
||||
"bytes via Pydantic when a return type or response model is set, which is "
|
||||
"faster and doesn't need a custom response class. Read more in the FastAPI "
|
||||
"docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model "
|
||||
"and https://fastapi.tiangolo.com/tutorial/response-model/",
|
||||
category=FastAPIDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
class UJSONResponse(JSONResponse):
|
||||
"""
|
||||
JSON response using the high-performance ujson library to serialize data to JSON.
|
||||
"""JSON response using the ujson library to serialize data to JSON.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
|
||||
**Deprecated**: `UJSONResponse` is deprecated. FastAPI now serializes data
|
||||
directly to JSON bytes via Pydantic when a return type or response model is
|
||||
set, which is faster and doesn't need a custom response class.
|
||||
|
||||
Read more in the
|
||||
[FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model)
|
||||
and the
|
||||
[FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/).
|
||||
|
||||
**Note**: `ujson` is not included with FastAPI and must be installed
|
||||
separately, e.g. `pip install ujson`.
|
||||
"""
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
@@ -33,12 +52,29 @@ class UJSONResponse(JSONResponse):
|
||||
return ujson.dumps(content, ensure_ascii=False).encode("utf-8")
|
||||
|
||||
|
||||
@deprecated(
|
||||
"ORJSONResponse is deprecated, FastAPI now serializes data directly to JSON "
|
||||
"bytes via Pydantic when a return type or response model is set, which is "
|
||||
"faster and doesn't need a custom response class. Read more in the FastAPI "
|
||||
"docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model "
|
||||
"and https://fastapi.tiangolo.com/tutorial/response-model/",
|
||||
category=FastAPIDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
class ORJSONResponse(JSONResponse):
|
||||
"""
|
||||
JSON response using the high-performance orjson library to serialize data to JSON.
|
||||
"""JSON response using the orjson library to serialize data to JSON.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
|
||||
**Deprecated**: `ORJSONResponse` is deprecated. FastAPI now serializes data
|
||||
directly to JSON bytes via Pydantic when a return type or response model is
|
||||
set, which is faster and doesn't need a custom response class.
|
||||
|
||||
Read more in the
|
||||
[FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model)
|
||||
and the
|
||||
[FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/).
|
||||
|
||||
**Note**: `orjson` is not included with FastAPI and must be installed
|
||||
separately, e.g. `pip install orjson`.
|
||||
"""
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
|
||||
@@ -329,6 +329,7 @@ def get_request_handler(
|
||||
response_model_exclude_none: bool = False,
|
||||
dependency_overrides_provider: Any | None = None,
|
||||
embed_body_fields: bool = False,
|
||||
strict_content_type: bool | DefaultPlaceholder = Default(True),
|
||||
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
||||
assert dependant.call is not None, "dependant.call must be a function"
|
||||
is_coroutine = dependant.is_coroutine_callable
|
||||
@@ -337,6 +338,10 @@ def get_request_handler(
|
||||
actual_response_class: type[Response] = response_class.value
|
||||
else:
|
||||
actual_response_class = response_class
|
||||
if isinstance(strict_content_type, DefaultPlaceholder):
|
||||
actual_strict_content_type: bool = strict_content_type.value
|
||||
else:
|
||||
actual_strict_content_type = strict_content_type
|
||||
|
||||
async def app(request: Request) -> Response:
|
||||
response: Response | None = None
|
||||
@@ -370,7 +375,8 @@ def get_request_handler(
|
||||
json_body: Any = Undefined
|
||||
content_type_value = request.headers.get("content-type")
|
||||
if not content_type_value:
|
||||
json_body = await request.json()
|
||||
if not actual_strict_content_type:
|
||||
json_body = await request.json()
|
||||
else:
|
||||
message = email.message.Message()
|
||||
message["content-type"] = content_type_value
|
||||
@@ -599,6 +605,7 @@ class APIRoute(routing.Route):
|
||||
openapi_extra: dict[str, Any] | None = None,
|
||||
generate_unique_id_function: Callable[["APIRoute"], str]
|
||||
| DefaultPlaceholder = Default(generate_unique_id),
|
||||
strict_content_type: bool | DefaultPlaceholder = Default(True),
|
||||
) -> None:
|
||||
self.path = path
|
||||
self.endpoint = endpoint
|
||||
@@ -625,6 +632,7 @@ class APIRoute(routing.Route):
|
||||
self.callbacks = callbacks
|
||||
self.openapi_extra = openapi_extra
|
||||
self.generate_unique_id_function = generate_unique_id_function
|
||||
self.strict_content_type = strict_content_type
|
||||
self.tags = tags or []
|
||||
self.responses = responses or {}
|
||||
self.name = get_name(endpoint) if name is None else name
|
||||
@@ -713,6 +721,7 @@ class APIRoute(routing.Route):
|
||||
response_model_exclude_none=self.response_model_exclude_none,
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
embed_body_fields=self._embed_body_fields,
|
||||
strict_content_type=self.strict_content_type,
|
||||
)
|
||||
|
||||
def matches(self, scope: Scope) -> tuple[Match, Scope]:
|
||||
@@ -963,6 +972,29 @@ class APIRouter(routing.Router):
|
||||
"""
|
||||
),
|
||||
] = Default(generate_unique_id),
|
||||
strict_content_type: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
Enable strict checking for request Content-Type headers.
|
||||
|
||||
When `True` (the default), requests with a body that do not include
|
||||
a `Content-Type` header will **not** be parsed as JSON.
|
||||
|
||||
This prevents potential cross-site request forgery (CSRF) attacks
|
||||
that exploit the browser's ability to send requests without a
|
||||
Content-Type header, bypassing CORS preflight checks. In particular
|
||||
applicable for apps that need to be run locally (in localhost).
|
||||
|
||||
When `False`, requests without a `Content-Type` header will have
|
||||
their body parsed as JSON, which maintains compatibility with
|
||||
certain clients that don't send `Content-Type` headers.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/).
|
||||
"""
|
||||
),
|
||||
] = Default(True),
|
||||
) -> None:
|
||||
# Determine the lifespan context to use
|
||||
if lifespan is None:
|
||||
@@ -1009,6 +1041,7 @@ class APIRouter(routing.Router):
|
||||
self.route_class = route_class
|
||||
self.default_response_class = default_response_class
|
||||
self.generate_unique_id_function = generate_unique_id_function
|
||||
self.strict_content_type = strict_content_type
|
||||
|
||||
def route(
|
||||
self,
|
||||
@@ -1059,6 +1092,7 @@ class APIRouter(routing.Router):
|
||||
openapi_extra: dict[str, Any] | None = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str]
|
||||
| DefaultPlaceholder = Default(generate_unique_id),
|
||||
strict_content_type: bool | DefaultPlaceholder = Default(True),
|
||||
) -> None:
|
||||
route_class = route_class_override or self.route_class
|
||||
responses = responses or {}
|
||||
@@ -1105,6 +1139,9 @@ class APIRouter(routing.Router):
|
||||
callbacks=current_callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=current_generate_unique_id,
|
||||
strict_content_type=get_value_or_default(
|
||||
strict_content_type, self.strict_content_type
|
||||
),
|
||||
)
|
||||
self.routes.append(route)
|
||||
|
||||
@@ -1480,6 +1517,11 @@ class APIRouter(routing.Router):
|
||||
callbacks=current_callbacks,
|
||||
openapi_extra=route.openapi_extra,
|
||||
generate_unique_id_function=current_generate_unique_id,
|
||||
strict_content_type=get_value_or_default(
|
||||
route.strict_content_type,
|
||||
router.strict_content_type,
|
||||
self.strict_content_type,
|
||||
),
|
||||
)
|
||||
elif isinstance(route, routing.Route):
|
||||
methods = list(route.methods or [])
|
||||
|
||||
@@ -105,10 +105,6 @@ all = [
|
||||
"itsdangerous >=1.1.0",
|
||||
# For Starlette's schema generation, would not be used with FastAPI
|
||||
"pyyaml >=5.3.1",
|
||||
# For UJSONResponse
|
||||
"ujson >=5.8.0",
|
||||
# For ORJSONResponse
|
||||
"orjson >=3.9.3",
|
||||
# To validate email fields
|
||||
"email-validator >=2.0.0",
|
||||
# Uvicorn with uvloop
|
||||
@@ -151,6 +147,10 @@ docs = [
|
||||
docs-tests = [
|
||||
"httpx >=0.23.0,<1.0.0",
|
||||
"ruff >=0.14.14",
|
||||
# For UJSONResponse
|
||||
"ujson >=5.8.0",
|
||||
# For ORJSONResponse
|
||||
"orjson >=3.9.3",
|
||||
]
|
||||
github-actions = [
|
||||
"httpx >=0.27.0,<1.0.0",
|
||||
@@ -242,6 +242,7 @@ relative_files = true
|
||||
context = '${CONTEXT}'
|
||||
dynamic_context = "test_function"
|
||||
omit = [
|
||||
"tests/benchmarks/*",
|
||||
"docs_src/response_model/tutorial003_04_py39.py",
|
||||
"docs_src/response_model/tutorial003_04_py310.py",
|
||||
"docs_src/dependencies/tutorial013_an_py310.py", # temporary code example?
|
||||
|
||||
@@ -5,6 +5,7 @@ import time
|
||||
from collections import Counter
|
||||
from collections.abc import Container
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import ceil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -15,12 +16,63 @@ from pydantic import BaseModel, SecretStr
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
github_graphql_url = "https://api.github.com/graphql"
|
||||
questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0"
|
||||
questions_category_id = "DIC_kwDOCZduT84B6E2a"
|
||||
|
||||
|
||||
POINTS_PER_MINUTE_LIMIT = 84 # 5000 points per hour
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
def __init__(self) -> None:
|
||||
self.last_query_cost: int = 1
|
||||
self.remaining_points: int = 5000
|
||||
self.reset_at: datetime = datetime.fromtimestamp(0, timezone.utc)
|
||||
self.last_request_start_time: datetime = datetime.fromtimestamp(0, timezone.utc)
|
||||
self.speed_multiplier: float = 1.0
|
||||
|
||||
def __enter__(self) -> "RateLimiter":
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
|
||||
# Handle primary rate limits
|
||||
primary_limit_wait_time = 0.0
|
||||
if self.remaining_points <= self.last_query_cost:
|
||||
primary_limit_wait_time = (self.reset_at - now).total_seconds() + 2
|
||||
logging.warning(
|
||||
f"Approaching GitHub API rate limit, remaining points: {self.remaining_points}, "
|
||||
f"reset time in {primary_limit_wait_time} seconds"
|
||||
)
|
||||
|
||||
# Handle secondary rate limits
|
||||
secondary_limit_wait_time = 0.0
|
||||
points_per_minute = POINTS_PER_MINUTE_LIMIT * self.speed_multiplier
|
||||
interval = 60 / (points_per_minute / self.last_query_cost)
|
||||
time_since_last_request = (now - self.last_request_start_time).total_seconds()
|
||||
if time_since_last_request < interval:
|
||||
secondary_limit_wait_time = interval - time_since_last_request
|
||||
|
||||
final_wait_time = ceil(max(primary_limit_wait_time, secondary_limit_wait_time))
|
||||
logging.info(f"Sleeping for {final_wait_time} seconds to respect rate limit")
|
||||
time.sleep(max(final_wait_time, 1))
|
||||
|
||||
self.last_request_start_time = datetime.now(tz=timezone.utc)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
pass
|
||||
|
||||
def update_request_info(self, cost: int, remaining: int, reset_at: str) -> None:
|
||||
self.last_query_cost = cost
|
||||
self.remaining_points = remaining
|
||||
self.reset_at = datetime.fromisoformat(reset_at.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
rate_limiter = RateLimiter()
|
||||
|
||||
|
||||
discussions_query = """
|
||||
query Q($after: String, $category_id: ID) {
|
||||
repository(name: "fastapi", owner: "fastapi") {
|
||||
discussions(first: 100, after: $after, categoryId: $category_id) {
|
||||
discussions(first: 30, after: $after, categoryId: $category_id) {
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
@@ -58,6 +110,11 @@ query Q($after: String, $category_id: ID) {
|
||||
}
|
||||
}
|
||||
}
|
||||
rateLimit {
|
||||
cost
|
||||
remaining
|
||||
resetAt
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -120,7 +177,7 @@ class Settings(BaseSettings):
|
||||
github_token: SecretStr
|
||||
github_repository: str
|
||||
httpx_timeout: int = 30
|
||||
sleep_interval: int = 5
|
||||
speed_multiplier: float = 1.0
|
||||
|
||||
|
||||
def get_graphql_response(
|
||||
@@ -158,11 +215,18 @@ def get_graphql_question_discussion_edges(
|
||||
settings: Settings,
|
||||
after: str | None = None,
|
||||
) -> list[DiscussionsEdge]:
|
||||
data = get_graphql_response(
|
||||
settings=settings,
|
||||
query=discussions_query,
|
||||
after=after,
|
||||
category_id=questions_category_id,
|
||||
with rate_limiter:
|
||||
data = get_graphql_response(
|
||||
settings=settings,
|
||||
query=discussions_query,
|
||||
after=after,
|
||||
category_id=questions_category_id,
|
||||
)
|
||||
|
||||
rate_limiter.update_request_info(
|
||||
cost=data["data"]["rateLimit"]["cost"],
|
||||
remaining=data["data"]["rateLimit"]["remaining"],
|
||||
reset_at=data["data"]["rateLimit"]["resetAt"],
|
||||
)
|
||||
graphql_response = DiscussionsResponse.model_validate(data)
|
||||
return graphql_response.data.repository.discussions.edges
|
||||
@@ -185,8 +249,6 @@ def get_discussion_nodes(settings: Settings) -> list[DiscussionsNode]:
|
||||
for discussion_edge in discussion_edges:
|
||||
discussion_nodes.append(discussion_edge.node)
|
||||
last_edge = discussion_edges[-1]
|
||||
# Handle GitHub secondary rate limits, requests per minute
|
||||
time.sleep(settings.sleep_interval)
|
||||
discussion_edges = get_graphql_question_discussion_edges(
|
||||
settings=settings, after=last_edge.cursor
|
||||
)
|
||||
@@ -318,6 +380,7 @@ def main() -> None:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
settings = Settings()
|
||||
logging.info(f"Using config: {settings.model_dump_json()}")
|
||||
rate_limiter.speed_multiplier = settings.speed_multiplier
|
||||
g = Github(settings.github_token.get_secret_value())
|
||||
repo = g.get_repo(settings.github_repository)
|
||||
|
||||
|
||||
73
tests/test_deprecated_responses.py
Normal file
73
tests/test_deprecated_responses.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from fastapi.responses import ORJSONResponse, UJSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
# ORJSON
|
||||
|
||||
|
||||
def _make_orjson_app() -> FastAPI:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
|
||||
@app.get("/items")
|
||||
def get_items() -> Item:
|
||||
return Item(name="widget", price=9.99)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_orjson_response_returns_correct_data():
|
||||
app = _make_orjson_app()
|
||||
client = TestClient(app)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
response = client.get("/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "widget", "price": 9.99}
|
||||
|
||||
|
||||
def test_orjson_response_emits_deprecation_warning():
|
||||
with pytest.warns(FastAPIDeprecationWarning, match="ORJSONResponse is deprecated"):
|
||||
ORJSONResponse(content={"hello": "world"})
|
||||
|
||||
|
||||
# UJSON
|
||||
|
||||
|
||||
def _make_ujson_app() -> FastAPI:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
app = FastAPI(default_response_class=UJSONResponse)
|
||||
|
||||
@app.get("/items")
|
||||
def get_items() -> Item:
|
||||
return Item(name="widget", price=9.99)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_ujson_response_returns_correct_data():
|
||||
app = _make_ujson_app()
|
||||
client = TestClient(app)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
response = client.get("/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "widget", "price": 9.99}
|
||||
|
||||
|
||||
def test_ujson_response_emits_deprecation_warning():
|
||||
with pytest.warns(FastAPIDeprecationWarning, match="UJSONResponse is deprecated"):
|
||||
UJSONResponse(content={"hello": "world"})
|
||||
@@ -1,9 +1,14 @@
|
||||
import warnings
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
|
||||
|
||||
@app.get("/orjson_non_str_keys")
|
||||
@@ -16,6 +21,8 @@ client = TestClient(app)
|
||||
|
||||
|
||||
def test_orjson_non_str_keys():
|
||||
with client:
|
||||
response = client.get("/orjson_non_str_keys")
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
with client:
|
||||
response = client.get("/orjson_non_str_keys")
|
||||
assert response.json() == {"msg": "Hello World", "1": 1}
|
||||
|
||||
44
tests/test_strict_content_type_app_level.py
Normal file
44
tests/test_strict_content_type_app_level.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
app_default = FastAPI()
|
||||
|
||||
|
||||
@app_default.post("/items/")
|
||||
async def app_default_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
app_lax = FastAPI(strict_content_type=False)
|
||||
|
||||
|
||||
@app_lax.post("/items/")
|
||||
async def app_lax_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
client_default = TestClient(app_default)
|
||||
client_lax = TestClient(app_lax)
|
||||
|
||||
|
||||
def test_default_strict_rejects_no_content_type():
|
||||
response = client_default.post("/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_default_strict_accepts_json_content_type():
|
||||
response = client_default.post("/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"key": "value"}
|
||||
|
||||
|
||||
def test_lax_accepts_no_content_type():
|
||||
response = client_lax.post("/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"key": "value"}
|
||||
|
||||
|
||||
def test_lax_accepts_json_content_type():
|
||||
response = client_lax.post("/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"key": "value"}
|
||||
91
tests/test_strict_content_type_nested.py
Normal file
91
tests/test_strict_content_type_nested.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
# Lax app with nested routers, inner overrides to strict
|
||||
|
||||
app_nested = FastAPI(strict_content_type=False) # lax app
|
||||
outer_router = APIRouter(prefix="/outer") # inherits lax from app
|
||||
inner_strict = APIRouter(prefix="/strict", strict_content_type=True)
|
||||
inner_default = APIRouter(prefix="/default")
|
||||
|
||||
|
||||
@inner_strict.post("/items/")
|
||||
async def inner_strict_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
@inner_default.post("/items/")
|
||||
async def inner_default_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
outer_router.include_router(inner_strict)
|
||||
outer_router.include_router(inner_default)
|
||||
app_nested.include_router(outer_router)
|
||||
|
||||
client_nested = TestClient(app_nested)
|
||||
|
||||
|
||||
def test_strict_inner_on_lax_app_rejects_no_content_type():
|
||||
response = client_nested.post("/outer/strict/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_default_inner_inherits_lax_from_app():
|
||||
response = client_nested.post("/outer/default/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"key": "value"}
|
||||
|
||||
|
||||
def test_strict_inner_accepts_json_content_type():
|
||||
response = client_nested.post("/outer/strict/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_default_inner_accepts_json_content_type():
|
||||
response = client_nested.post("/outer/default/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# Strict app -> lax outer router -> strict inner router
|
||||
|
||||
app_mixed = FastAPI(strict_content_type=True)
|
||||
mixed_outer = APIRouter(prefix="/outer", strict_content_type=False)
|
||||
mixed_inner = APIRouter(prefix="/inner", strict_content_type=True)
|
||||
|
||||
|
||||
@mixed_outer.post("/items/")
|
||||
async def mixed_outer_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
@mixed_inner.post("/items/")
|
||||
async def mixed_inner_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
mixed_outer.include_router(mixed_inner)
|
||||
app_mixed.include_router(mixed_outer)
|
||||
|
||||
client_mixed = TestClient(app_mixed)
|
||||
|
||||
|
||||
def test_lax_outer_on_strict_app_accepts_no_content_type():
|
||||
response = client_mixed.post("/outer/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"key": "value"}
|
||||
|
||||
|
||||
def test_strict_inner_on_lax_outer_rejects_no_content_type():
|
||||
response = client_mixed.post("/outer/inner/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_lax_outer_accepts_json_content_type():
|
||||
response = client_mixed.post("/outer/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_strict_inner_on_lax_outer_accepts_json_content_type():
|
||||
response = client_mixed.post("/outer/inner/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
61
tests/test_strict_content_type_router_level.py
Normal file
61
tests/test_strict_content_type_router_level.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
router_lax = APIRouter(prefix="/lax", strict_content_type=False)
|
||||
router_strict = APIRouter(prefix="/strict", strict_content_type=True)
|
||||
router_default = APIRouter(prefix="/default")
|
||||
|
||||
|
||||
@router_lax.post("/items/")
|
||||
async def router_lax_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
@router_strict.post("/items/")
|
||||
async def router_strict_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
@router_default.post("/items/")
|
||||
async def router_default_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
app.include_router(router_lax)
|
||||
app.include_router(router_strict)
|
||||
app.include_router(router_default)
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_lax_router_on_strict_app_accepts_no_content_type():
|
||||
response = client.post("/lax/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"key": "value"}
|
||||
|
||||
|
||||
def test_strict_router_on_strict_app_rejects_no_content_type():
|
||||
response = client.post("/strict/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_default_router_inherits_strict_from_app():
|
||||
response = client.post("/default/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_lax_router_accepts_json_content_type():
|
||||
response = client.post("/lax/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_strict_router_accepts_json_content_type():
|
||||
response = client.post("/strict/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_default_router_accepts_json_content_type():
|
||||
response = client.post("/default/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
@@ -189,18 +189,12 @@ def test_geo_json(client: TestClient):
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
|
||||
def test_no_content_type_is_json(client: TestClient):
|
||||
def test_no_content_type_json(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
content='{"name": "Foo", "price": 50.5}',
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"description": None,
|
||||
"price": 50.5,
|
||||
"tax": None,
|
||||
}
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
|
||||
def test_wrong_headers(client: TestClient):
|
||||
|
||||
@@ -17,12 +17,14 @@ def get_client(request: pytest.FixtureRequest):
|
||||
return client
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_get_custom_response(client: TestClient):
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
from docs_src.custom_response.tutorial001b_py310 import app
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
from docs_src.custom_response.tutorial001b_py310 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_get_custom_response():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
name="client",
|
||||
params=[
|
||||
"tutorial001_py310",
|
||||
],
|
||||
)
|
||||
def get_client(request: pytest.FixtureRequest):
|
||||
mod = importlib.import_module(f"docs_src.strict_content_type.{request.param}")
|
||||
client = TestClient(mod.app)
|
||||
return client
|
||||
|
||||
|
||||
def test_lax_post_without_content_type_is_parsed_as_json(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
content='{"name": "Foo", "price": 50.5}',
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"name": "Foo", "price": 50.5}
|
||||
|
||||
|
||||
def test_lax_post_with_json_content_type(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
json={"name": "Foo", "price": 50.5},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"name": "Foo", "price": 50.5}
|
||||
|
||||
|
||||
def test_lax_post_with_text_plain_is_still_rejected(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
content='{"name": "Foo", "price": 50.5}',
|
||||
headers={"Content-Type": "text/plain"},
|
||||
)
|
||||
assert response.status_code == 422, response.text
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.websockets import WebSocketDisconnect
|
||||
|
||||
from docs_src.websockets.tutorial001_py310 import app
|
||||
from docs_src.websockets_.tutorial001_py310 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from ...utils import needs_py310
|
||||
],
|
||||
)
|
||||
def get_app(request: pytest.FixtureRequest):
|
||||
mod = importlib.import_module(f"docs_src.websockets.{request.param}")
|
||||
mod = importlib.import_module(f"docs_src.websockets_.{request.param}")
|
||||
|
||||
return mod.app
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from fastapi.testclient import TestClient
|
||||
],
|
||||
)
|
||||
def get_mod(request: pytest.FixtureRequest):
|
||||
mod = importlib.import_module(f"docs_src.websockets.{request.param}")
|
||||
mod = importlib.import_module(f"docs_src.websockets_.{request.param}")
|
||||
|
||||
return mod
|
||||
|
||||
|
||||
80
uv.lock
generated
80
uv.lock
generated
@@ -1083,12 +1083,10 @@ all = [
|
||||
{ name = "httpx" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pydantic-extra-types" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "ujson" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
standard = [
|
||||
@@ -1134,6 +1132,7 @@ dev = [
|
||||
{ name = "mkdocs-redirects" },
|
||||
{ name = "mkdocstrings", extra = ["python"] },
|
||||
{ name = "mypy" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pillow" },
|
||||
{ name = "playwright" },
|
||||
{ name = "prek" },
|
||||
@@ -1151,6 +1150,7 @@ dev = [
|
||||
{ name = "typer" },
|
||||
{ name = "types-orjson" },
|
||||
{ name = "types-ujson" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "black" },
|
||||
@@ -1165,15 +1165,19 @@ docs = [
|
||||
{ name = "mkdocs-material" },
|
||||
{ name = "mkdocs-redirects" },
|
||||
{ name = "mkdocstrings", extra = ["python"] },
|
||||
{ name = "orjson" },
|
||||
{ name = "pillow" },
|
||||
{ name = "python-slugify" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "ruff" },
|
||||
{ name = "typer" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
docs-tests = [
|
||||
{ name = "httpx" },
|
||||
{ name = "orjson" },
|
||||
{ name = "ruff" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
github-actions = [
|
||||
{ name = "httpx" },
|
||||
@@ -1192,6 +1196,7 @@ tests = [
|
||||
{ name = "httpx" },
|
||||
{ name = "inline-snapshot" },
|
||||
{ name = "mypy" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pwdlib", extra = ["argon2"] },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "pytest" },
|
||||
@@ -1202,6 +1207,7 @@ tests = [
|
||||
{ name = "strawberry-graphql" },
|
||||
{ name = "types-orjson" },
|
||||
{ name = "types-ujson" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
translations = [
|
||||
{ name = "gitpython" },
|
||||
@@ -1225,7 +1231,6 @@ requires-dist = [
|
||||
{ name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" },
|
||||
{ name = "jinja2", marker = "extra == 'standard'", specifier = ">=3.1.5" },
|
||||
{ name = "jinja2", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=3.1.5" },
|
||||
{ name = "orjson", marker = "extra == 'all'", specifier = ">=3.9.3" },
|
||||
{ name = "pydantic", specifier = ">=2.7.0" },
|
||||
{ name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" },
|
||||
{ name = "pydantic-extra-types", marker = "extra == 'standard'", specifier = ">=2.0.0" },
|
||||
@@ -1240,7 +1245,6 @@ requires-dist = [
|
||||
{ name = "starlette", specifier = ">=0.40.0,<1.0.0" },
|
||||
{ name = "typing-extensions", specifier = ">=4.8.0" },
|
||||
{ name = "typing-inspection", specifier = ">=0.4.2" },
|
||||
{ name = "ujson", marker = "extra == 'all'", specifier = ">=5.8.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.12.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.12.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.12.0" },
|
||||
@@ -1269,6 +1273,7 @@ dev = [
|
||||
{ name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
|
||||
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
|
||||
{ name = "mypy", specifier = ">=1.14.1" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "pillow", specifier = ">=11.3.0" },
|
||||
{ name = "playwright", specifier = ">=1.57.0" },
|
||||
{ name = "prek", specifier = ">=0.2.22" },
|
||||
@@ -1286,6 +1291,7 @@ dev = [
|
||||
{ name = "typer", specifier = ">=0.21.1" },
|
||||
{ name = "types-orjson", specifier = ">=3.6.2" },
|
||||
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "black", specifier = ">=25.1.0" },
|
||||
@@ -1300,15 +1306,19 @@ docs = [
|
||||
{ name = "mkdocs-material", specifier = ">=9.7.0" },
|
||||
{ name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
|
||||
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "pillow", specifier = ">=11.3.0" },
|
||||
{ name = "python-slugify", specifier = ">=8.0.4" },
|
||||
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
|
||||
{ name = "ruff", specifier = ">=0.14.14" },
|
||||
{ name = "typer", specifier = ">=0.21.1" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
docs-tests = [
|
||||
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "ruff", specifier = ">=0.14.14" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
github-actions = [
|
||||
{ name = "httpx", specifier = ">=0.27.0,<1.0.0" },
|
||||
@@ -1327,6 +1337,7 @@ tests = [
|
||||
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" },
|
||||
{ name = "inline-snapshot", specifier = ">=0.21.1" },
|
||||
{ name = "mypy", specifier = ">=1.14.1" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" },
|
||||
{ name = "pyjwt", specifier = ">=2.9.0" },
|
||||
{ name = "pytest", specifier = ">=9.0.0" },
|
||||
@@ -1337,6 +1348,7 @@ tests = [
|
||||
{ name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" },
|
||||
{ name = "types-orjson", specifier = ">=3.6.2" },
|
||||
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
translations = [
|
||||
{ name = "gitpython", specifier = ">=3.1.46" },
|
||||
@@ -1595,7 +1607,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.1.2"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "blinker" },
|
||||
@@ -1605,9 +1617,9 @@ dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1911,40 +1923,36 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "griffe"
|
||||
version = "1.15.0"
|
||||
name = "griffelib"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "griffe-typingdoc"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "griffe" },
|
||||
{ name = "griffelib" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/77/d5e5fa0a8391bc2890ae45255847197299739833108dd76ee3c9b2ff0bba/griffe_typingdoc-0.3.0.tar.gz", hash = "sha256:59d9ef98d02caa7aed88d8df1119c9e48c02ed049ea50ce4018ace9331d20f8b", size = 33169, upload-time = "2025-10-23T12:01:39.037Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/26/28182e0c8055842bf3da774dee1d5b789c0f236c078dcbdca1937b5214dc/griffe_typingdoc-0.3.1.tar.gz", hash = "sha256:2ff4703115cb7f8a65b9fdcdd1f3c3a15f813b6554621b52eaad094c4782ce96", size = 31218, upload-time = "2026-02-21T09:38:54.409Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/af/aa32c13f753e2625ec895b1f56eee3c9380a2088a88a2c028955e223856e/griffe_typingdoc-0.3.0-py3-none-any.whl", hash = "sha256:4f6483fff7733a679d1dce142fb029f314125f3caaf0d620eb82e7390c8564bb", size = 9923, upload-time = "2025-10-23T12:01:37.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/c4/cf543fbde49e1ae44830ef0840a4d6ee9f4e4f338138a7766d4e37cf6440/griffe_typingdoc-0.3.1-py3-none-any.whl", hash = "sha256:ecbd457ef6883126b8b6023abf12e08c58e1c152238a2f0e2afdd67a64b07021", size = 10092, upload-time = "2026-02-20T14:53:47.84Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "griffe-warnings-deprecated"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "griffe" },
|
||||
{ name = "griffelib" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/0e/f034e1714eb2c694d6196c75f77a02f9c69d19f9961c4804a016397bf3e5/griffe_warnings_deprecated-1.1.0.tar.gz", hash = "sha256:7bf21de327d59c66c7ce08d0166aa4292ce0577ff113de5878f428d102b6f7c5", size = 33260, upload-time = "2024-12-10T21:02:18.395Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/9e/fc86f1e9270f143a395a601de81aa42a871722c34d4b3c7763658dc2e04d/griffe_warnings_deprecated-1.1.1.tar.gz", hash = "sha256:9261369bf2acb8b5d24a0dc7895cce788208513d4349031d4ea315b979b2e99f", size = 26262, upload-time = "2026-02-21T09:38:55.858Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/4c/b7241f03ad1f22ec2eed33b0f90c4f8c949e3395c4b7488670b07225a20b/griffe_warnings_deprecated-1.1.0-py3-none-any.whl", hash = "sha256:e7b0e8bfd6e5add3945d4d9805b2a41c72409e456733965be276d55f01e8a7a2", size = 5854, upload-time = "2024-12-10T21:02:16.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/3c/c2a9eee79bf2c8002d2fa370534bee93fdca39e8b1fc82e83d552d5d2c07/griffe_warnings_deprecated-1.1.1-py3-none-any.whl", hash = "sha256:4b7d765e82ca9139ed44ffe7bdebed0d3a46ce014ad5a35a2c22e9a16288737a", size = 6565, upload-time = "2026-02-20T15:35:23.577Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2991,17 +2999,17 @@ python = [
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings-python"
|
||||
version = "2.0.1"
|
||||
version = "2.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "griffe" },
|
||||
{ name = "griffelib" },
|
||||
{ name = "mkdocs-autorefs" },
|
||||
{ name = "mkdocstrings" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3925,33 +3933,33 @@ email = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-ai"
|
||||
version = "1.56.0"
|
||||
version = "1.62.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", "temporal", "ui", "vertexai", "xai"] },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/1a/800a1e02b259152a49d4c11d9103784a7482c7e9b067eeea23e949d3d80f/pydantic_ai-1.56.0.tar.gz", hash = "sha256:643ff71612df52315b3b4c4b41543657f603f567223eb33245dc8098f005bdc4", size = 11795, upload-time = "2026-02-06T01:13:21.122Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/97/e3158fa976a29e9580ba1c59601590424bbb81179c359fd29de0dc23aa09/pydantic_ai-1.62.0.tar.gz", hash = "sha256:d6ae517e365ea3ea162ca8ae643f319e105b71b0b6218b83dcad1d1eb2e38c9b", size = 12130, upload-time = "2026-02-19T05:07:07.853Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/35/f4a7fd2b9962ddb9b021f76f293e74fda71da190bb74b57ed5b343c93022/pydantic_ai-1.56.0-py3-none-any.whl", hash = "sha256:b6b3ac74bdc004693834750da4420ea2cde0d3cbc3f134c0b7544f98f1c00859", size = 7222, upload-time = "2026-02-06T01:13:11.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/7a/053aebfab576603e95fcfce1139de4a87e12bd5a2ef1ba00007a931c3ff0/pydantic_ai-1.62.0-py3-none-any.whl", hash = "sha256:1eb88f745ae045e63da41ad68966e8876c964d0f023fbf5d6a3f5d243370bd04", size = 7227, upload-time = "2026-02-19T05:06:58.341Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-ai-slim"
|
||||
version = "1.56.0"
|
||||
version = "1.62.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "genai-prices" },
|
||||
{ name = "griffe" },
|
||||
{ name = "griffelib" },
|
||||
{ name = "httpx" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-graph" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/5c/3a577825b9c1da8f287be7f2ee6fe9aab48bc8a80e65c8518052c589f51c/pydantic_ai_slim-1.56.0.tar.gz", hash = "sha256:9f9f9c56b1c735837880a515ae5661b465b40207b25f3a3434178098b2137f05", size = 415265, upload-time = "2026-02-06T01:13:23.58Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/8d/6350a49f2e4b636efbcfc233221420ab576e4ba4edba38254cb84ae4a1e6/pydantic_ai_slim-1.62.0.tar.gz", hash = "sha256:00d84f659107bbbd88823a3d3dbe7348385935a9870b9d7d4ba799256f6b6983", size = 422452, upload-time = "2026-02-19T05:07:10.292Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/4b/34682036528eeb9aaf093c2073540ddf399ab37b99d282a69ca41356f1aa/pydantic_ai_slim-1.56.0-py3-none-any.whl", hash = "sha256:d657e4113485020500b23b7390b0066e2a0277edc7577eaad2290735ca5dd7d5", size = 542270, upload-time = "2026-02-06T01:13:14.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/67/21e9b3b0944568662e3790c936226bd48a9f27c6b5f27b5916f5857bc4d8/pydantic_ai_slim-1.62.0-py3-none-any.whl", hash = "sha256:5210073fadd46f65859a67da67845093c487f025fa430ed027151f22ec684ab2", size = 549296, upload-time = "2026-02-19T05:07:01.624Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -4169,7 +4177,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-graph"
|
||||
version = "1.56.0"
|
||||
version = "1.62.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
@@ -4177,9 +4185,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/03/f92881cdb12d6f43e60e9bfd602e41c95408f06e2324d3729f7a194e2bcd/pydantic_graph-1.56.0.tar.gz", hash = "sha256:5e22972dbb43dbc379ab9944252ff864019abf3c7d465dcdf572fc8aec9a44a1", size = 58460, upload-time = "2026-02-06T01:13:26.708Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/b6/0b084c847ecd99624f4fbc5c8ecd3f67a2388a282a32612b2a68c3b3595f/pydantic_graph-1.62.0.tar.gz", hash = "sha256:efe56bee3a8ca35b11a3be6a5f7352419fe182ef1e1323a3267ee12dec95f3c7", size = 58529, upload-time = "2026-02-19T05:07:12.947Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/07/8c823eb4d196137c123d4d67434e185901d3cbaea3b0c2b7667da84e72c1/pydantic_graph-1.56.0-py3-none-any.whl", hash = "sha256:ec3f0a1d6fcedd4eb9c59fef45079c2ee4d4185878d70dae26440a9c974c6bb3", size = 72346, upload-time = "2026-02-06T01:13:18.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/12/1a9cbcd59fd070ba72b0fe544caa6ca97758518643523ec2bf1162084e0d/pydantic_graph-1.62.0-py3-none-any.whl", hash = "sha256:abe0e7b356b4d3202b069ec020d8dd1f647f55e9a0e85cd272dab48250bde87d", size = 72350, upload-time = "2026-02-19T05:07:05.305Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user