Compare commits

..

1 Commits

Author SHA1 Message Date
Yurii Motov
c5e31ae863 Fix doctrings for max_digits and decimal_places 2026-02-19 14:15:01 +01:00
57 changed files with 909 additions and 2512 deletions

2
.github/labeler.yml vendored
View File

@@ -29,6 +29,8 @@ internal:
- scripts/**
- .gitignore
- .pre-commit-config.yaml
- pdm_build.py
- requirements*.txt
- uv.lock
- docs/en/data/sponsors.yml
- docs/en/overrides/main.html

View File

@@ -8,6 +8,11 @@ on:
jobs:
publish:
runs-on: ubuntu-latest
strategy:
matrix:
package:
- fastapi
- fastapi-slim
permissions:
id-token: write
contents: read
@@ -21,9 +26,14 @@ jobs:
uses: actions/setup-python@v6
with:
python-version-file: ".python-version"
# Issue ref: https://github.com/actions/setup-python/issues/436
# cache: "pip"
# cache-dependency-path: pyproject.toml
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Build distribution
run: uv build
env:
TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }}
- name: Publish
run: uv publish

View File

@@ -45,7 +45,7 @@ jobs:
test:
needs:
- changes
if: needs.changes.outputs.src == 'true' || github.ref == 'refs/heads/master'
if: needs.changes.outputs.src == 'true'
strategy:
matrix:
os: [ windows-latest, macos-latest ]
@@ -68,8 +68,10 @@ 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
@@ -107,10 +109,20 @@ 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'
@@ -120,39 +132,6 @@ 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
@@ -197,7 +176,6 @@ jobs:
if: always()
needs:
- coverage-combine
- benchmark
runs-on: ubuntu-latest
steps:
- name: Dump GitHub context
@@ -208,4 +186,4 @@ jobs:
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
allowed-skips: coverage-combine,test,benchmark
allowed-skips: coverage-combine,test

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
# Custom Response - HTML, Stream, File, others { #custom-response-html-stream-file-others }
By default, **FastAPI** will return JSON responses.
By default, **FastAPI** will return the responses using `JSONResponse`.
You can override it by returning a `Response` directly as seen in [Return a Response directly](response-directly.md){.internal-link target=_blank}.
@@ -10,27 +10,43 @@ But you can also declare the `Response` that you want to be used (e.g. any `Resp
The contents that you return from your *path operation function* will be put inside of that `Response`.
And if that `Response` has a JSON media type (`application/json`), like is the case with the `JSONResponse` and `UJSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*.
/// note
If you use a response class with no media type, FastAPI will expect your response to have no content, so it will not document the response format in its generated OpenAPI docs.
///
## JSON Responses { #json-responses }
## Use `ORJSONResponse` { #use-orjsonresponse }
By default FastAPI returns JSON responses.
For example, if you are squeezing performance, you can install and use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> and set the response to be `ORJSONResponse`.
If you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic.
Import the `Response` class (sub-class) you want to use and declare it in the *path operation decorator*.
If you don't declare a response model, FastAPI will use the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} and put it in a `JSONResponse`.
For large responses, returning a `Response` directly is much faster than returning a dictionary.
If you declare a `response_class` with a JSON media type (`application/json`), like is the case with the `JSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*. But the data won't be serialized to JSON bytes with Pydantic, instead it will be converted with the `jsonable_encoder` and then passed to the `JSONResponse` class, which will serialize it to bytes using the standard JSON library in Python.
This is because by default, FastAPI will inspect every item inside and make sure it is serializable as JSON, using the same [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} explained in the tutorial. This is what allows you to return **arbitrary objects**, for example database models.
### JSON Performance { #json-performance }
But if you are certain that the content that you are returning is **serializable with JSON**, you can pass it directly to the response class and avoid the extra overhead that FastAPI would have by passing your return content through the `jsonable_encoder` before passing it to the response class.
In short, if you want the maximum performance, use a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} and don't declare a `response_class` in the *path operation decorator*.
{* ../../docs_src/custom_response/tutorial001b_py310.py hl[2,7] *}
{* ../../docs_src/response_model/tutorial001_01_py310.py ln[15:17] hl[16] *}
/// info
The parameter `response_class` will also be used to define the "media type" of the response.
In this case, the HTTP header `Content-Type` will be set to `application/json`.
And it will be documented as such in OpenAPI.
///
/// tip
The `ORJSONResponse` is only available in FastAPI, not in Starlette.
///
## HTML Response { #html-response }
@@ -138,6 +154,40 @@ Takes some data and returns an `application/json` encoded response.
This is the default response used in **FastAPI**, as you read above.
### `ORJSONResponse` { #orjsonresponse }
A fast alternative JSON response using <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>, as you read above.
/// info
This requires installing `orjson` for example with `pip install orjson`.
///
### `UJSONResponse` { #ujsonresponse }
An alternative JSON response using <a href="https://github.com/ultrajson/ultrajson" class="external-link" target="_blank">`ujson`</a>.
/// info
This requires installing `ujson` for example with `pip install ujson`.
///
/// warning
`ujson` is less careful than Python's built-in implementation in how it handles some edge-cases.
///
{* ../../docs_src/custom_response/tutorial001_py310.py hl[2,7] *}
/// tip
It's possible that `ORJSONResponse` might be a faster alternative.
///
### `RedirectResponse` { #redirectresponse }
Returns an HTTP redirect. Uses a 307 status code (Temporary Redirect) by default.
@@ -218,7 +268,7 @@ In this case, you can return the file path directly from your *path operation* f
You can create your own custom response class, inheriting from `Response` and using it.
For example, let's say that you want to use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> with some settings.
For example, let's say that you want to use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>, but with some custom settings not used in the included `ORJSONResponse` class.
Let's say you want it to return indented and formatted JSON, so you want to use the orjson option `orjson.OPT_INDENT_2`.
@@ -242,21 +292,13 @@ Now instead of returning:
Of course, you will probably find much better ways to take advantage of this than formatting JSON. 😉
### `orjson` or Response Model { #orjson-or-response-model }
If what you are looking for is performance, you are probably better off using a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} than an `orjson` response.
With a response model, FastAPI will use Pydantic to serialize the data to JSON, without using intermediate steps, like converting it with `jsonable_encoder`, which would happen in any other case.
And under the hood, Pydantic uses the same underlying Rust mechanisms as `orjson` to serialize to JSON, so you will already get the best performance with a response model.
## Default response class { #default-response-class }
When creating a **FastAPI** class instance or an `APIRouter` you can specify which response class to use by default.
The parameter that defines this is `default_response_class`.
In the example below, **FastAPI** will use `HTMLResponse` by default, in all *path operations*, instead of JSON.
In the example below, **FastAPI** will use `ORJSONResponse` by default, in all *path operations*, instead of `JSONResponse`.
{* ../../docs_src/custom_response/tutorial010_py310.py hl[2,4] *}

View File

@@ -1,63 +0,0 @@
# JSON with Bytes as Base64 { #json-with-bytes-as-base64 }
If your app needs to receive and send JSON data, but you need to include binary data in it, you can encode it as base64.
## Base64 vs Files { #base64-vs-files }
Consider first if you can use [Request Files](../tutorial/request-files.md){.internal-link target=_blank} for uploading binary data and [Custom Response - FileResponse](./custom-response.md#fileresponse--fileresponse-){.internal-link target=_blank} for sending binary data, instead of encoding it in JSON.
JSON can only contain UTF-8 encoded strings, so it can't contain raw bytes.
Base64 can encode binary data in strings, but to do it, it needs to use more characters than the original binary data, so it would normally be less efficient than regular files.
Use base64 only if you definitely need to include binary data in JSON, and you can't use files for that.
## Pydantic `bytes` { #pydantic-bytes }
You can declare a Pydantic model with `bytes` fields, and then use `val_json_bytes` in the model config to tell it to use base64 to *validate* input JSON data, as part of that validation it will decode the base64 string into bytes.
{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:9,29:35] hl[9] *}
If you check the `/docs`, they will show that the field `data` expects base64 encoded bytes:
<div class="screenshot">
<img src="/img/tutorial/json-base64-bytes/image01.png">
</div>
You could send a request like:
```json
{
"description": "Some data",
"data": "aGVsbG8="
}
```
/// tip
`aGVsbG8=` is the base64 encoding of `hello`.
///
And then Pydantic will decode the base64 string and give you the original bytes in the `data` field of the model.
You will receive a response like:
```json
{
"description": "Some data",
"content": "hello"
}
```
## Pydantic `bytes` for Output Data { #pydantic-bytes-for-output-data }
You can also use `bytes` fields with `ser_json_bytes` in the model config for output data, and Pydantic will *serialize* the bytes as base64 when generating the JSON response.
{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:2,12:16,29,38:41] hl[16] *}
## Pydantic `bytes` for Input and Output Data { #pydantic-bytes-for-input-and-output-data }
And of course, you can use the same model configured to use base64 to handle both input (*validate*) with `val_json_bytes` and output (*serialize*) with `ser_json_bytes` when receiving and sending JSON data.
{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:2,19:26,29,44:46] hl[23:26] *}

View File

@@ -2,23 +2,19 @@
When you create a **FastAPI** *path operation* you can normally return any data from it: a `dict`, a `list`, a Pydantic model, a database model, etc.
If you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic.
By default, **FastAPI** would automatically convert that return value to JSON using the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}.
If you don't declare a response model, FastAPI will use the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} and put it in a `JSONResponse`.
Then, behind the scenes, it would put that JSON-compatible data (e.g. a `dict`) inside of a `JSONResponse` that would be used to send the response to the client.
You could also create a `JSONResponse` directly and return it.
But you can return a `JSONResponse` directly from your *path operations*.
/// tip
You will normally have much better performance using a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} than returning a `JSONResponse` directly, as that way it serializes the data using Pydantic, in Rust.
///
It might be useful, for example, to return custom headers or cookies.
## Return a `Response` { #return-a-response }
You can return any `Response` or any sub-class of it.
In fact, you can return any `Response` or any sub-class of it.
/// info
/// tip
`JSONResponse` itself is a sub-class of `Response`.
@@ -60,18 +56,6 @@ You could put your XML content in a string, put that in a `Response`, and return
{* ../../docs_src/response_directly/tutorial002_py310.py hl[1,18] *}
## How a Response Model Works { #how-a-response-model-works }
When you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} in a path operation, **FastAPI** will use it to serialize the data to JSON, using Pydantic.
{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *}
As that will happen on the Rust side, the performance will be much better than if it was done with regular Python and the `JSONResponse` class.
When using a response model FastAPI won't use the `jsonable_encoder` to convert the data (which would be slower) nor the `JSONResponse` class.
Instead it takes the JSON bytes generated with Pydantic using the response model and returns a `Response` with the right media type for JSON directly (`application/json`).
## Notes { #notes }
When you return a `Response` directly its data is not validated, converted (serialized), or documented automatically.

View File

@@ -1,88 +0,0 @@
# 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.
///

View File

@@ -6,10 +6,6 @@ Here are several pointers to other places in the docs, for general or frequent q
To ensure that you don't return more data than you should, read the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}.
## Optimize Response Performance - Response Model - Return Type { #optimize-response-performance-response-model-return-type }
To optimize performance when returning JSON data, use a return type or response model, that way Pydantic will handle the serialization to JSON on the Rust side, without going through Python. Read more in the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}.
## Documentation Tags - OpenAPI { #documentation-tags-openapi }
To add tags to your *path operations*, and group them in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -22,13 +22,7 @@ from fastapi.responses import (
## FastAPI Responses
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).
There are a couple of custom FastAPI response classes, you can use them to optimize JSON performance.
::: fastapi.responses.UJSONResponse
options:

View File

@@ -7,57 +7,6 @@ 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
* ✨ Serialize JSON response with Pydantic (in Rust), when there's a Pydantic return type or response model. PR [#14962](https://github.com/fastapi/fastapi/pull/14962) by [@tiangolo](https://github.com/tiangolo).
* This results in 2x (or more) performance increase for JSON responses.
* New docs: [Custom Response - JSON Performance](https://fastapi.tiangolo.com/advanced/custom-response/#json-performance).
## 0.129.2
### Internal
* ⬆️ Upgrade pytest. PR [#14959](https://github.com/fastapi/fastapi/pull/14959) by [@tiangolo](https://github.com/tiangolo).
* 👷 Fix CI, do not attempt to publish `fastapi-slim`. PR [#14958](https://github.com/fastapi/fastapi/pull/14958) by [@tiangolo](https://github.com/tiangolo).
* Drop support for `fastapi-slim`, no more versions will be released, use only `"fastapi[standard]"` or `fastapi`. PR [#14957](https://github.com/fastapi/fastapi/pull/14957) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Update pyproject.toml, remove unneeded lines. PR [#14956](https://github.com/fastapi/fastapi/pull/14956) by [@tiangolo](https://github.com/tiangolo).
## 0.129.1
### Fixes
* ♻️ Fix JSON Schema for bytes, use `"contentMediaType": "application/octet-stream"` instead of `"format": "binary"`. PR [#14953](https://github.com/fastapi/fastapi/pull/14953) by [@tiangolo](https://github.com/tiangolo).
### Docs
* 🔨 Add Kapa.ai widget (AI chatbot). PR [#14938](https://github.com/fastapi/fastapi/pull/14938) by [@tiangolo](https://github.com/tiangolo).
@@ -82,7 +31,6 @@ hide:
### Internal
* 👷 Always run tests on push to `master` branch and when run by scheduler. PR [#14940](https://github.com/fastapi/fastapi/pull/14940) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🎨 Upgrade typing syntax for Python 3.10. PR [#14932](https://github.com/fastapi/fastapi/pull/14932) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump cryptography from 46.0.4 to 46.0.5. PR [#14892](https://github.com/fastapi/fastapi/pull/14892) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pillow from 12.1.0 to 12.1.1. PR [#14899](https://github.com/fastapi/fastapi/pull/14899) by [@dependabot[bot]](https://github.com/apps/dependabot).

View File

@@ -13,7 +13,6 @@ FastAPI will use this return type to:
* Add a **JSON Schema** for the response, in the OpenAPI *path operation*.
* This will be used by the **automatic docs**.
* It will also be used by automatic client code generation tools.
* **Serialize** the returned data to JSON using Pydantic, which is written in **Rust**, so it will be **much faster**.
But most importantly:

View File

@@ -192,8 +192,6 @@ nav:
- advanced/wsgi.md
- 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

View File

@@ -1,9 +1,9 @@
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi.responses import ORJSONResponse
app = FastAPI(default_response_class=HTMLResponse)
app = FastAPI(default_response_class=ORJSONResponse)
@app.get("/items/")
async def read_items():
return "<h1>Items</h1><p>This is a list of items.</p>"
return [{"item_id": "Foo"}]

View File

View File

@@ -1,46 +0,0 @@
from fastapi import FastAPI
from pydantic import BaseModel
class DataInput(BaseModel):
description: str
data: bytes
model_config = {"val_json_bytes": "base64"}
class DataOutput(BaseModel):
description: str
data: bytes
model_config = {"ser_json_bytes": "base64"}
class DataInputOutput(BaseModel):
description: str
data: bytes
model_config = {
"val_json_bytes": "base64",
"ser_json_bytes": "base64",
}
app = FastAPI()
@app.post("/data")
def post_data(body: DataInput):
content = body.data.decode("utf-8")
return {"description": body.description, "content": content}
@app.get("/data")
def get_data() -> DataOutput:
data = "hello".encode("utf-8")
return DataOutput(description="A plumbus", data=data)
@app.post("/data-in-out")
def post_data_in_out(body: DataInputOutput) -> DataInputOutput:
return body

View File

@@ -0,0 +1,2 @@
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
return item_a, item_b, item_c, item_d, item_e

View File

View File

@@ -1,14 +0,0 @@
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

View File

@@ -1,640 +0,0 @@
---
name: fastapi
description: FastAPI best practices and conventions. Use when working with FastAPI APIs and Pydantic models for them. Keeps FastAPI code clean and up to date with the latest features and patterns, updated with new versions. Write new code or refactor and update old code.
---
# FastAPI
Official FastAPI skill to write code with best practices, keeping up to date with new versions and features.
## Use the `fastapi` CLI
To run the development server, in localhost, with reload:
```bash
fastapi dev
```
And to run the production server:
```bash
fastapi run
```
### Add an entrypoint in `pyproject.toml`
FastAPI CLI will read the entrypoint in `pyproject.toml` to know where the FastAPI app is declared.
```toml
[tool.fastapi]
entrypoint = "my_app.main:app"
```
This way the same command can be used without having to pass a path every time.
### Use `fastapi` with a path
When adding the entrypoint to `pyproject.toml` is not possible, or the user explicitly asks not to, or it's running an independent small app, you can pass the path to the file to the `fastapi` command:
```bash
fastapi dev my_app/main.py
```
## Use `Annotated`
Always prefer the `Annotated` style for parameter and dependency declarations.
It keeps the function signatures working in other contexts, respects the types, allows reusability.
### In Parameter Declarations
Use `Annotated` for parameter declarations, including `Path`, `Query`, `Header`, etc.:
```python
from typing import Annotated
from fastapi import FastAPI, Path, Query
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(
item_id: Annotated[int, Path(ge=1, description="The item ID")],
q: Annotated[str | None, Query(max_length=50)] = None,
):
return {"message": "Hello World"}
```
instead of:
```python
# DO NOT DO THIS
from fastapi import FastAPI, Path, Query
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(
item_id: int = Path(ge=1, description="The item ID"),
q: str | None = Query(default=None, max_length=50),
):
return {"message": "Hello World"}
```
### For Dependencies
Use `Annotated` for dependencies with `Depends()`.
Unless asked not to, create a new type alias for the dependency to allow re-using it.
```python
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def get_current_user():
return {"username": "johndoe"}
CurrentUserDep = Annotated[dict, Depends(get_current_user)]
@app.get("/items/")
async def read_item(current_user: CurrentUserDep):
return {"message": "Hello World"}
```
instead of:
```python
# DO NOT DO THIS
from fastapi import Depends, FastAPI
app = FastAPI()
def get_current_user():
return {"username": "johndoe"}
@app.get("/items/")
async def read_item(current_user: dict = Depends(get_current_user)):
return {"message": "Hello World"}
```
## Do not use Ellipsis for *path operations*
Do not use `...` as a default value for required parameters.
Do this, without Ellipsis (`...`):
```python
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_item(project_id: Annotated[int, Query()]):
return {"message": "Hello World"}
```
instead of this:
```python
# DO NOT DO THIS
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_item(project_id: int = Query(...)):
return {"message": "Hello World"}
```
## Do not use Ellipsis for Pydantic models
Do this, without Ellipsis (`...`):
```python
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float = Field(gt=0)
@app.post("/items/")
async def create_item(item: Item):
return {"message": "Hello World"}
```
instead of this:
```python
# DO NOT DO THIS
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
name: str = ...
description: str | None = None
price: float = Field(..., gt=0)
@app.post("/items/")
async def create_item(item: Item):
return {"message": "Hello World"}
```
## Return Type or Response Model
When possible, include a return type. It will be used to validate, filter, document, and serialize the response.
```python
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
@app.get("/items/me")
async def get_item() -> Item:
return Item(name="Plumbus", description="All-purpose home device")
```
**Important**: Return types or response models are what filter data ensuring no sensitive information is exposed. And they are used to serialize data with Pydantic, this is the main idea that can increase response performance.
The return type doesn't have to be a Pydantic model, it could be a different type, like a list of Pydantic models, or a dict, etc.
### When to use `response_model` instead
If the return type is not the same as the type that you want to use to validate, filter, serialize, use the `response_model` parameter on the decorator instead.
```python
from typing import Any
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
@app.get("/items/me", response_model=Item)
async def get_item() -> Any:
return {"name": "Foo", "description": "A very nice Item"}
```
This can be in particular useful when filtering data to expose only the public fields, and avoid exposing sensitive information.
```python
from typing import Any
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class InternalItem(BaseModel):
name: str
description: str | None = None
secret_key: str
class Item(BaseModel):
name: str
description: str | None = None
@app.get("/items/me", response_model=Item)
async def get_item() -> Any:
item = InternalItem(
name="Foo", description="A very nice Item", secret_key="supersecret"
)
return item
```
## Performance
Do not use `ORJSONResponse` or `UJSONResponse`, they are deprecated.
Instead declare a return type or response model. Pydantic will handle the data serialization in the Rust side.
### Including Routers
When declaring routers, prefer to add router level parameters like prefix, tags, etc. to the router itself, instead of in `include_router()`.
Do this:
```python
from fastapi import APIRouter, FastAPI
app = FastAPI()
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/")
async def list_items():
return []
# In main.py
app.include_router(router)
```
instead of this:
```python
# DO NOT DO THIS
from fastapi import APIRouter, FastAPI
app = FastAPI()
router = APIRouter()
@router.get("/")
async def list_items():
return []
# In main.py
app.include_router(router, prefix="/items", tags=["items"])
```
There could be exceptions but try to follow this convention.
Apply shared dependencies at the router level via `dependencies=[Depends(...)]`.
## Dependency Injection
Use dependencies when:
* They can't be declared in Pydantic validation and require additional logic
* The logic depends on external resources or could block in any other way
* Other dependencies need their results (it's a sub-dependency)
* The logic can be shared by multiple endpoints to do things like error early, authentication, etc.
* They need to handle cleanup (e.g., DB sessions, file handles), using dependencies with `yield`
* Their logic needs request input data, like headers, query parameters, etc.
### Dependencies with `yield` and `scope`
When the using dependencies with `yield`, they can have a `scope` that defines when the exit code is run.
Use the default scope `"request"` to run the exit code after the response is sent back.
```python
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
DBDep = Annotated[DBSession, Depends(get_db)]
@app.get("/items/")
async def read_items(db: DBDep):
return db.query(Item).all()
```
Use the scope `"function"` when they should run the exit code after the response data is generated but before the response is sent back to the client.
```python
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def get_username():
try:
yield "Rick"
finally:
print("Cleanup up before response is sent")
UserNameDep = Annotated[str, Depends(get_username, scope="function")]
@app.get("/users/me")
def get_user_me(username: UserNameDep):
return username
```
### Class Dependencies
Avoid creating class dependencies when possible.
If a class is needed, instead create a regular function dependency that return a class instance.
Do this:
```python
from dataclasses import dataclass
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
@dataclass
class CommonQueryParams:
q: str | None = None
skip: int = 0
limit: int = 100
def get_common_params(
q: str | None = None, skip: int = 0, limit: int = 100
) -> CommonQueryParams:
return CommonQueryParams(q=q, skip=skip, limit=limit)
CommonsDep = Annotated[CommonQueryParams, Depends(get_common_params)]
@app.get("/items/")
async def read_items(commons: CommonsDep):
return {"q": commons.q, "skip": commons.skip, "limit": commons.limit}
```
instead of this:
```python
# DO NOT DO THIS
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
class CommonQueryParams:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
self.q = q
self.skip = skip
self.limit = limit
@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends()]):
return {"q": commons.q, "skip": commons.skip, "limit": commons.limit}
```
## Async vs Sync *path operations*
Use `async` *path operations* only when fully certain that the logic called inside is compatible with async and await (it's called with `await`) or doesn't block.
```python
from fastapi import FastAPI
app = FastAPI()
# Use async def when calling async code
@app.get("/async-items/")
async def read_async_items():
data = await some_async_library.fetch_items()
return data
# Use plain def when calling blocking/sync code or when in doubt
@app.get("/items/")
def read_items():
data = some_blocking_library.fetch_items()
return data
```
In case of doubt, or by default, use regular `def` functions, those will be run in a threadpool so they don't block the event loop.
The same rules apply to dependencies.
Make sure blocking code is not run inside of `async` functions. The logic will work, but will damage heavily the performance.
### Asyncer
When needing to run blocking code inside of async functions, or async code inside of blocking functions, suggest using Asyncer.
Install:
```bash
uv add asyncer
```
Run blocking sync code inside of async with `asyncify()`:
```python
from asyncer import asyncify
from fastapi import FastAPI
app = FastAPI()
def do_blocking_work(name: str) -> str:
# Some blocking I/O operation
return f"Hello {name}"
@app.get("/items/")
async def read_items():
result = await asyncify(do_blocking_work)(name="World")
return {"message": result}
```
And run async code inside of blocking sync code with `syncify()`:
```python
from asyncer import syncify
from fastapi import FastAPI
app = FastAPI()
async def do_async_work(name: str) -> str:
return f"Hello {name}"
@app.get("/items/")
def read_items():
result = syncify(do_async_work)(name="World")
return {"message": result}
```
## Use uv
If uv is available, use it to manage dependencies.
## SQLModel for SQL databases
When working with SQL databases, prefer using SQLModel as it is integrated with Pydantic and will allow declaring data validation with the same models.
## Do not use Pydantic RootModels
Do not use Pydantic `RootModel`, instead use regular type annotations with `Annotated` and Pydantic validation utilities.
For example for a list with validations you could do:
```python
from typing import Annotated
from fastapi import Body, FastAPI
from pydantic import Field
app = FastAPI()
@app.post("/items/")
async def create_items(items: Annotated[list[int], Field(min_length=1), Body()]):
return items
```
instead of:
```python
# DO NOT DO THIS
from typing import Annotated
from fastapi import FastAPI
from pydantic import Field, RootModel
app = FastAPI()
class ItemList(RootModel[Annotated[list[int], Field(min_length=1)]]):
pass
@app.post("/items/")
async def create_items(items: ItemList):
return items
```
FastAPI supports these type annotations and will create a Pydantic `TypeAdapter` for them, so that types can work as normally.
## Use one HTTP operation per function
Don't mix HTTP operations in a single function, having one function per HTTP operation helps separate concerns and organize the code.
Do this:
```python
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
@app.get("/items/")
async def list_items():
return []
@app.post("/items/")
async def create_item(item: Item):
return item
```
instead of this:
```python
# DO NOT DO THIS
from fastapi import FastAPI, Request
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
@app.api_route("/items/", methods=["GET", "POST"])
async def handle_items(request: Request):
if request.method == "GET":
return []
# ...
```

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.132.0"
__version__ = "0.129.0"
from starlette import status as status

View File

@@ -27,7 +27,7 @@ from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-
)
from pydantic._internal._typing_extra import eval_type_lenient
from pydantic.fields import FieldInfo as FieldInfo
from pydantic.json_schema import GenerateJsonSchema as _GenerateJsonSchema
from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
from pydantic_core import CoreSchema as CoreSchema
from pydantic_core import PydanticUndefined
@@ -40,23 +40,6 @@ RequiredParam = PydanticUndefined
Undefined = PydanticUndefined
evaluate_forwardref = eval_type_lenient
class GenerateJsonSchema(_GenerateJsonSchema):
# TODO: remove when this is merged (or equivalent): https://github.com/pydantic/pydantic/pull/12841
# and dropping support for any version of Pydantic before that one (so, in a very long time)
def bytes_schema(self, schema: CoreSchema) -> JsonSchemaValue:
json_schema = {"type": "string", "contentMediaType": "application/octet-stream"}
bytes_mode = (
self._config.ser_json_bytes
if self.mode == "serialization"
else self._config.val_json_bytes
)
if bytes_mode == "base64":
json_schema["contentEncoding"] = "base64"
self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes)
return json_schema
# TODO: remove when dropping support for Pydantic < v2.12.3
_Attrs = {
"default": ...,
@@ -199,32 +182,6 @@ class ModelField:
exclude_none=exclude_none,
)
def serialize_json(
self,
value: Any,
*,
include: IncEx | None = None,
exclude: IncEx | None = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> bytes:
# What calls this code passes a value that already called
# self._type_adapter.validate_python(value)
# This uses Pydantic's dump_json() which serializes directly to JSON
# bytes in one pass (via Rust), avoiding the intermediate Python dict
# step of dump_python(mode="json") + json.dumps().
return self._type_adapter.dump_json(
value,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
def __hash__(self) -> int:
# Each ModelField is unique for our purposes, to allow making a dict from
# ModelField to its JSON Schema.

View File

@@ -840,29 +840,6 @@ 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(
@@ -997,7 +974,6 @@ 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]]

View File

@@ -139,7 +139,7 @@ class UploadFile(StarletteUploadFile):
def __get_pydantic_json_schema__(
cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler
) -> dict[str, Any]:
return {"type": "string", "contentMediaType": "application/octet-stream"}
return {"type": "string", "format": "binary"}
@classmethod
def __get_pydantic_core_schema__(

View File

@@ -211,7 +211,7 @@ def Path( # noqa: N802
int | None,
Doc(
"""
Maximum number of allow digits for strings.
Maximum number of digits allowed for decimal values.
"""
),
] = _Unset,
@@ -219,7 +219,7 @@ def Path( # noqa: N802
int | None,
Doc(
"""
Maximum number of decimal places allowed for numbers.
Maximum number of decimal places allowed for decimal values.
"""
),
] = _Unset,
@@ -570,7 +570,7 @@ def Query( # noqa: N802
int | None,
Doc(
"""
Maximum number of allow digits for strings.
Maximum number of digits allowed for decimal values.
"""
),
] = _Unset,
@@ -578,7 +578,7 @@ def Query( # noqa: N802
int | None,
Doc(
"""
Maximum number of decimal places allowed for numbers.
Maximum number of decimal places allowed for decimal values.
"""
),
] = _Unset,
@@ -892,7 +892,7 @@ def Header( # noqa: N802
int | None,
Doc(
"""
Maximum number of allow digits for strings.
Maximum number of digits allowed for decimal values.
"""
),
] = _Unset,
@@ -900,7 +900,7 @@ def Header( # noqa: N802
int | None,
Doc(
"""
Maximum number of decimal places allowed for numbers.
Maximum number of decimal places allowed for decimal values.
"""
),
] = _Unset,
@@ -1198,7 +1198,7 @@ def Cookie( # noqa: N802
int | None,
Doc(
"""
Maximum number of allow digits for strings.
Maximum number of digits allowed for decimal values.
"""
),
] = _Unset,
@@ -1206,7 +1206,7 @@ def Cookie( # noqa: N802
int | None,
Doc(
"""
Maximum number of decimal places allowed for numbers.
Maximum number of decimal places allowed for decimal values.
"""
),
] = _Unset,
@@ -1526,7 +1526,7 @@ def Body( # noqa: N802
int | None,
Doc(
"""
Maximum number of allow digits for strings.
Maximum number of digits allowed for decimal values.
"""
),
] = _Unset,
@@ -1534,7 +1534,7 @@ def Body( # noqa: N802
int | None,
Doc(
"""
Maximum number of decimal places allowed for numbers.
Maximum number of decimal places allowed for decimal values.
"""
),
] = _Unset,
@@ -1842,7 +1842,7 @@ def Form( # noqa: N802
int | None,
Doc(
"""
Maximum number of allow digits for strings.
Maximum number of digits allowed for decimal values.
"""
),
] = _Unset,
@@ -1850,7 +1850,7 @@ def Form( # noqa: N802
int | None,
Doc(
"""
Maximum number of decimal places allowed for numbers.
Maximum number of decimal places allowed for decimal values.
"""
),
] = _Unset,
@@ -2157,7 +2157,7 @@ def File( # noqa: N802
int | None,
Doc(
"""
Maximum number of allow digits for strings.
Maximum number of digits allowed for decimal values.
"""
),
] = _Unset,
@@ -2165,7 +2165,7 @@ def File( # noqa: N802
int | None,
Doc(
"""
Maximum number of decimal places allowed for numbers.
Maximum number of decimal places allowed for decimal values.
"""
),
] = _Unset,

View File

@@ -1,6 +1,5 @@
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
@@ -8,7 +7,6 @@ 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
@@ -22,29 +20,12 @@ 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 ujson library to serialize data to JSON.
"""
JSON response using the high-performance ujson library to serialize data to JSON.
**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`.
Read more about it in the
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
"""
def render(self, content: Any) -> bytes:
@@ -52,29 +33,12 @@ 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 orjson library to serialize data to JSON.
"""
JSON response using the high-performance orjson library to serialize data to JSON.
**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`.
Read more about it in the
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
"""
def render(self, content: Any) -> bytes:

View File

@@ -271,7 +271,6 @@ async def serialize_response(
exclude_none: bool = False,
is_coroutine: bool = True,
endpoint_ctx: EndpointContext | None = None,
dump_json: bool = False,
) -> Any:
if field:
if is_coroutine:
@@ -287,8 +286,8 @@ async def serialize_response(
body=response_content,
endpoint_ctx=ctx,
)
serializer = field.serialize_json if dump_json else field.serialize
return serializer(
return field.serialize(
value,
include=include,
exclude=exclude,
@@ -329,7 +328,6 @@ 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
@@ -338,10 +336,6 @@ 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
@@ -375,8 +369,7 @@ def get_request_handler(
json_body: Any = Undefined
content_type_value = request.headers.get("content-type")
if not content_type_value:
if not actual_strict_content_type:
json_body = await request.json()
json_body = await request.json()
else:
message = email.message.Message()
message["content-type"] = content_type_value
@@ -450,14 +443,6 @@ def get_request_handler(
response_args["status_code"] = current_status_code
if solved_result.response.status_code:
response_args["status_code"] = solved_result.response.status_code
# Use the fast path (dump_json) when no custom response
# class was set and a response field with a TypeAdapter
# exists. Serializes directly to JSON bytes via Pydantic's
# Rust core, skipping the intermediate Python dict +
# json.dumps() step.
use_dump_json = response_field is not None and isinstance(
response_class, DefaultPlaceholder
)
content = await serialize_response(
field=response_field,
response_content=raw_response,
@@ -469,16 +454,8 @@ def get_request_handler(
exclude_none=response_model_exclude_none,
is_coroutine=is_coroutine,
endpoint_ctx=endpoint_ctx,
dump_json=use_dump_json,
)
if use_dump_json:
response = Response(
content=content,
media_type="application/json",
**response_args,
)
else:
response = actual_response_class(content, **response_args)
response = actual_response_class(content, **response_args)
if not is_body_allowed_for_status_code(response.status_code):
response.body = b""
response.headers.raw.extend(solved_result.response.headers.raw)
@@ -605,7 +582,6 @@ 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
@@ -632,7 +608,6 @@ 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
@@ -721,7 +696,6 @@ 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]:
@@ -972,29 +946,6 @@ 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:
@@ -1041,7 +992,6 @@ 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,
@@ -1092,7 +1042,6 @@ 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 {}
@@ -1139,9 +1088,6 @@ 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)
@@ -1517,11 +1463,6 @@ 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 [])

40
pdm_build.py Normal file
View File

@@ -0,0 +1,40 @@
import os
from typing import Any
from pdm.backend.hooks import Context
TIANGOLO_BUILD_PACKAGE = os.getenv("TIANGOLO_BUILD_PACKAGE")
def pdm_build_initialize(context: Context) -> None:
metadata = context.config.metadata
# Get main version
version = metadata["version"]
# Get custom config for the current package, from the env var
all_configs_config: dict[str, Any] = context.config.data["tool"]["tiangolo"][
"_internal-slim-build"
]["packages"]
if TIANGOLO_BUILD_PACKAGE not in all_configs_config:
return
config = all_configs_config[TIANGOLO_BUILD_PACKAGE]
project_config: dict[str, Any] = config["project"]
# Override main [project] configs with custom configs for this package
for key, value in project_config.items():
metadata[key] = value
# Get custom build config for the current package
build_config: dict[str, Any] = (
config.get("tool", {}).get("pdm", {}).get("build", {})
)
# Override PDM build config with custom build config for this package
for key, value in build_config.items():
context.config.build_config[key] = value
# Get main dependencies
dependencies: list[str] = metadata.get("dependencies", [])
# Sync versions in dependencies
new_dependencies = []
for dep in dependencies:
new_dep = f"{dep}>={version}"
new_dependencies.append(new_dep)
metadata["dependencies"] = new_dependencies

View File

@@ -57,6 +57,7 @@ Issues = "https://github.com/fastapi/fastapi/issues"
Changelog = "https://fastapi.tiangolo.com/release-notes/"
[project.optional-dependencies]
standard = [
"fastapi-cli[standard] >=0.0.8",
# For the test client
@@ -105,6 +106,10 @@ 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
@@ -147,10 +152,6 @@ 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",
@@ -170,7 +171,7 @@ tests = [
"mypy >=1.14.1",
"pwdlib[argon2] >=0.2.1",
"pyjwt >=2.9.0",
"pytest >=9.0.0",
"pytest >=7.1.3,<9.0.0",
"pytest-codspeed >=4.2.0",
"pyyaml >=5.3.1,<7.0.0",
"sqlmodel >=0.0.31",
@@ -198,6 +199,32 @@ source-includes = [
"docs/en/docs/img/favicon.png",
]
[tool.tiangolo._internal-slim-build.packages.fastapi-slim.project]
name = "fastapi-slim"
readme = "fastapi-slim/README.md"
dependencies = [
"fastapi",
]
optional-dependencies = {}
scripts = {}
[tool.tiangolo._internal-slim-build.packages.fastapi-slim.tool.pdm.build]
# excludes needs to explicitly exclude the top level python packages,
# otherwise PDM includes them by default
# A "*" glob pattern can't be used here because in PDM internals, the patterns are put
# in a set (unordered, order varies) and each excluded file is assigned one of the
# glob patterns that matches, as the set is unordered, the matched pattern could be "*"
# independent of the order here. And then the internal code would give it a lower score
# than the one for a default included file.
# By not using "*" and explicitly excluding the top level packages, they get a higher
# score than the default inclusion
excludes = ["fastapi", "tests", "pdm_build.py"]
# source-includes needs to explicitly define some value because PDM will check the
# truthy value of the list, and if empty, will include some defaults, including "tests",
# an empty string doesn't match anything, but makes the list truthy, so that PDM
# doesn't override it during the build.
source-includes = [""]
[tool.mypy]
plugins = ["pydantic.mypy"]
strict = true
@@ -218,16 +245,25 @@ disallow_incomplete_defs = false
disallow_untyped_defs = false
disallow_untyped_calls = false
[tool.pytest]
minversion = "9.0"
[tool.pytest.ini_options]
addopts = [
"--strict-config",
"--strict-markers",
"--ignore=docs_src",
]
strict_xfail = true
xfail_strict = true
junit_family = "xunit2"
filterwarnings = [
"error",
# see https://trio.readthedocs.io/en/stable/history.html#trio-0-22-0-2022-09-28
"ignore:You seem to already have a custom.*:RuntimeWarning:trio",
# TODO: remove after upgrading SQLAlchemy to a version that includes the following changes
# https://github.com/sqlalchemy/sqlalchemy/commit/59521abcc0676e936b31a523bd968fc157fef0c2
'ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning:sqlalchemy',
# Trio 24.1.0 raises a warning from attrs
# Ref: https://github.com/python-trio/trio/pull/3054
# Remove once there's a new version of Trio
'ignore:The `hash` argument is deprecated*:DeprecationWarning:trio',
]
[tool.coverage.run]
@@ -242,9 +278,9 @@ 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/tutorial008_an_py39.py", # difficult to mock
"docs_src/dependencies/tutorial013_an_py310.py", # temporary code example?
"docs_src/dependencies/tutorial014_an_py310.py", # temporary code example?
# Pydantic v1 migration, no longer tested
@@ -252,6 +288,202 @@ omit = [
"docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py",
"docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py",
"docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py",
# TODO: remove all the ignores below when all translations use the new Python 3.10 files
"docs_src/additional_responses/tutorial001_py39.py",
"docs_src/additional_responses/tutorial003_py39.py",
"docs_src/advanced_middleware/tutorial001_py39.py",
"docs_src/advanced_middleware/tutorial002_py39.py",
"docs_src/advanced_middleware/tutorial003_py39.py",
"docs_src/app_testing/app_a_py39/main.py",
"docs_src/app_testing/app_a_py39/test_main.py",
"docs_src/app_testing/tutorial001_py39.py",
"docs_src/app_testing/tutorial002_py39.py",
"docs_src/app_testing/tutorial003_py39.py",
"docs_src/app_testing/tutorial004_py39.py",
"docs_src/async_tests/app_a_py39/main.py",
"docs_src/async_tests/app_a_py39/test_main.py",
"docs_src/authentication_error_status_code/tutorial001_an_py39.py",
"docs_src/background_tasks/tutorial001_py39.py",
"docs_src/behind_a_proxy/tutorial001_01_py39.py",
"docs_src/behind_a_proxy/tutorial001_py39.py",
"docs_src/behind_a_proxy/tutorial002_py39.py",
"docs_src/behind_a_proxy/tutorial003_py39.py",
"docs_src/behind_a_proxy/tutorial004_py39.py",
"docs_src/bigger_applications/app_an_py39/dependencies.py",
"docs_src/bigger_applications/app_an_py39/internal/admin.py",
"docs_src/bigger_applications/app_an_py39/main.py",
"docs_src/bigger_applications/app_an_py39/routers/items.py",
"docs_src/bigger_applications/app_an_py39/routers/users.py",
"docs_src/bigger_applications/app_py39/dependencies.py",
"docs_src/bigger_applications/app_py39/main.py",
"docs_src/body_nested_models/tutorial008_py39.py",
"docs_src/body_nested_models/tutorial009_py39.py",
"docs_src/conditional_openapi/tutorial001_py39.py",
"docs_src/configure_swagger_ui/tutorial001_py39.py",
"docs_src/configure_swagger_ui/tutorial002_py39.py",
"docs_src/configure_swagger_ui/tutorial003_py39.py",
"docs_src/cors/tutorial001_py39.py",
"docs_src/custom_docs_ui/tutorial001_py39.py",
"docs_src/custom_docs_ui/tutorial002_py39.py",
"docs_src/custom_response/tutorial001_py39.py",
"docs_src/custom_response/tutorial001b_py39.py",
"docs_src/custom_response/tutorial002_py39.py",
"docs_src/custom_response/tutorial003_py39.py",
"docs_src/custom_response/tutorial004_py39.py",
"docs_src/custom_response/tutorial005_py39.py",
"docs_src/custom_response/tutorial006_py39.py",
"docs_src/custom_response/tutorial006b_py39.py",
"docs_src/custom_response/tutorial006c_py39.py",
"docs_src/custom_response/tutorial007_py39.py",
"docs_src/custom_response/tutorial008_py39.py",
"docs_src/custom_response/tutorial009_py39.py",
"docs_src/custom_response/tutorial009b_py39.py",
"docs_src/custom_response/tutorial009c_py39.py",
"docs_src/custom_response/tutorial010_py39.py",
"docs_src/debugging/tutorial001_py39.py",
"docs_src/dependencies/tutorial006_an_py39.py",
"docs_src/dependencies/tutorial006_py39.py",
"docs_src/dependencies/tutorial007_py39.py",
"docs_src/dependencies/tutorial008_py39.py",
"docs_src/dependencies/tutorial008b_an_py39.py",
"docs_src/dependencies/tutorial008b_py39.py",
"docs_src/dependencies/tutorial008c_an_py39.py",
"docs_src/dependencies/tutorial008c_py39.py",
"docs_src/dependencies/tutorial008d_an_py39.py",
"docs_src/dependencies/tutorial008d_py39.py",
"docs_src/dependencies/tutorial008e_an_py39.py",
"docs_src/dependencies/tutorial008e_py39.py",
"docs_src/dependencies/tutorial010_py39.py",
"docs_src/dependencies/tutorial011_an_py39.py",
"docs_src/dependencies/tutorial011_py39.py",
"docs_src/dependencies/tutorial012_an_py39.py",
"docs_src/dependencies/tutorial012_py39.py",
"docs_src/events/tutorial001_py39.py",
"docs_src/events/tutorial002_py39.py",
"docs_src/events/tutorial003_py39.py",
"docs_src/extending_openapi/tutorial001_py39.py",
"docs_src/extra_models/tutorial004_py39.py",
"docs_src/extra_models/tutorial005_py39.py",
"docs_src/first_steps/tutorial001_py39.py",
"docs_src/first_steps/tutorial003_py39.py",
"docs_src/generate_clients/tutorial001_py39.py",
"docs_src/generate_clients/tutorial002_py39.py",
"docs_src/generate_clients/tutorial003_py39.py",
"docs_src/generate_clients/tutorial004_py39.py",
"docs_src/graphql_/tutorial001_py39.py",
"docs_src/handling_errors/tutorial001_py39.py",
"docs_src/handling_errors/tutorial002_py39.py",
"docs_src/handling_errors/tutorial003_py39.py",
"docs_src/handling_errors/tutorial004_py39.py",
"docs_src/handling_errors/tutorial005_py39.py",
"docs_src/handling_errors/tutorial006_py39.py",
"docs_src/metadata/tutorial001_1_py39.py",
"docs_src/metadata/tutorial001_py39.py",
"docs_src/metadata/tutorial002_py39.py",
"docs_src/metadata/tutorial003_py39.py",
"docs_src/metadata/tutorial004_py39.py",
"docs_src/middleware/tutorial001_py39.py",
"docs_src/openapi_webhooks/tutorial001_py39.py",
"docs_src/path_operation_advanced_configuration/tutorial001_py39.py",
"docs_src/path_operation_advanced_configuration/tutorial002_py39.py",
"docs_src/path_operation_advanced_configuration/tutorial003_py39.py",
"docs_src/path_operation_advanced_configuration/tutorial005_py39.py",
"docs_src/path_operation_advanced_configuration/tutorial006_py39.py",
"docs_src/path_operation_advanced_configuration/tutorial007_py39.py",
"docs_src/path_operation_configuration/tutorial002b_py39.py",
"docs_src/path_operation_configuration/tutorial006_py39.py",
"docs_src/path_params/tutorial001_py39.py",
"docs_src/path_params/tutorial002_py39.py",
"docs_src/path_params/tutorial003_py39.py",
"docs_src/path_params/tutorial003b_py39.py",
"docs_src/path_params/tutorial004_py39.py",
"docs_src/path_params/tutorial005_py39.py",
"docs_src/path_params_numeric_validations/tutorial002_an_py39.py",
"docs_src/path_params_numeric_validations/tutorial002_py39.py",
"docs_src/path_params_numeric_validations/tutorial003_an_py39.py",
"docs_src/path_params_numeric_validations/tutorial003_py39.py",
"docs_src/path_params_numeric_validations/tutorial004_an_py39.py",
"docs_src/path_params_numeric_validations/tutorial004_py39.py",
"docs_src/path_params_numeric_validations/tutorial005_an_py39.py",
"docs_src/path_params_numeric_validations/tutorial005_py39.py",
"docs_src/path_params_numeric_validations/tutorial006_an_py39.py",
"docs_src/path_params_numeric_validations/tutorial006_py39.py",
"docs_src/python_types/tutorial001_py39.py",
"docs_src/python_types/tutorial002_py39.py",
"docs_src/python_types/tutorial003_py39.py",
"docs_src/python_types/tutorial004_py39.py",
"docs_src/python_types/tutorial005_py39.py",
"docs_src/python_types/tutorial006_py39.py",
"docs_src/python_types/tutorial007_py39.py",
"docs_src/python_types/tutorial008_py39.py",
"docs_src/python_types/tutorial008b_py39.py",
"docs_src/python_types/tutorial009_py39.py",
"docs_src/python_types/tutorial009b_py39.py",
"docs_src/python_types/tutorial009c_py39.py",
"docs_src/python_types/tutorial010_py39.py",
"docs_src/python_types/tutorial013_py39.py",
"docs_src/query_params/tutorial001_py39.py",
"docs_src/query_params/tutorial005_py39.py",
"docs_src/query_params_str_validations/tutorial005_an_py39.py",
"docs_src/query_params_str_validations/tutorial005_py39.py",
"docs_src/query_params_str_validations/tutorial006_an_py39.py",
"docs_src/query_params_str_validations/tutorial006_py39.py",
"docs_src/query_params_str_validations/tutorial012_an_py39.py",
"docs_src/query_params_str_validations/tutorial012_py39.py",
"docs_src/query_params_str_validations/tutorial013_an_py39.py",
"docs_src/query_params_str_validations/tutorial013_py39.py",
"docs_src/request_files/tutorial001_03_an_py39.py",
"docs_src/request_files/tutorial001_03_py39.py",
"docs_src/request_files/tutorial001_an_py39.py",
"docs_src/request_files/tutorial001_py39.py",
"docs_src/request_files/tutorial002_an_py39.py",
"docs_src/request_files/tutorial002_py39.py",
"docs_src/request_files/tutorial003_an_py39.py",
"docs_src/request_files/tutorial003_py39.py",
"docs_src/request_form_models/tutorial001_an_py39.py",
"docs_src/request_form_models/tutorial001_py39.py",
"docs_src/request_form_models/tutorial002_an_py39.py",
"docs_src/request_form_models/tutorial002_py39.py",
"docs_src/request_forms/tutorial001_an_py39.py",
"docs_src/request_forms/tutorial001_py39.py",
"docs_src/request_forms_and_files/tutorial001_an_py39.py",
"docs_src/request_forms_and_files/tutorial001_py39.py",
"docs_src/response_change_status_code/tutorial001_py39.py",
"docs_src/response_cookies/tutorial001_py39.py",
"docs_src/response_cookies/tutorial002_py39.py",
"docs_src/response_directly/tutorial002_py39.py",
"docs_src/response_headers/tutorial001_py39.py",
"docs_src/response_headers/tutorial002_py39.py",
"docs_src/response_model/tutorial003_02_py39.py",
"docs_src/response_model/tutorial003_03_py39.py",
"docs_src/response_status_code/tutorial001_py39.py",
"docs_src/response_status_code/tutorial002_py39.py",
"docs_src/security/tutorial001_an_py39.py",
"docs_src/security/tutorial001_py39.py",
"docs_src/security/tutorial006_an_py39.py",
"docs_src/security/tutorial006_py39.py",
"docs_src/security/tutorial007_an_py39.py",
"docs_src/security/tutorial007_py39.py",
"docs_src/settings/app01_py39/config.py",
"docs_src/settings/app01_py39/main.py",
"docs_src/settings/app02_an_py39/config.py",
"docs_src/settings/app02_an_py39/main.py",
"docs_src/settings/app02_an_py39/test_main.py",
"docs_src/settings/app02_py39/config.py",
"docs_src/settings/app02_py39/main.py",
"docs_src/settings/app02_py39/test_main.py",
"docs_src/settings/app03_an_py39/config.py",
"docs_src/settings/app03_an_py39/main.py",
"docs_src/settings/app03_py39/config.py",
"docs_src/settings/app03_py39/main.py",
"docs_src/settings/tutorial001_py39.py",
"docs_src/static_files/tutorial001_py39.py",
"docs_src/sub_applications/tutorial001_py39.py",
"docs_src/templates/tutorial001_py39.py",
"docs_src/using_request_directly/tutorial001_py39.py",
"docs_src/websockets/tutorial001_py39.py",
"docs_src/websockets/tutorial003_py39.py",
"docs_src/wsgi/tutorial001_py39.py",
]
[tool.coverage.report]
@@ -316,7 +548,6 @@ ignore = [
"docs_src/security/tutorial005_an_py39.py" = ["B904"]
"docs_src/security/tutorial005_py310.py" = ["B904"]
"docs_src/security/tutorial005_py39.py" = ["B904"]
"docs_src/json_base64_bytes/tutorial001_py310.py" = ["UP012"]
[tool.ruff.lint.isort]
known-third-party = ["fastapi", "pydantic", "starlette"]

View File

@@ -5,7 +5,6 @@ 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
@@ -16,63 +15,12 @@ from pydantic import BaseModel, SecretStr
from pydantic_settings import BaseSettings
github_graphql_url = "https://api.github.com/graphql"
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()
questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0"
discussions_query = """
query Q($after: String, $category_id: ID) {
repository(name: "fastapi", owner: "fastapi") {
discussions(first: 30, after: $after, categoryId: $category_id) {
discussions(first: 100, after: $after, categoryId: $category_id) {
edges {
cursor
node {
@@ -110,11 +58,6 @@ query Q($after: String, $category_id: ID) {
}
}
}
rateLimit {
cost
remaining
resetAt
}
}
"""
@@ -177,7 +120,7 @@ class Settings(BaseSettings):
github_token: SecretStr
github_repository: str
httpx_timeout: int = 30
speed_multiplier: float = 1.0
sleep_interval: int = 5
def get_graphql_response(
@@ -215,18 +158,11 @@ def get_graphql_question_discussion_edges(
settings: Settings,
after: str | None = None,
) -> list[DiscussionsEdge]:
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"],
data = get_graphql_response(
settings=settings,
query=discussions_query,
after=after,
category_id=questions_category_id,
)
graphql_response = DiscussionsResponse.model_validate(data)
return graphql_response.data.repository.discussions.edges
@@ -249,6 +185,8 @@ 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
)
@@ -380,7 +318,6 @@ 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)

View File

@@ -1,37 +0,0 @@
import subprocess
import time
import httpx
from playwright.sync_api import Playwright, sync_playwright
# Run playwright codegen to generate the code below, copy paste the sections in run()
def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False)
# Update the viewport manually
context = browser.new_context(viewport={"width": 960, "height": 1080})
page = context.new_page()
page.goto("http://localhost:8000/docs")
page.get_by_role("button", name="POST /data Post Data").click()
# Manually add the screenshot
page.screenshot(path="docs/en/docs/img/tutorial/json-base64-bytes/image01.png")
# ---------------------
context.close()
browser.close()
process = subprocess.Popen(
["fastapi", "run", "docs_src/json_base64_bytes/tutorial001_py310.py"]
)
try:
for _ in range(3):
try:
response = httpx.get("http://localhost:8000/docs")
except httpx.ConnectError:
time.sleep(1)
break
with sync_playwright() as playwright:
run(playwright)
finally:
process.terminate()

View File

@@ -1,73 +0,0 @@
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"})

View File

@@ -1,51 +0,0 @@
from unittest.mock import patch
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
app = FastAPI()
@app.get("/default")
def get_default() -> Item:
return Item(name="widget", price=9.99)
@app.get("/explicit", response_class=JSONResponse)
def get_explicit() -> Item:
return Item(name="widget", price=9.99)
client = TestClient(app)
def test_default_response_class_skips_json_dumps():
"""When no response_class is set, the fast path serializes directly to
JSON bytes via Pydantic's dump_json and never calls json.dumps."""
with patch(
"starlette.responses.json.dumps", wraps=__import__("json").dumps
) as mock_dumps:
response = client.get("/default")
assert response.status_code == 200
assert response.json() == {"name": "widget", "price": 9.99}
mock_dumps.assert_not_called()
def test_explicit_response_class_uses_json_dumps():
"""When response_class is explicitly set to JSONResponse, the normal path
is used and json.dumps is called via JSONResponse.render()."""
with patch(
"starlette.responses.json.dumps", wraps=__import__("json").dumps
) as mock_dumps:
response = client.get("/explicit")
assert response.status_code == 200
assert response.json() == {"name": "widget", "price": 9.99}
mock_dumps.assert_called_once()

View File

@@ -1,14 +1,9 @@
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
with warnings.catch_warnings():
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
app = FastAPI(default_response_class=ORJSONResponse)
app = FastAPI(default_response_class=ORJSONResponse)
@app.get("/orjson_non_str_keys")
@@ -21,8 +16,6 @@ client = TestClient(app)
def test_orjson_non_str_keys():
with warnings.catch_warnings():
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
with client:
response = client.get("/orjson_non_str_keys")
with client:
response = client.get("/orjson_non_str_keys")
assert response.json() == {"msg": "Hello World", "1": 1}

View File

@@ -37,10 +37,7 @@ def test_list_schema(path: str):
"properties": {
"p": {
"type": "array",
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"items": {"type": "string", "format": "binary"},
"title": "P",
},
},
@@ -118,10 +115,7 @@ def test_list_alias_schema(path: str):
"properties": {
"p_alias": {
"type": "array",
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"items": {"type": "string", "format": "binary"},
"title": "P Alias",
},
},
@@ -227,10 +221,7 @@ def test_list_validation_alias_schema(path: str):
"properties": {
"p_val_alias": {
"type": "array",
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"items": {"type": "string", "format": "binary"},
"title": "P Val Alias",
},
},
@@ -347,10 +338,7 @@ def test_list_alias_and_validation_alias_schema(path: str):
"properties": {
"p_val_alias": {
"type": "array",
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"items": {"type": "string", "format": "binary"},
"title": "P Val Alias",
},
},

View File

@@ -37,7 +37,7 @@ def test_optional_schema(path: str):
"properties": {
"p": {
"anyOf": [
{"type": "string", "contentMediaType": "application/octet-stream"},
{"type": "string", "format": "binary"},
{"type": "null"},
],
"title": "P",
@@ -109,7 +109,7 @@ def test_optional_alias_schema(path: str):
"properties": {
"p_alias": {
"anyOf": [
{"type": "string", "contentMediaType": "application/octet-stream"},
{"type": "string", "format": "binary"},
{"type": "null"},
],
"title": "P Alias",
@@ -200,7 +200,7 @@ def test_optional_validation_alias_schema(path: str):
"properties": {
"p_val_alias": {
"anyOf": [
{"type": "string", "contentMediaType": "application/octet-stream"},
{"type": "string", "format": "binary"},
{"type": "null"},
],
"title": "P Val Alias",
@@ -296,7 +296,7 @@ def test_optional_alias_and_validation_alias_schema(path: str):
"properties": {
"p_val_alias": {
"anyOf": [
{"type": "string", "contentMediaType": "application/octet-stream"},
{"type": "string", "format": "binary"},
{"type": "null"},
],
"title": "P Val Alias",

View File

@@ -41,10 +41,7 @@ def test_optional_list_schema(path: str):
"anyOf": [
{
"type": "array",
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
@@ -119,10 +116,7 @@ def test_optional_list_alias_schema(path: str):
"anyOf": [
{
"type": "array",
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
@@ -211,10 +205,7 @@ def test_optional_validation_alias_schema(path: str):
"anyOf": [
{
"type": "array",
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
@@ -310,10 +301,7 @@ def test_optional_list_alias_and_validation_alias_schema(path: str):
"anyOf": [
{
"type": "array",
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],

View File

@@ -35,11 +35,7 @@ def test_required_schema(path: str):
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": {
"title": "P",
"type": "string",
"contentMediaType": "application/octet-stream",
},
"p": {"title": "P", "type": "string", "format": "binary"},
},
"required": ["p"],
"title": body_model_name,
@@ -113,11 +109,7 @@ def test_required_alias_schema(path: str):
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": {
"title": "P Alias",
"type": "string",
"contentMediaType": "application/octet-stream",
},
"p_alias": {"title": "P Alias", "type": "string", "format": "binary"},
},
"required": ["p_alias"],
"title": body_model_name,
@@ -224,7 +216,7 @@ def test_required_validation_alias_schema(path: str):
"p_val_alias": {
"title": "P Val Alias",
"type": "string",
"contentMediaType": "application/octet-stream",
"format": "binary",
},
},
"required": ["p_val_alias"],
@@ -337,7 +329,7 @@ def test_required_alias_and_validation_alias_schema(path: str):
"p_val_alias": {
"title": "P Val Alias",
"type": "string",
"contentMediaType": "application/octet-stream",
"format": "binary",
},
},
"required": ["p_val_alias"],

View File

@@ -1,44 +0,0 @@
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"}

View File

@@ -1,91 +0,0 @@
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

View File

@@ -1,61 +0,0 @@
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

View File

@@ -189,12 +189,18 @@ def test_geo_json(client: TestClient):
assert response.status_code == 200, response.text
def test_no_content_type_json(client: TestClient):
def test_no_content_type_is_json(client: TestClient):
response = client.post(
"/items/",
content='{"name": "Foo", "price": 50.5}',
)
assert response.status_code == 422, response.text
assert response.status_code == 200, response.text
assert response.json() == {
"name": "Foo",
"description": None,
"price": 50.5,
"tax": None,
}
def test_wrong_headers(client: TestClient):

View File

@@ -9,6 +9,7 @@ from inline_snapshot import snapshot
name="client",
params=[
pytest.param("tutorial001_py310"),
pytest.param("tutorial010_py310"),
],
)
def get_client(request: pytest.FixtureRequest):
@@ -17,14 +18,12 @@ 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

View File

@@ -1,25 +1,17 @@
import warnings
import pytest
from fastapi.exceptions import FastAPIDeprecationWarning
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
with warnings.catch_warnings():
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
from docs_src.custom_response.tutorial001b_py310 import app
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

View File

@@ -1,50 +0,0 @@
import importlib
import pytest
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
@pytest.fixture(
name="client",
params=[
pytest.param("tutorial010_py310"),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.custom_response.{request.param}")
client = TestClient(mod.app)
return client
def test_get_custom_response(client: TestClient):
response = client.get("/items/")
assert response.status_code == 200, response.text
assert response.text == snapshot("<h1>Items</h1><p>This is a list of items.</p>")
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"text/html": {"schema": {"type": "string"}}
},
}
},
"summary": "Read Items",
"operationId": "read_items_items__get",
}
}
},
}
)

View File

@@ -1,225 +0,0 @@
import importlib
import pytest
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from tests.utils import needs_py310
@pytest.fixture(
name="client",
params=[pytest.param("tutorial001_py310", marks=needs_py310)],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.json_base64_bytes.{request.param}")
client = TestClient(mod.app)
return client
def test_post_data(client: TestClient):
response = client.post(
"/data",
json={
"description": "A file",
"data": "SGVsbG8sIFdvcmxkIQ==",
},
)
assert response.status_code == 200, response.text
assert response.json() == {"description": "A file", "content": "Hello, World!"}
def test_get_data(client: TestClient):
response = client.get("/data")
assert response.status_code == 200, response.text
assert response.json() == {"description": "A plumbus", "data": "aGVsbG8="}
def test_post_data_in_out(client: TestClient):
response = client.post(
"/data-in-out",
json={
"description": "A plumbus",
"data": "SGVsbG8sIFdvcmxkIQ==",
},
)
assert response.status_code == 200, response.text
assert response.json() == {
"description": "A plumbus",
"data": "SGVsbG8sIFdvcmxkIQ==",
}
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/data": {
"get": {
"summary": "Get Data",
"operationId": "get_data_data_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DataOutput"
}
}
},
}
},
},
"post": {
"summary": "Post Data",
"operationId": "post_data_data_post",
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/DataInput"}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
},
},
"/data-in-out": {
"post": {
"summary": "Post Data In Out",
"operationId": "post_data_in_out_data_in_out_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DataInputOutput"
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DataInputOutput"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"DataInput": {
"properties": {
"description": {"type": "string", "title": "Description"},
"data": {
"type": "string",
"contentEncoding": "base64",
"contentMediaType": "application/octet-stream",
"title": "Data",
},
},
"type": "object",
"required": ["description", "data"],
"title": "DataInput",
},
"DataInputOutput": {
"properties": {
"description": {"type": "string", "title": "Description"},
"data": {
"type": "string",
"contentEncoding": "base64",
"contentMediaType": "application/octet-stream",
"title": "Data",
},
},
"type": "object",
"required": ["description", "data"],
"title": "DataInputOutput",
},
"DataOutput": {
"properties": {
"description": {"type": "string", "title": "Description"},
"data": {
"type": "string",
"contentEncoding": "base64",
"contentMediaType": "application/octet-stream",
"title": "Data",
},
},
"type": "object",
"required": ["description", "data"],
"title": "DataOutput",
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"ValidationError": {
"properties": {
"ctx": {"title": "Context", "type": "object"},
"input": {"title": "Input"},
"loc": {
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
"type": "array",
"title": "Location",
},
"msg": {"type": "string", "title": "Message"},
"type": {"type": "string", "title": "Error Type"},
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError",
},
}
},
}
)

View File

@@ -162,8 +162,8 @@ def test_openapi_schema(client: TestClient):
"properties": {
"file": {
"title": "File",
"contentMediaType": "application/octet-stream",
"type": "string",
"format": "binary",
}
},
},
@@ -175,7 +175,7 @@ def test_openapi_schema(client: TestClient):
"file": {
"title": "File",
"type": "string",
"contentMediaType": "application/octet-stream",
"format": "binary",
}
},
},

View File

@@ -134,10 +134,7 @@ def test_openapi_schema(client: TestClient):
"file": {
"title": "File",
"anyOf": [
{
"type": "string",
"contentMediaType": "application/octet-stream",
},
{"type": "string", "format": "binary"},
{"type": "null"},
],
}
@@ -150,10 +147,7 @@ def test_openapi_schema(client: TestClient):
"file": {
"title": "File",
"anyOf": [
{
"type": "string",
"contentMediaType": "application/octet-stream",
},
{"type": "string", "format": "binary"},
{"type": "null"},
],
}

View File

@@ -123,7 +123,7 @@ def test_openapi_schema(client: TestClient):
"title": "File",
"type": "string",
"description": "A file read as bytes",
"contentMediaType": "application/octet-stream",
"format": "binary",
}
},
},
@@ -134,9 +134,9 @@ def test_openapi_schema(client: TestClient):
"properties": {
"file": {
"title": "File",
"contentMediaType": "application/octet-stream",
"type": "string",
"description": "A file read as UploadFile",
"format": "binary",
}
},
},

View File

@@ -195,10 +195,7 @@ def test_openapi_schema(client: TestClient):
"files": {
"title": "Files",
"type": "array",
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"items": {"type": "string", "format": "binary"},
}
},
},
@@ -210,10 +207,7 @@ def test_openapi_schema(client: TestClient):
"files": {
"title": "Files",
"type": "array",
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"items": {"type": "string", "format": "binary"},
}
},
},

View File

@@ -165,10 +165,7 @@ def test_openapi_schema(client: TestClient):
"files": {
"title": "Files",
"type": "array",
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"items": {"type": "string", "format": "binary"},
"description": "Multiple files as bytes",
}
},
@@ -181,10 +178,7 @@ def test_openapi_schema(client: TestClient):
"files": {
"title": "Files",
"type": "array",
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"items": {"type": "string", "format": "binary"},
"description": "Multiple files as UploadFile",
}
},

View File

@@ -198,12 +198,12 @@ def test_openapi_schema(client: TestClient):
"file": {
"title": "File",
"type": "string",
"contentMediaType": "application/octet-stream",
"format": "binary",
},
"fileb": {
"title": "Fileb",
"contentMediaType": "application/octet-stream",
"type": "string",
"format": "binary",
},
"token": {"title": "Token", "type": "string"},
},

View File

@@ -1,43 +0,0 @@
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

90
uv.lock generated
View File

@@ -1083,10 +1083,12 @@ 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 = [
@@ -1132,7 +1134,6 @@ dev = [
{ name = "mkdocs-redirects" },
{ name = "mkdocstrings", extra = ["python"] },
{ name = "mypy" },
{ name = "orjson" },
{ name = "pillow" },
{ name = "playwright" },
{ name = "prek" },
@@ -1150,7 +1151,6 @@ dev = [
{ name = "typer" },
{ name = "types-orjson" },
{ name = "types-ujson" },
{ name = "ujson" },
]
docs = [
{ name = "black" },
@@ -1165,19 +1165,15 @@ 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" },
@@ -1196,7 +1192,6 @@ tests = [
{ name = "httpx" },
{ name = "inline-snapshot" },
{ name = "mypy" },
{ name = "orjson" },
{ name = "pwdlib", extra = ["argon2"] },
{ name = "pyjwt" },
{ name = "pytest" },
@@ -1207,7 +1202,6 @@ tests = [
{ name = "strawberry-graphql" },
{ name = "types-orjson" },
{ name = "types-ujson" },
{ name = "ujson" },
]
translations = [
{ name = "gitpython" },
@@ -1231,6 +1225,7 @@ 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" },
@@ -1245,6 +1240,7 @@ 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" },
@@ -1273,7 +1269,6 @@ 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" },
@@ -1281,7 +1276,7 @@ dev = [
{ name = "pydantic-ai", specifier = ">=0.4.10" },
{ name = "pygithub", specifier = ">=2.8.1" },
{ name = "pyjwt", specifier = ">=2.9.0" },
{ name = "pytest", specifier = ">=9.0.0" },
{ name = "pytest", specifier = ">=7.1.3,<9.0.0" },
{ name = "pytest-codspeed", specifier = ">=4.2.0" },
{ name = "python-slugify", specifier = ">=8.0.4" },
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
@@ -1291,7 +1286,6 @@ 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" },
@@ -1306,19 +1300,15 @@ 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" },
@@ -1337,10 +1327,9 @@ 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" },
{ name = "pytest", specifier = ">=7.1.3,<9.0.0" },
{ name = "pytest-codspeed", specifier = ">=4.2.0" },
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
{ name = "ruff", specifier = ">=0.14.14" },
@@ -1348,7 +1337,6 @@ 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" },
@@ -1607,7 +1595,7 @@ wheels = [
[[package]]
name = "flask"
version = "3.1.3"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
@@ -1617,9 +1605,9 @@ dependencies = [
{ name = "markupsafe" },
{ name = "werkzeug" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -1923,36 +1911,40 @@ wheels = [
]
[[package]]
name = "griffelib"
version = "2.0.0"
name = "griffe"
version = "1.15.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/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" },
{ 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" },
]
[[package]]
name = "griffe-typingdoc"
version = "0.3.1"
version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "griffelib" },
{ name = "griffe" },
{ name = "typing-extensions" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
name = "griffe-warnings-deprecated"
version = "1.1.1"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "griffelib" },
{ name = "griffe" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -2999,17 +2991,17 @@ python = [
[[package]]
name = "mkdocstrings-python"
version = "2.0.3"
version = "2.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "griffelib" },
{ name = "griffe" },
{ name = "mkdocs-autorefs" },
{ name = "mkdocstrings" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -3933,33 +3925,33 @@ email = [
[[package]]
name = "pydantic-ai"
version = "1.62.0"
version = "1.56.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/20/97/e3158fa976a29e9580ba1c59601590424bbb81179c359fd29de0dc23aa09/pydantic_ai-1.62.0.tar.gz", hash = "sha256:d6ae517e365ea3ea162ca8ae643f319e105b71b0b6218b83dcad1d1eb2e38c9b", size = 12130, upload-time = "2026-02-19T05:07:07.853Z" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
name = "pydantic-ai-slim"
version = "1.62.0"
version = "1.56.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "genai-prices" },
{ name = "griffelib" },
{ name = "griffe" },
{ name = "httpx" },
{ name = "opentelemetry-api" },
{ name = "pydantic" },
{ name = "pydantic-graph" },
{ name = "typing-inspection" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[package.optional-dependencies]
@@ -4177,7 +4169,7 @@ wheels = [
[[package]]
name = "pydantic-graph"
version = "1.62.0"
version = "1.56.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -4185,9 +4177,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "typing-inspection" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -4338,7 +4330,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.0.2"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -4349,9 +4341,9 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]