mirror of
https://github.com/fastapi/fastapi.git
synced 2026-02-24 02:38:13 -05:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01fb6bfef8 | ||
|
|
2f9c914d44 | ||
|
|
0cf27ecf88 | ||
|
|
3f30ca1a5e | ||
|
|
6af3832126 | ||
|
|
acdf52e0c8 | ||
|
|
5c863d0718 | ||
|
|
ac8621a76e | ||
|
|
22354a2530 | ||
|
|
94a1ee749e | ||
|
|
248d7fb9f5 | ||
|
|
da1937443d | ||
|
|
5161f7b42b | ||
|
|
fef2ce70d9 | ||
|
|
a3c8c37272 | ||
|
|
2826124378 | ||
|
|
4da264f0f3 | ||
|
|
c5559a66dd | ||
|
|
1cea8f659c | ||
|
|
b423b73c35 | ||
|
|
70e8558352 | ||
|
|
48e9835732 | ||
|
|
2e62fb1513 | ||
|
|
eb544e704c | ||
|
|
bc06e4296d | ||
|
|
590a5e5355 | ||
|
|
1e78a36b73 | ||
|
|
f921de6495 | ||
|
|
4ab8138554 | ||
|
|
468d5173ed | ||
|
|
c9455d5400 | ||
|
|
69ae1d0f28 | ||
|
|
083b6ebe9e | ||
|
|
1b9a351ee8 | ||
|
|
f55ab7e020 |
2
.github/labeler.yml
vendored
2
.github/labeler.yml
vendored
@@ -29,8 +29,6 @@ internal:
|
||||
- scripts/**
|
||||
- .gitignore
|
||||
- .pre-commit-config.yaml
|
||||
- pdm_build.py
|
||||
- requirements*.txt
|
||||
- uv.lock
|
||||
- docs/en/data/sponsors.yml
|
||||
- docs/en/overrides/main.html
|
||||
|
||||
10
.github/workflows/publish.yml
vendored
10
.github/workflows/publish.yml
vendored
@@ -8,11 +8,6 @@ on:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
package:
|
||||
- fastapi
|
||||
- fastapi-slim
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
@@ -26,14 +21,9 @@ 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
|
||||
|
||||
48
.github/workflows/test.yml
vendored
48
.github/workflows/test.yml
vendored
@@ -68,10 +68,8 @@ jobs:
|
||||
python-version: "3.13"
|
||||
coverage: coverage
|
||||
uv-resolution: highest
|
||||
# Ubuntu with 3.13 needs coverage for CodSpeed benchmarks
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.13"
|
||||
coverage: coverage
|
||||
uv-resolution: highest
|
||||
codspeed: codspeed
|
||||
- os: ubuntu-latest
|
||||
@@ -109,20 +107,10 @@ jobs:
|
||||
run: uv pip install "git+https://github.com/Kludex/starlette@main"
|
||||
- run: mkdir coverage
|
||||
- name: Test
|
||||
if: matrix.codspeed != 'codspeed'
|
||||
run: uv run --no-sync bash scripts/test.sh
|
||||
env:
|
||||
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
|
||||
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
|
||||
- name: CodSpeed benchmarks
|
||||
if: matrix.codspeed == 'codspeed'
|
||||
uses: CodSpeedHQ/action@v4
|
||||
env:
|
||||
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
|
||||
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
|
||||
with:
|
||||
mode: simulation
|
||||
run: uv run --no-sync coverage run -m pytest tests/ --codspeed
|
||||
# Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow
|
||||
- name: Store coverage files
|
||||
if: matrix.coverage == 'coverage'
|
||||
@@ -132,6 +120,39 @@ jobs:
|
||||
path: coverage
|
||||
include-hidden-files: true
|
||||
|
||||
benchmark:
|
||||
needs:
|
||||
- changes
|
||||
if: needs.changes.outputs.src == 'true' || github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
UV_PYTHON: "3.13"
|
||||
UV_RESOLUTION: highest
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
- name: Install Dependencies
|
||||
run: uv sync --no-dev --group tests --extra all
|
||||
- name: CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@v4
|
||||
with:
|
||||
mode: simulation
|
||||
run: uv run --no-sync pytest tests/benchmarks --codspeed
|
||||
|
||||
coverage-combine:
|
||||
needs:
|
||||
- test
|
||||
@@ -176,6 +197,7 @@ jobs:
|
||||
if: always()
|
||||
needs:
|
||||
- coverage-combine
|
||||
- benchmark
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
@@ -186,4 +208,4 @@ jobs:
|
||||
uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
allowed-skips: coverage-combine,test
|
||||
allowed-skips: coverage-combine,test,benchmark
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# Custom Response - HTML, Stream, File, others { #custom-response-html-stream-file-others }
|
||||
|
||||
By default, **FastAPI** will return the responses using `JSONResponse`.
|
||||
By default, **FastAPI** will return JSON responses.
|
||||
|
||||
You can override it by returning a `Response` directly as seen in [Return a Response directly](response-directly.md){.internal-link target=_blank}.
|
||||
|
||||
@@ -10,43 +10,27 @@ 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.
|
||||
|
||||
///
|
||||
|
||||
## Use `ORJSONResponse` { #use-orjsonresponse }
|
||||
## JSON Responses { #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`.
|
||||
By default FastAPI returns JSON responses.
|
||||
|
||||
Import the `Response` class (sub-class) you want to use and declare it in the *path operation decorator*.
|
||||
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.
|
||||
|
||||
For large responses, returning a `Response` directly is much faster than returning a dictionary.
|
||||
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`.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
### JSON Performance { #json-performance }
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial001b_py310.py hl[2,7] *}
|
||||
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*.
|
||||
|
||||
/// 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.
|
||||
|
||||
///
|
||||
{* ../../docs_src/response_model/tutorial001_01_py310.py ln[15:17] hl[16] *}
|
||||
|
||||
## HTML Response { #html-response }
|
||||
|
||||
@@ -154,40 +138,6 @@ 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.
|
||||
@@ -268,7 +218,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>, but with some custom settings not used in the included `ORJSONResponse` class.
|
||||
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.
|
||||
|
||||
Let's say you want it to return indented and formatted JSON, so you want to use the orjson option `orjson.OPT_INDENT_2`.
|
||||
|
||||
@@ -292,13 +242,21 @@ 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 `ORJSONResponse` by default, in all *path operations*, instead of `JSONResponse`.
|
||||
In the example below, **FastAPI** will use `HTMLResponse` by default, in all *path operations*, instead of JSON.
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial010_py310.py hl[2,4] *}
|
||||
|
||||
|
||||
@@ -2,19 +2,23 @@
|
||||
|
||||
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.
|
||||
|
||||
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 declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic.
|
||||
|
||||
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.
|
||||
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`.
|
||||
|
||||
But you can return a `JSONResponse` directly from your *path operations*.
|
||||
You could also create a `JSONResponse` directly and return it.
|
||||
|
||||
It might be useful, for example, to return custom headers or cookies.
|
||||
/// 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.
|
||||
|
||||
///
|
||||
|
||||
## Return a `Response` { #return-a-response }
|
||||
|
||||
In fact, you can return any `Response` or any sub-class of it.
|
||||
You can return any `Response` or any sub-class of it.
|
||||
|
||||
/// tip
|
||||
/// info
|
||||
|
||||
`JSONResponse` itself is a sub-class of `Response`.
|
||||
|
||||
@@ -56,6 +60,18 @@ 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.
|
||||
|
||||
88
docs/en/docs/advanced/strict-content-type.md
Normal file
88
docs/en/docs/advanced/strict-content-type.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Strict Content-Type Checking { #strict-content-type-checking }
|
||||
|
||||
By default, **FastAPI** uses strict `Content-Type` header checking for JSON request bodies, this means that JSON requests **must** include a valid `Content-Type` header (e.g. `application/json`) in order for the body to be parsed as JSON.
|
||||
|
||||
## CSRF Risk { #csrf-risk }
|
||||
|
||||
This default behavior provides protection against a class of **Cross-Site Request Forgery (CSRF)** attacks in a very specific scenario.
|
||||
|
||||
These attacks exploit the fact that browsers allow scripts to send requests without doing any CORS preflight check when they:
|
||||
|
||||
* don't have a `Content-Type` header (e.g. using `fetch()` with a `Blob` body)
|
||||
* and don't send any authentication credentials.
|
||||
|
||||
This type of attack is mainly relevant when:
|
||||
|
||||
* the application is running locally (e.g. on `localhost`) or in an internal network
|
||||
* and the application doesn't have any authentication, it expects that any request from the same network can be trusted.
|
||||
|
||||
## Example Attack { #example-attack }
|
||||
|
||||
Imagine you build a way to run a local AI agent.
|
||||
|
||||
It provides an API at
|
||||
|
||||
```
|
||||
http://localhost:8000/v1/agents/multivac
|
||||
```
|
||||
|
||||
There's also a frontend at
|
||||
|
||||
```
|
||||
http://localhost:8000
|
||||
```
|
||||
|
||||
/// tip
|
||||
|
||||
Note that both have the same host.
|
||||
|
||||
///
|
||||
|
||||
Then using the frontend you can make the AI agent do things on your behalf.
|
||||
|
||||
As it's running **locally**, and not in the open internet, you decide to **not have any authentication** set up, just trusting the access to the local network.
|
||||
|
||||
Then one of your users could install it and run it locally.
|
||||
|
||||
Then they could open a malicious website, e.g. something like
|
||||
|
||||
```
|
||||
https://evilhackers.example.com
|
||||
```
|
||||
|
||||
And that malicious website sends requests using `fetch()` with a `Blob` body to the local API at
|
||||
|
||||
```
|
||||
http://localhost:8000/v1/agents/multivac
|
||||
```
|
||||
|
||||
Even though the host of the malicious website and the local app is different, the browser won't trigger a CORS preflight request because:
|
||||
|
||||
* It's running without any authentication, it doesn't have to send any credentials.
|
||||
* The browser thinks it's not sending JSON (because of the missing `Content-Type` header).
|
||||
|
||||
Then the malicious website could make the local AI agent send angry messages to the user's ex-boss... or worse. 😅
|
||||
|
||||
## Open Internet { #open-internet }
|
||||
|
||||
If your app is in the open internet, you wouldn't "trust the network" and let anyone send privileged requests without authentication.
|
||||
|
||||
Attackers could simply run a script to send requests to your API, no need for browser interaction, so you are probably already securing any privileged endpoints.
|
||||
|
||||
In that case **this attack / risk doesn't apply to you**.
|
||||
|
||||
This risk and attack is mainly relevant when the app runs on the **local network** and that is the **only assumed protection**.
|
||||
|
||||
## Allowing Requests Without Content-Type { #allowing-requests-without-content-type }
|
||||
|
||||
If you need to support clients that don't send a `Content-Type` header, you can disable strict checking by setting `strict_content_type=False`:
|
||||
|
||||
{* ../../docs_src/strict_content_type/tutorial001_py310.py hl[4] *}
|
||||
|
||||
With this setting, requests without a `Content-Type` header will have their body parsed as JSON, which is the same behavior as older versions of FastAPI.
|
||||
|
||||
/// info
|
||||
|
||||
This behavior and configuration was added in FastAPI 0.132.0.
|
||||
|
||||
///
|
||||
@@ -6,6 +6,10 @@ 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}.
|
||||
|
||||
@@ -22,7 +22,13 @@ from fastapi.responses import (
|
||||
|
||||
## FastAPI Responses
|
||||
|
||||
There are a couple of custom FastAPI response classes, you can use them to optimize JSON performance.
|
||||
There were a couple of custom FastAPI response classes that were intended to optimize JSON performance.
|
||||
|
||||
However, they are now deprecated as you will now get better performance by using a [Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/).
|
||||
|
||||
That way, Pydantic will serialize the data into JSON bytes on the Rust side, which will achieve better performance than these custom JSON responses.
|
||||
|
||||
Read more about it in [Custom Response - HTML, Stream, File, others - `orjson` or Response Model](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model).
|
||||
|
||||
::: fastapi.responses.UJSONResponse
|
||||
options:
|
||||
|
||||
@@ -7,6 +7,51 @@ 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
|
||||
|
||||
@@ -13,6 +13,7 @@ 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:
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ nav:
|
||||
- advanced/generate-clients.md
|
||||
- advanced/advanced-python-types.md
|
||||
- advanced/json-base64-bytes.md
|
||||
- advanced/strict-content-type.md
|
||||
- fastapi-cli.md
|
||||
- Deployment:
|
||||
- deployment/index.md
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
app = FastAPI(default_response_class=HTMLResponse)
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items():
|
||||
return [{"item_id": "Foo"}]
|
||||
return "<h1>Items</h1><p>This is a list of items.</p>"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
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
|
||||
0
docs_src/strict_content_type/__init__.py
Normal file
0
docs_src/strict_content_type/__init__.py
Normal file
14
docs_src/strict_content_type/tutorial001_py310.py
Normal file
14
docs_src/strict_content_type/tutorial001_py310.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI(strict_content_type=False)
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
@app.post("/items/")
|
||||
async def create_item(item: Item):
|
||||
return item
|
||||
640
fastapi/.agents/skills/fastapi/SKILL.md
Normal file
640
fastapi/.agents/skills/fastapi/SKILL.md
Normal file
@@ -0,0 +1,640 @@
|
||||
---
|
||||
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 []
|
||||
# ...
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.129.1"
|
||||
__version__ = "0.132.0"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -199,6 +199,32 @@ 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.
|
||||
|
||||
@@ -840,6 +840,29 @@ class FastAPI(Starlette):
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
strict_content_type: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
Enable strict checking for request Content-Type headers.
|
||||
|
||||
When `True` (the default), requests with a body that do not include
|
||||
a `Content-Type` header will **not** be parsed as JSON.
|
||||
|
||||
This prevents potential cross-site request forgery (CSRF) attacks
|
||||
that exploit the browser's ability to send requests without a
|
||||
Content-Type header, bypassing CORS preflight checks. In particular
|
||||
applicable for apps that need to be run locally (in localhost).
|
||||
|
||||
When `False`, requests without a `Content-Type` header will have
|
||||
their body parsed as JSON, which maintains compatibility with
|
||||
certain clients that don't send `Content-Type` headers.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/).
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
**extra: Annotated[
|
||||
Any,
|
||||
Doc(
|
||||
@@ -974,6 +997,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema=include_in_schema,
|
||||
responses=responses,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
strict_content_type=strict_content_type,
|
||||
)
|
||||
self.exception_handlers: dict[
|
||||
Any, Callable[[Request, Any], Response | Awaitable[Response]]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from starlette.responses import FileResponse as FileResponse # noqa
|
||||
from starlette.responses import HTMLResponse as HTMLResponse # noqa
|
||||
from starlette.responses import JSONResponse as JSONResponse # noqa
|
||||
@@ -7,6 +8,7 @@ from starlette.responses import PlainTextResponse as PlainTextResponse # noqa
|
||||
from starlette.responses import RedirectResponse as RedirectResponse # noqa
|
||||
from starlette.responses import Response as Response # noqa
|
||||
from starlette.responses import StreamingResponse as StreamingResponse # noqa
|
||||
from typing_extensions import deprecated
|
||||
|
||||
try:
|
||||
import ujson
|
||||
@@ -20,12 +22,29 @@ except ImportError: # pragma: nocover
|
||||
orjson = None # type: ignore
|
||||
|
||||
|
||||
@deprecated(
|
||||
"UJSONResponse is deprecated, FastAPI now serializes data directly to JSON "
|
||||
"bytes via Pydantic when a return type or response model is set, which is "
|
||||
"faster and doesn't need a custom response class. Read more in the FastAPI "
|
||||
"docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model "
|
||||
"and https://fastapi.tiangolo.com/tutorial/response-model/",
|
||||
category=FastAPIDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
class UJSONResponse(JSONResponse):
|
||||
"""
|
||||
JSON response using the high-performance ujson library to serialize data to JSON.
|
||||
"""JSON response using the ujson library to serialize data to JSON.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
|
||||
**Deprecated**: `UJSONResponse` is deprecated. FastAPI now serializes data
|
||||
directly to JSON bytes via Pydantic when a return type or response model is
|
||||
set, which is faster and doesn't need a custom response class.
|
||||
|
||||
Read more in the
|
||||
[FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model)
|
||||
and the
|
||||
[FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/).
|
||||
|
||||
**Note**: `ujson` is not included with FastAPI and must be installed
|
||||
separately, e.g. `pip install ujson`.
|
||||
"""
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
@@ -33,12 +52,29 @@ class UJSONResponse(JSONResponse):
|
||||
return ujson.dumps(content, ensure_ascii=False).encode("utf-8")
|
||||
|
||||
|
||||
@deprecated(
|
||||
"ORJSONResponse is deprecated, FastAPI now serializes data directly to JSON "
|
||||
"bytes via Pydantic when a return type or response model is set, which is "
|
||||
"faster and doesn't need a custom response class. Read more in the FastAPI "
|
||||
"docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model "
|
||||
"and https://fastapi.tiangolo.com/tutorial/response-model/",
|
||||
category=FastAPIDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
class ORJSONResponse(JSONResponse):
|
||||
"""
|
||||
JSON response using the high-performance orjson library to serialize data to JSON.
|
||||
"""JSON response using the orjson library to serialize data to JSON.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
|
||||
**Deprecated**: `ORJSONResponse` is deprecated. FastAPI now serializes data
|
||||
directly to JSON bytes via Pydantic when a return type or response model is
|
||||
set, which is faster and doesn't need a custom response class.
|
||||
|
||||
Read more in the
|
||||
[FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model)
|
||||
and the
|
||||
[FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/).
|
||||
|
||||
**Note**: `orjson` is not included with FastAPI and must be installed
|
||||
separately, e.g. `pip install orjson`.
|
||||
"""
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
|
||||
@@ -271,6 +271,7 @@ 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:
|
||||
@@ -286,8 +287,8 @@ async def serialize_response(
|
||||
body=response_content,
|
||||
endpoint_ctx=ctx,
|
||||
)
|
||||
|
||||
return field.serialize(
|
||||
serializer = field.serialize_json if dump_json else field.serialize
|
||||
return serializer(
|
||||
value,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
@@ -328,6 +329,7 @@ def get_request_handler(
|
||||
response_model_exclude_none: bool = False,
|
||||
dependency_overrides_provider: Any | None = None,
|
||||
embed_body_fields: bool = False,
|
||||
strict_content_type: bool | DefaultPlaceholder = Default(True),
|
||||
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
||||
assert dependant.call is not None, "dependant.call must be a function"
|
||||
is_coroutine = dependant.is_coroutine_callable
|
||||
@@ -336,6 +338,10 @@ def get_request_handler(
|
||||
actual_response_class: type[Response] = response_class.value
|
||||
else:
|
||||
actual_response_class = response_class
|
||||
if isinstance(strict_content_type, DefaultPlaceholder):
|
||||
actual_strict_content_type: bool = strict_content_type.value
|
||||
else:
|
||||
actual_strict_content_type = strict_content_type
|
||||
|
||||
async def app(request: Request) -> Response:
|
||||
response: Response | None = None
|
||||
@@ -369,7 +375,8 @@ def get_request_handler(
|
||||
json_body: Any = Undefined
|
||||
content_type_value = request.headers.get("content-type")
|
||||
if not content_type_value:
|
||||
json_body = await request.json()
|
||||
if not actual_strict_content_type:
|
||||
json_body = await request.json()
|
||||
else:
|
||||
message = email.message.Message()
|
||||
message["content-type"] = content_type_value
|
||||
@@ -443,6 +450,14 @@ 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,
|
||||
@@ -454,8 +469,16 @@ def get_request_handler(
|
||||
exclude_none=response_model_exclude_none,
|
||||
is_coroutine=is_coroutine,
|
||||
endpoint_ctx=endpoint_ctx,
|
||||
dump_json=use_dump_json,
|
||||
)
|
||||
response = actual_response_class(content, **response_args)
|
||||
if use_dump_json:
|
||||
response = Response(
|
||||
content=content,
|
||||
media_type="application/json",
|
||||
**response_args,
|
||||
)
|
||||
else:
|
||||
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)
|
||||
@@ -582,6 +605,7 @@ class APIRoute(routing.Route):
|
||||
openapi_extra: dict[str, Any] | None = None,
|
||||
generate_unique_id_function: Callable[["APIRoute"], str]
|
||||
| DefaultPlaceholder = Default(generate_unique_id),
|
||||
strict_content_type: bool | DefaultPlaceholder = Default(True),
|
||||
) -> None:
|
||||
self.path = path
|
||||
self.endpoint = endpoint
|
||||
@@ -608,6 +632,7 @@ class APIRoute(routing.Route):
|
||||
self.callbacks = callbacks
|
||||
self.openapi_extra = openapi_extra
|
||||
self.generate_unique_id_function = generate_unique_id_function
|
||||
self.strict_content_type = strict_content_type
|
||||
self.tags = tags or []
|
||||
self.responses = responses or {}
|
||||
self.name = get_name(endpoint) if name is None else name
|
||||
@@ -696,6 +721,7 @@ class APIRoute(routing.Route):
|
||||
response_model_exclude_none=self.response_model_exclude_none,
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
embed_body_fields=self._embed_body_fields,
|
||||
strict_content_type=self.strict_content_type,
|
||||
)
|
||||
|
||||
def matches(self, scope: Scope) -> tuple[Match, Scope]:
|
||||
@@ -946,6 +972,29 @@ class APIRouter(routing.Router):
|
||||
"""
|
||||
),
|
||||
] = Default(generate_unique_id),
|
||||
strict_content_type: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
Enable strict checking for request Content-Type headers.
|
||||
|
||||
When `True` (the default), requests with a body that do not include
|
||||
a `Content-Type` header will **not** be parsed as JSON.
|
||||
|
||||
This prevents potential cross-site request forgery (CSRF) attacks
|
||||
that exploit the browser's ability to send requests without a
|
||||
Content-Type header, bypassing CORS preflight checks. In particular
|
||||
applicable for apps that need to be run locally (in localhost).
|
||||
|
||||
When `False`, requests without a `Content-Type` header will have
|
||||
their body parsed as JSON, which maintains compatibility with
|
||||
certain clients that don't send `Content-Type` headers.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/).
|
||||
"""
|
||||
),
|
||||
] = Default(True),
|
||||
) -> None:
|
||||
# Determine the lifespan context to use
|
||||
if lifespan is None:
|
||||
@@ -992,6 +1041,7 @@ class APIRouter(routing.Router):
|
||||
self.route_class = route_class
|
||||
self.default_response_class = default_response_class
|
||||
self.generate_unique_id_function = generate_unique_id_function
|
||||
self.strict_content_type = strict_content_type
|
||||
|
||||
def route(
|
||||
self,
|
||||
@@ -1042,6 +1092,7 @@ class APIRouter(routing.Router):
|
||||
openapi_extra: dict[str, Any] | None = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str]
|
||||
| DefaultPlaceholder = Default(generate_unique_id),
|
||||
strict_content_type: bool | DefaultPlaceholder = Default(True),
|
||||
) -> None:
|
||||
route_class = route_class_override or self.route_class
|
||||
responses = responses or {}
|
||||
@@ -1088,6 +1139,9 @@ class APIRouter(routing.Router):
|
||||
callbacks=current_callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=current_generate_unique_id,
|
||||
strict_content_type=get_value_or_default(
|
||||
strict_content_type, self.strict_content_type
|
||||
),
|
||||
)
|
||||
self.routes.append(route)
|
||||
|
||||
@@ -1463,6 +1517,11 @@ class APIRouter(routing.Router):
|
||||
callbacks=current_callbacks,
|
||||
openapi_extra=route.openapi_extra,
|
||||
generate_unique_id_function=current_generate_unique_id,
|
||||
strict_content_type=get_value_or_default(
|
||||
route.strict_content_type,
|
||||
router.strict_content_type,
|
||||
self.strict_content_type,
|
||||
),
|
||||
)
|
||||
elif isinstance(route, routing.Route):
|
||||
methods = list(route.methods or [])
|
||||
|
||||
40
pdm_build.py
40
pdm_build.py
@@ -1,40 +0,0 @@
|
||||
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
|
||||
250
pyproject.toml
250
pyproject.toml
@@ -57,7 +57,6 @@ 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
|
||||
@@ -106,10 +105,6 @@ all = [
|
||||
"itsdangerous >=1.1.0",
|
||||
# For Starlette's schema generation, would not be used with FastAPI
|
||||
"pyyaml >=5.3.1",
|
||||
# For UJSONResponse
|
||||
"ujson >=5.8.0",
|
||||
# For ORJSONResponse
|
||||
"orjson >=3.9.3",
|
||||
# To validate email fields
|
||||
"email-validator >=2.0.0",
|
||||
# Uvicorn with uvloop
|
||||
@@ -152,6 +147,10 @@ docs = [
|
||||
docs-tests = [
|
||||
"httpx >=0.23.0,<1.0.0",
|
||||
"ruff >=0.14.14",
|
||||
# For UJSONResponse
|
||||
"ujson >=5.8.0",
|
||||
# For ORJSONResponse
|
||||
"orjson >=3.9.3",
|
||||
]
|
||||
github-actions = [
|
||||
"httpx >=0.27.0,<1.0.0",
|
||||
@@ -171,7 +170,7 @@ tests = [
|
||||
"mypy >=1.14.1",
|
||||
"pwdlib[argon2] >=0.2.1",
|
||||
"pyjwt >=2.9.0",
|
||||
"pytest >=7.1.3,<9.0.0",
|
||||
"pytest >=9.0.0",
|
||||
"pytest-codspeed >=4.2.0",
|
||||
"pyyaml >=5.3.1,<7.0.0",
|
||||
"sqlmodel >=0.0.31",
|
||||
@@ -199,32 +198,6 @@ 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
|
||||
@@ -245,25 +218,16 @@ disallow_incomplete_defs = false
|
||||
disallow_untyped_defs = false
|
||||
disallow_untyped_calls = false
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
[tool.pytest]
|
||||
minversion = "9.0"
|
||||
addopts = [
|
||||
"--strict-config",
|
||||
"--strict-markers",
|
||||
"--ignore=docs_src",
|
||||
]
|
||||
xfail_strict = true
|
||||
junit_family = "xunit2"
|
||||
strict_xfail = true
|
||||
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]
|
||||
@@ -278,9 +242,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
|
||||
@@ -288,202 +252,6 @@ 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]
|
||||
|
||||
@@ -5,6 +5,7 @@ import time
|
||||
from collections import Counter
|
||||
from collections.abc import Container
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import ceil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -15,12 +16,63 @@ from pydantic import BaseModel, SecretStr
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
github_graphql_url = "https://api.github.com/graphql"
|
||||
questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0"
|
||||
questions_category_id = "DIC_kwDOCZduT84B6E2a"
|
||||
|
||||
|
||||
POINTS_PER_MINUTE_LIMIT = 84 # 5000 points per hour
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
def __init__(self) -> None:
|
||||
self.last_query_cost: int = 1
|
||||
self.remaining_points: int = 5000
|
||||
self.reset_at: datetime = datetime.fromtimestamp(0, timezone.utc)
|
||||
self.last_request_start_time: datetime = datetime.fromtimestamp(0, timezone.utc)
|
||||
self.speed_multiplier: float = 1.0
|
||||
|
||||
def __enter__(self) -> "RateLimiter":
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
|
||||
# Handle primary rate limits
|
||||
primary_limit_wait_time = 0.0
|
||||
if self.remaining_points <= self.last_query_cost:
|
||||
primary_limit_wait_time = (self.reset_at - now).total_seconds() + 2
|
||||
logging.warning(
|
||||
f"Approaching GitHub API rate limit, remaining points: {self.remaining_points}, "
|
||||
f"reset time in {primary_limit_wait_time} seconds"
|
||||
)
|
||||
|
||||
# Handle secondary rate limits
|
||||
secondary_limit_wait_time = 0.0
|
||||
points_per_minute = POINTS_PER_MINUTE_LIMIT * self.speed_multiplier
|
||||
interval = 60 / (points_per_minute / self.last_query_cost)
|
||||
time_since_last_request = (now - self.last_request_start_time).total_seconds()
|
||||
if time_since_last_request < interval:
|
||||
secondary_limit_wait_time = interval - time_since_last_request
|
||||
|
||||
final_wait_time = ceil(max(primary_limit_wait_time, secondary_limit_wait_time))
|
||||
logging.info(f"Sleeping for {final_wait_time} seconds to respect rate limit")
|
||||
time.sleep(max(final_wait_time, 1))
|
||||
|
||||
self.last_request_start_time = datetime.now(tz=timezone.utc)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
pass
|
||||
|
||||
def update_request_info(self, cost: int, remaining: int, reset_at: str) -> None:
|
||||
self.last_query_cost = cost
|
||||
self.remaining_points = remaining
|
||||
self.reset_at = datetime.fromisoformat(reset_at.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
rate_limiter = RateLimiter()
|
||||
|
||||
|
||||
discussions_query = """
|
||||
query Q($after: String, $category_id: ID) {
|
||||
repository(name: "fastapi", owner: "fastapi") {
|
||||
discussions(first: 100, after: $after, categoryId: $category_id) {
|
||||
discussions(first: 30, after: $after, categoryId: $category_id) {
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
@@ -58,6 +110,11 @@ query Q($after: String, $category_id: ID) {
|
||||
}
|
||||
}
|
||||
}
|
||||
rateLimit {
|
||||
cost
|
||||
remaining
|
||||
resetAt
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -120,7 +177,7 @@ class Settings(BaseSettings):
|
||||
github_token: SecretStr
|
||||
github_repository: str
|
||||
httpx_timeout: int = 30
|
||||
sleep_interval: int = 5
|
||||
speed_multiplier: float = 1.0
|
||||
|
||||
|
||||
def get_graphql_response(
|
||||
@@ -158,11 +215,18 @@ def get_graphql_question_discussion_edges(
|
||||
settings: Settings,
|
||||
after: str | None = None,
|
||||
) -> list[DiscussionsEdge]:
|
||||
data = get_graphql_response(
|
||||
settings=settings,
|
||||
query=discussions_query,
|
||||
after=after,
|
||||
category_id=questions_category_id,
|
||||
with rate_limiter:
|
||||
data = get_graphql_response(
|
||||
settings=settings,
|
||||
query=discussions_query,
|
||||
after=after,
|
||||
category_id=questions_category_id,
|
||||
)
|
||||
|
||||
rate_limiter.update_request_info(
|
||||
cost=data["data"]["rateLimit"]["cost"],
|
||||
remaining=data["data"]["rateLimit"]["remaining"],
|
||||
reset_at=data["data"]["rateLimit"]["resetAt"],
|
||||
)
|
||||
graphql_response = DiscussionsResponse.model_validate(data)
|
||||
return graphql_response.data.repository.discussions.edges
|
||||
@@ -185,8 +249,6 @@ def get_discussion_nodes(settings: Settings) -> list[DiscussionsNode]:
|
||||
for discussion_edge in discussion_edges:
|
||||
discussion_nodes.append(discussion_edge.node)
|
||||
last_edge = discussion_edges[-1]
|
||||
# Handle GitHub secondary rate limits, requests per minute
|
||||
time.sleep(settings.sleep_interval)
|
||||
discussion_edges = get_graphql_question_discussion_edges(
|
||||
settings=settings, after=last_edge.cursor
|
||||
)
|
||||
@@ -318,6 +380,7 @@ def main() -> None:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
settings = Settings()
|
||||
logging.info(f"Using config: {settings.model_dump_json()}")
|
||||
rate_limiter.speed_multiplier = settings.speed_multiplier
|
||||
g = Github(settings.github_token.get_secret_value())
|
||||
repo = g.get_repo(settings.github_repository)
|
||||
|
||||
|
||||
73
tests/test_deprecated_responses.py
Normal file
73
tests/test_deprecated_responses.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from fastapi.responses import ORJSONResponse, UJSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
# ORJSON
|
||||
|
||||
|
||||
def _make_orjson_app() -> FastAPI:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
|
||||
@app.get("/items")
|
||||
def get_items() -> Item:
|
||||
return Item(name="widget", price=9.99)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_orjson_response_returns_correct_data():
|
||||
app = _make_orjson_app()
|
||||
client = TestClient(app)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
response = client.get("/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "widget", "price": 9.99}
|
||||
|
||||
|
||||
def test_orjson_response_emits_deprecation_warning():
|
||||
with pytest.warns(FastAPIDeprecationWarning, match="ORJSONResponse is deprecated"):
|
||||
ORJSONResponse(content={"hello": "world"})
|
||||
|
||||
|
||||
# UJSON
|
||||
|
||||
|
||||
def _make_ujson_app() -> FastAPI:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
app = FastAPI(default_response_class=UJSONResponse)
|
||||
|
||||
@app.get("/items")
|
||||
def get_items() -> Item:
|
||||
return Item(name="widget", price=9.99)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_ujson_response_returns_correct_data():
|
||||
app = _make_ujson_app()
|
||||
client = TestClient(app)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
response = client.get("/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "widget", "price": 9.99}
|
||||
|
||||
|
||||
def test_ujson_response_emits_deprecation_warning():
|
||||
with pytest.warns(FastAPIDeprecationWarning, match="UJSONResponse is deprecated"):
|
||||
UJSONResponse(content={"hello": "world"})
|
||||
51
tests/test_dump_json_fast_path.py
Normal file
51
tests/test_dump_json_fast_path.py
Normal file
@@ -0,0 +1,51 @@
|
||||
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()
|
||||
@@ -1,9 +1,14 @@
|
||||
import warnings
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
|
||||
|
||||
@app.get("/orjson_non_str_keys")
|
||||
@@ -16,6 +21,8 @@ client = TestClient(app)
|
||||
|
||||
|
||||
def test_orjson_non_str_keys():
|
||||
with client:
|
||||
response = client.get("/orjson_non_str_keys")
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
with client:
|
||||
response = client.get("/orjson_non_str_keys")
|
||||
assert response.json() == {"msg": "Hello World", "1": 1}
|
||||
|
||||
44
tests/test_strict_content_type_app_level.py
Normal file
44
tests/test_strict_content_type_app_level.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
app_default = FastAPI()
|
||||
|
||||
|
||||
@app_default.post("/items/")
|
||||
async def app_default_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
app_lax = FastAPI(strict_content_type=False)
|
||||
|
||||
|
||||
@app_lax.post("/items/")
|
||||
async def app_lax_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
client_default = TestClient(app_default)
|
||||
client_lax = TestClient(app_lax)
|
||||
|
||||
|
||||
def test_default_strict_rejects_no_content_type():
|
||||
response = client_default.post("/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_default_strict_accepts_json_content_type():
|
||||
response = client_default.post("/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"key": "value"}
|
||||
|
||||
|
||||
def test_lax_accepts_no_content_type():
|
||||
response = client_lax.post("/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"key": "value"}
|
||||
|
||||
|
||||
def test_lax_accepts_json_content_type():
|
||||
response = client_lax.post("/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"key": "value"}
|
||||
91
tests/test_strict_content_type_nested.py
Normal file
91
tests/test_strict_content_type_nested.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
# Lax app with nested routers, inner overrides to strict
|
||||
|
||||
app_nested = FastAPI(strict_content_type=False) # lax app
|
||||
outer_router = APIRouter(prefix="/outer") # inherits lax from app
|
||||
inner_strict = APIRouter(prefix="/strict", strict_content_type=True)
|
||||
inner_default = APIRouter(prefix="/default")
|
||||
|
||||
|
||||
@inner_strict.post("/items/")
|
||||
async def inner_strict_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
@inner_default.post("/items/")
|
||||
async def inner_default_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
outer_router.include_router(inner_strict)
|
||||
outer_router.include_router(inner_default)
|
||||
app_nested.include_router(outer_router)
|
||||
|
||||
client_nested = TestClient(app_nested)
|
||||
|
||||
|
||||
def test_strict_inner_on_lax_app_rejects_no_content_type():
|
||||
response = client_nested.post("/outer/strict/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_default_inner_inherits_lax_from_app():
|
||||
response = client_nested.post("/outer/default/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"key": "value"}
|
||||
|
||||
|
||||
def test_strict_inner_accepts_json_content_type():
|
||||
response = client_nested.post("/outer/strict/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_default_inner_accepts_json_content_type():
|
||||
response = client_nested.post("/outer/default/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# Strict app -> lax outer router -> strict inner router
|
||||
|
||||
app_mixed = FastAPI(strict_content_type=True)
|
||||
mixed_outer = APIRouter(prefix="/outer", strict_content_type=False)
|
||||
mixed_inner = APIRouter(prefix="/inner", strict_content_type=True)
|
||||
|
||||
|
||||
@mixed_outer.post("/items/")
|
||||
async def mixed_outer_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
@mixed_inner.post("/items/")
|
||||
async def mixed_inner_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
mixed_outer.include_router(mixed_inner)
|
||||
app_mixed.include_router(mixed_outer)
|
||||
|
||||
client_mixed = TestClient(app_mixed)
|
||||
|
||||
|
||||
def test_lax_outer_on_strict_app_accepts_no_content_type():
|
||||
response = client_mixed.post("/outer/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"key": "value"}
|
||||
|
||||
|
||||
def test_strict_inner_on_lax_outer_rejects_no_content_type():
|
||||
response = client_mixed.post("/outer/inner/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_lax_outer_accepts_json_content_type():
|
||||
response = client_mixed.post("/outer/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_strict_inner_on_lax_outer_accepts_json_content_type():
|
||||
response = client_mixed.post("/outer/inner/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
61
tests/test_strict_content_type_router_level.py
Normal file
61
tests/test_strict_content_type_router_level.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
router_lax = APIRouter(prefix="/lax", strict_content_type=False)
|
||||
router_strict = APIRouter(prefix="/strict", strict_content_type=True)
|
||||
router_default = APIRouter(prefix="/default")
|
||||
|
||||
|
||||
@router_lax.post("/items/")
|
||||
async def router_lax_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
@router_strict.post("/items/")
|
||||
async def router_strict_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
@router_default.post("/items/")
|
||||
async def router_default_post(data: dict):
|
||||
return data
|
||||
|
||||
|
||||
app.include_router(router_lax)
|
||||
app.include_router(router_strict)
|
||||
app.include_router(router_default)
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_lax_router_on_strict_app_accepts_no_content_type():
|
||||
response = client.post("/lax/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"key": "value"}
|
||||
|
||||
|
||||
def test_strict_router_on_strict_app_rejects_no_content_type():
|
||||
response = client.post("/strict/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_default_router_inherits_strict_from_app():
|
||||
response = client.post("/default/items/", content='{"key": "value"}')
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_lax_router_accepts_json_content_type():
|
||||
response = client.post("/lax/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_strict_router_accepts_json_content_type():
|
||||
response = client.post("/strict/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_default_router_accepts_json_content_type():
|
||||
response = client.post("/default/items/", json={"key": "value"})
|
||||
assert response.status_code == 200
|
||||
@@ -189,18 +189,12 @@ def test_geo_json(client: TestClient):
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
|
||||
def test_no_content_type_is_json(client: TestClient):
|
||||
def test_no_content_type_json(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
content='{"name": "Foo", "price": 50.5}',
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"description": None,
|
||||
"price": 50.5,
|
||||
"tax": None,
|
||||
}
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
|
||||
def test_wrong_headers(client: TestClient):
|
||||
|
||||
@@ -9,7 +9,6 @@ from inline_snapshot import snapshot
|
||||
name="client",
|
||||
params=[
|
||||
pytest.param("tutorial001_py310"),
|
||||
pytest.param("tutorial010_py310"),
|
||||
],
|
||||
)
|
||||
def get_client(request: pytest.FixtureRequest):
|
||||
@@ -18,12 +17,14 @@ def get_client(request: pytest.FixtureRequest):
|
||||
return client
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_get_custom_response(client: TestClient):
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
from docs_src.custom_response.tutorial001b_py310 import app
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
from docs_src.custom_response.tutorial001b_py310 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_get_custom_response():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
50
tests/test_tutorial/test_custom_response/test_tutorial010.py
Normal file
50
tests/test_tutorial/test_custom_response/test_tutorial010.py
Normal file
@@ -0,0 +1,50 @@
|
||||
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",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
name="client",
|
||||
params=[
|
||||
"tutorial001_py310",
|
||||
],
|
||||
)
|
||||
def get_client(request: pytest.FixtureRequest):
|
||||
mod = importlib.import_module(f"docs_src.strict_content_type.{request.param}")
|
||||
client = TestClient(mod.app)
|
||||
return client
|
||||
|
||||
|
||||
def test_lax_post_without_content_type_is_parsed_as_json(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
content='{"name": "Foo", "price": 50.5}',
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"name": "Foo", "price": 50.5}
|
||||
|
||||
|
||||
def test_lax_post_with_json_content_type(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
json={"name": "Foo", "price": 50.5},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"name": "Foo", "price": 50.5}
|
||||
|
||||
|
||||
def test_lax_post_with_text_plain_is_still_rejected(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
content='{"name": "Foo", "price": 50.5}',
|
||||
headers={"Content-Type": "text/plain"},
|
||||
)
|
||||
assert response.status_code == 422, response.text
|
||||
90
uv.lock
generated
90
uv.lock
generated
@@ -1083,12 +1083,10 @@ all = [
|
||||
{ name = "httpx" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pydantic-extra-types" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "ujson" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
standard = [
|
||||
@@ -1134,6 +1132,7 @@ dev = [
|
||||
{ name = "mkdocs-redirects" },
|
||||
{ name = "mkdocstrings", extra = ["python"] },
|
||||
{ name = "mypy" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pillow" },
|
||||
{ name = "playwright" },
|
||||
{ name = "prek" },
|
||||
@@ -1151,6 +1150,7 @@ dev = [
|
||||
{ name = "typer" },
|
||||
{ name = "types-orjson" },
|
||||
{ name = "types-ujson" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "black" },
|
||||
@@ -1165,15 +1165,19 @@ docs = [
|
||||
{ name = "mkdocs-material" },
|
||||
{ name = "mkdocs-redirects" },
|
||||
{ name = "mkdocstrings", extra = ["python"] },
|
||||
{ name = "orjson" },
|
||||
{ name = "pillow" },
|
||||
{ name = "python-slugify" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "ruff" },
|
||||
{ name = "typer" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
docs-tests = [
|
||||
{ name = "httpx" },
|
||||
{ name = "orjson" },
|
||||
{ name = "ruff" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
github-actions = [
|
||||
{ name = "httpx" },
|
||||
@@ -1192,6 +1196,7 @@ tests = [
|
||||
{ name = "httpx" },
|
||||
{ name = "inline-snapshot" },
|
||||
{ name = "mypy" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pwdlib", extra = ["argon2"] },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "pytest" },
|
||||
@@ -1202,6 +1207,7 @@ tests = [
|
||||
{ name = "strawberry-graphql" },
|
||||
{ name = "types-orjson" },
|
||||
{ name = "types-ujson" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
translations = [
|
||||
{ name = "gitpython" },
|
||||
@@ -1225,7 +1231,6 @@ requires-dist = [
|
||||
{ name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" },
|
||||
{ name = "jinja2", marker = "extra == 'standard'", specifier = ">=3.1.5" },
|
||||
{ name = "jinja2", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=3.1.5" },
|
||||
{ name = "orjson", marker = "extra == 'all'", specifier = ">=3.9.3" },
|
||||
{ name = "pydantic", specifier = ">=2.7.0" },
|
||||
{ name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" },
|
||||
{ name = "pydantic-extra-types", marker = "extra == 'standard'", specifier = ">=2.0.0" },
|
||||
@@ -1240,7 +1245,6 @@ requires-dist = [
|
||||
{ name = "starlette", specifier = ">=0.40.0,<1.0.0" },
|
||||
{ name = "typing-extensions", specifier = ">=4.8.0" },
|
||||
{ name = "typing-inspection", specifier = ">=0.4.2" },
|
||||
{ name = "ujson", marker = "extra == 'all'", specifier = ">=5.8.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.12.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.12.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.12.0" },
|
||||
@@ -1269,6 +1273,7 @@ dev = [
|
||||
{ name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
|
||||
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
|
||||
{ name = "mypy", specifier = ">=1.14.1" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "pillow", specifier = ">=11.3.0" },
|
||||
{ name = "playwright", specifier = ">=1.57.0" },
|
||||
{ name = "prek", specifier = ">=0.2.22" },
|
||||
@@ -1276,7 +1281,7 @@ dev = [
|
||||
{ name = "pydantic-ai", specifier = ">=0.4.10" },
|
||||
{ name = "pygithub", specifier = ">=2.8.1" },
|
||||
{ name = "pyjwt", specifier = ">=2.9.0" },
|
||||
{ name = "pytest", specifier = ">=7.1.3,<9.0.0" },
|
||||
{ name = "pytest", specifier = ">=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" },
|
||||
@@ -1286,6 +1291,7 @@ dev = [
|
||||
{ name = "typer", specifier = ">=0.21.1" },
|
||||
{ name = "types-orjson", specifier = ">=3.6.2" },
|
||||
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "black", specifier = ">=25.1.0" },
|
||||
@@ -1300,15 +1306,19 @@ docs = [
|
||||
{ name = "mkdocs-material", specifier = ">=9.7.0" },
|
||||
{ name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
|
||||
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "pillow", specifier = ">=11.3.0" },
|
||||
{ name = "python-slugify", specifier = ">=8.0.4" },
|
||||
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
|
||||
{ name = "ruff", specifier = ">=0.14.14" },
|
||||
{ name = "typer", specifier = ">=0.21.1" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
docs-tests = [
|
||||
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "ruff", specifier = ">=0.14.14" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
github-actions = [
|
||||
{ name = "httpx", specifier = ">=0.27.0,<1.0.0" },
|
||||
@@ -1327,9 +1337,10 @@ 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 = ">=7.1.3,<9.0.0" },
|
||||
{ name = "pytest", specifier = ">=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" },
|
||||
@@ -1337,6 +1348,7 @@ tests = [
|
||||
{ name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" },
|
||||
{ name = "types-orjson", specifier = ">=3.6.2" },
|
||||
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
translations = [
|
||||
{ name = "gitpython", specifier = ">=3.1.46" },
|
||||
@@ -1595,7 +1607,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.1.2"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "blinker" },
|
||||
@@ -1605,9 +1617,9 @@ dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1911,40 +1923,36 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "griffe"
|
||||
version = "1.15.0"
|
||||
name = "griffelib"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "griffe-typingdoc"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "griffe" },
|
||||
{ name = "griffelib" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/77/d5e5fa0a8391bc2890ae45255847197299739833108dd76ee3c9b2ff0bba/griffe_typingdoc-0.3.0.tar.gz", hash = "sha256:59d9ef98d02caa7aed88d8df1119c9e48c02ed049ea50ce4018ace9331d20f8b", size = 33169, upload-time = "2025-10-23T12:01:39.037Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/26/28182e0c8055842bf3da774dee1d5b789c0f236c078dcbdca1937b5214dc/griffe_typingdoc-0.3.1.tar.gz", hash = "sha256:2ff4703115cb7f8a65b9fdcdd1f3c3a15f813b6554621b52eaad094c4782ce96", size = 31218, upload-time = "2026-02-21T09:38:54.409Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/af/aa32c13f753e2625ec895b1f56eee3c9380a2088a88a2c028955e223856e/griffe_typingdoc-0.3.0-py3-none-any.whl", hash = "sha256:4f6483fff7733a679d1dce142fb029f314125f3caaf0d620eb82e7390c8564bb", size = 9923, upload-time = "2025-10-23T12:01:37.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/c4/cf543fbde49e1ae44830ef0840a4d6ee9f4e4f338138a7766d4e37cf6440/griffe_typingdoc-0.3.1-py3-none-any.whl", hash = "sha256:ecbd457ef6883126b8b6023abf12e08c58e1c152238a2f0e2afdd67a64b07021", size = 10092, upload-time = "2026-02-20T14:53:47.84Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "griffe-warnings-deprecated"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "griffe" },
|
||||
{ name = "griffelib" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/0e/f034e1714eb2c694d6196c75f77a02f9c69d19f9961c4804a016397bf3e5/griffe_warnings_deprecated-1.1.0.tar.gz", hash = "sha256:7bf21de327d59c66c7ce08d0166aa4292ce0577ff113de5878f428d102b6f7c5", size = 33260, upload-time = "2024-12-10T21:02:18.395Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/9e/fc86f1e9270f143a395a601de81aa42a871722c34d4b3c7763658dc2e04d/griffe_warnings_deprecated-1.1.1.tar.gz", hash = "sha256:9261369bf2acb8b5d24a0dc7895cce788208513d4349031d4ea315b979b2e99f", size = 26262, upload-time = "2026-02-21T09:38:55.858Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/4c/b7241f03ad1f22ec2eed33b0f90c4f8c949e3395c4b7488670b07225a20b/griffe_warnings_deprecated-1.1.0-py3-none-any.whl", hash = "sha256:e7b0e8bfd6e5add3945d4d9805b2a41c72409e456733965be276d55f01e8a7a2", size = 5854, upload-time = "2024-12-10T21:02:16.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/3c/c2a9eee79bf2c8002d2fa370534bee93fdca39e8b1fc82e83d552d5d2c07/griffe_warnings_deprecated-1.1.1-py3-none-any.whl", hash = "sha256:4b7d765e82ca9139ed44ffe7bdebed0d3a46ce014ad5a35a2c22e9a16288737a", size = 6565, upload-time = "2026-02-20T15:35:23.577Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2991,17 +2999,17 @@ python = [
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings-python"
|
||||
version = "2.0.1"
|
||||
version = "2.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "griffe" },
|
||||
{ name = "griffelib" },
|
||||
{ name = "mkdocs-autorefs" },
|
||||
{ name = "mkdocstrings" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3925,33 +3933,33 @@ email = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-ai"
|
||||
version = "1.56.0"
|
||||
version = "1.62.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai", "xai"] },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/1a/800a1e02b259152a49d4c11d9103784a7482c7e9b067eeea23e949d3d80f/pydantic_ai-1.56.0.tar.gz", hash = "sha256:643ff71612df52315b3b4c4b41543657f603f567223eb33245dc8098f005bdc4", size = 11795, upload-time = "2026-02-06T01:13:21.122Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/97/e3158fa976a29e9580ba1c59601590424bbb81179c359fd29de0dc23aa09/pydantic_ai-1.62.0.tar.gz", hash = "sha256:d6ae517e365ea3ea162ca8ae643f319e105b71b0b6218b83dcad1d1eb2e38c9b", size = 12130, upload-time = "2026-02-19T05:07:07.853Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/35/f4a7fd2b9962ddb9b021f76f293e74fda71da190bb74b57ed5b343c93022/pydantic_ai-1.56.0-py3-none-any.whl", hash = "sha256:b6b3ac74bdc004693834750da4420ea2cde0d3cbc3f134c0b7544f98f1c00859", size = 7222, upload-time = "2026-02-06T01:13:11.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/7a/053aebfab576603e95fcfce1139de4a87e12bd5a2ef1ba00007a931c3ff0/pydantic_ai-1.62.0-py3-none-any.whl", hash = "sha256:1eb88f745ae045e63da41ad68966e8876c964d0f023fbf5d6a3f5d243370bd04", size = 7227, upload-time = "2026-02-19T05:06:58.341Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-ai-slim"
|
||||
version = "1.56.0"
|
||||
version = "1.62.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "genai-prices" },
|
||||
{ name = "griffe" },
|
||||
{ name = "griffelib" },
|
||||
{ name = "httpx" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-graph" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/5c/3a577825b9c1da8f287be7f2ee6fe9aab48bc8a80e65c8518052c589f51c/pydantic_ai_slim-1.56.0.tar.gz", hash = "sha256:9f9f9c56b1c735837880a515ae5661b465b40207b25f3a3434178098b2137f05", size = 415265, upload-time = "2026-02-06T01:13:23.58Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/8d/6350a49f2e4b636efbcfc233221420ab576e4ba4edba38254cb84ae4a1e6/pydantic_ai_slim-1.62.0.tar.gz", hash = "sha256:00d84f659107bbbd88823a3d3dbe7348385935a9870b9d7d4ba799256f6b6983", size = 422452, upload-time = "2026-02-19T05:07:10.292Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/4b/34682036528eeb9aaf093c2073540ddf399ab37b99d282a69ca41356f1aa/pydantic_ai_slim-1.56.0-py3-none-any.whl", hash = "sha256:d657e4113485020500b23b7390b0066e2a0277edc7577eaad2290735ca5dd7d5", size = 542270, upload-time = "2026-02-06T01:13:14.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/67/21e9b3b0944568662e3790c936226bd48a9f27c6b5f27b5916f5857bc4d8/pydantic_ai_slim-1.62.0-py3-none-any.whl", hash = "sha256:5210073fadd46f65859a67da67845093c487f025fa430ed027151f22ec684ab2", size = 549296, upload-time = "2026-02-19T05:07:01.624Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -4169,7 +4177,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-graph"
|
||||
version = "1.56.0"
|
||||
version = "1.62.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
@@ -4177,9 +4185,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/03/f92881cdb12d6f43e60e9bfd602e41c95408f06e2324d3729f7a194e2bcd/pydantic_graph-1.56.0.tar.gz", hash = "sha256:5e22972dbb43dbc379ab9944252ff864019abf3c7d465dcdf572fc8aec9a44a1", size = 58460, upload-time = "2026-02-06T01:13:26.708Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/b6/0b084c847ecd99624f4fbc5c8ecd3f67a2388a282a32612b2a68c3b3595f/pydantic_graph-1.62.0.tar.gz", hash = "sha256:efe56bee3a8ca35b11a3be6a5f7352419fe182ef1e1323a3267ee12dec95f3c7", size = 58529, upload-time = "2026-02-19T05:07:12.947Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/07/8c823eb4d196137c123d4d67434e185901d3cbaea3b0c2b7667da84e72c1/pydantic_graph-1.56.0-py3-none-any.whl", hash = "sha256:ec3f0a1d6fcedd4eb9c59fef45079c2ee4d4185878d70dae26440a9c974c6bb3", size = 72346, upload-time = "2026-02-06T01:13:18.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/12/1a9cbcd59fd070ba72b0fe544caa6ca97758518643523ec2bf1162084e0d/pydantic_graph-1.62.0-py3-none-any.whl", hash = "sha256:abe0e7b356b4d3202b069ec020d8dd1f647f55e9a0e85cd272dab48250bde87d", size = 72350, upload-time = "2026-02-19T05:07:05.305Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4330,7 +4338,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.2"
|
||||
version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -4341,9 +4349,9 @@ dependencies = [
|
||||
{ name = "pygments" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user