mirror of
https://github.com/fastapi/fastapi.git
synced 2026-02-24 10:46:42 -05:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daba0aa328 | ||
|
|
0c3581d5c4 | ||
|
|
c73bc94537 | ||
|
|
6c68838615 | ||
|
|
29d082ba24 | ||
|
|
2686c7fbbf | ||
|
|
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 |
48
.github/workflows/test.yml
vendored
48
.github/workflows/test.yml
vendored
@@ -68,10 +68,8 @@ jobs:
|
|||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
coverage: coverage
|
coverage: coverage
|
||||||
uv-resolution: highest
|
uv-resolution: highest
|
||||||
# Ubuntu with 3.13 needs coverage for CodSpeed benchmarks
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
coverage: coverage
|
|
||||||
uv-resolution: highest
|
uv-resolution: highest
|
||||||
codspeed: codspeed
|
codspeed: codspeed
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
@@ -109,20 +107,10 @@ jobs:
|
|||||||
run: uv pip install "git+https://github.com/Kludex/starlette@main"
|
run: uv pip install "git+https://github.com/Kludex/starlette@main"
|
||||||
- run: mkdir coverage
|
- run: mkdir coverage
|
||||||
- name: Test
|
- name: Test
|
||||||
if: matrix.codspeed != 'codspeed'
|
|
||||||
run: uv run --no-sync bash scripts/test.sh
|
run: uv run --no-sync bash scripts/test.sh
|
||||||
env:
|
env:
|
||||||
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
|
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
|
||||||
CONTEXT: ${{ 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
|
# Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow
|
||||||
- name: Store coverage files
|
- name: Store coverage files
|
||||||
if: matrix.coverage == 'coverage'
|
if: matrix.coverage == 'coverage'
|
||||||
@@ -132,6 +120,39 @@ jobs:
|
|||||||
path: coverage
|
path: coverage
|
||||||
include-hidden-files: true
|
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:
|
coverage-combine:
|
||||||
needs:
|
needs:
|
||||||
- test
|
- test
|
||||||
@@ -176,6 +197,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
needs:
|
needs:
|
||||||
- coverage-combine
|
- coverage-combine
|
||||||
|
- benchmark
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Dump GitHub context
|
- name: Dump GitHub context
|
||||||
@@ -186,4 +208,4 @@ jobs:
|
|||||||
uses: re-actors/alls-green@release/v1
|
uses: re-actors/alls-green@release/v1
|
||||||
with:
|
with:
|
||||||
jobs: ${{ toJSON(needs) }}
|
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 }
|
# 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}.
|
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`.
|
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
|
/// 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.
|
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
|
{* ../../docs_src/response_model/tutorial001_01_py310.py ln[15:17] hl[16] *}
|
||||||
|
|
||||||
The parameter `response_class` will also be used to define the "media type" of the response.
|
|
||||||
|
|
||||||
In this case, the HTTP header `Content-Type` will be set to `application/json`.
|
|
||||||
|
|
||||||
And it will be documented as such in OpenAPI.
|
|
||||||
|
|
||||||
///
|
|
||||||
|
|
||||||
/// tip
|
|
||||||
|
|
||||||
The `ORJSONResponse` is only available in FastAPI, not in Starlette.
|
|
||||||
|
|
||||||
///
|
|
||||||
|
|
||||||
## HTML Response { #html-response }
|
## 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.
|
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 }
|
### `RedirectResponse` { #redirectresponse }
|
||||||
|
|
||||||
Returns an HTTP redirect. Uses a 307 status code (Temporary Redirect) by default.
|
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.
|
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`.
|
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. 😉
|
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 }
|
## 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.
|
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`.
|
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] *}
|
{* ../../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.
|
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 }
|
## 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`.
|
`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] *}
|
{* ../../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 }
|
## Notes { #notes }
|
||||||
|
|
||||||
When you return a `Response` directly its data is not validated, converted (serialized), or documented automatically.
|
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}.
|
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 }
|
## 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}.
|
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
|
## 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
|
::: fastapi.responses.UJSONResponse
|
||||||
options:
|
options:
|
||||||
|
|||||||
@@ -7,6 +7,54 @@ hide:
|
|||||||
|
|
||||||
## Latest Changes
|
## Latest Changes
|
||||||
|
|
||||||
|
## 0.133.0
|
||||||
|
|
||||||
|
### Upgrades
|
||||||
|
|
||||||
|
* ⬆️ Add support for Starlette 1.0.0+. PR [#14987](https://github.com/fastapi/fastapi/pull/14987) by [@tiangolo](https://github.com/tiangolo).
|
||||||
|
|
||||||
|
## 0.132.1
|
||||||
|
|
||||||
|
### Refactors
|
||||||
|
|
||||||
|
* ♻️ Refactor logic to handle OpenAPI and Swagger UI escaping data. PR [#14986](https://github.com/fastapi/fastapi/pull/14986) by [@tiangolo](https://github.com/tiangolo).
|
||||||
|
|
||||||
|
### 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
|
## 0.129.2
|
||||||
|
|
||||||
### Internal
|
### Internal
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ FastAPI will use this return type to:
|
|||||||
* Add a **JSON Schema** for the response, in the OpenAPI *path operation*.
|
* Add a **JSON Schema** for the response, in the OpenAPI *path operation*.
|
||||||
* This will be used by the **automatic docs**.
|
* This will be used by the **automatic docs**.
|
||||||
* It will also be used by automatic client code generation tools.
|
* 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:
|
But most importantly:
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ nav:
|
|||||||
- advanced/generate-clients.md
|
- advanced/generate-clients.md
|
||||||
- advanced/advanced-python-types.md
|
- advanced/advanced-python-types.md
|
||||||
- advanced/json-base64-bytes.md
|
- advanced/json-base64-bytes.md
|
||||||
|
- advanced/strict-content-type.md
|
||||||
- fastapi-cli.md
|
- fastapi-cli.md
|
||||||
- Deployment:
|
- Deployment:
|
||||||
- deployment/index.md
|
- deployment/index.md
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from fastapi import FastAPI
|
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/")
|
@app.get("/items/")
|
||||||
async def read_items():
|
async def read_items():
|
||||||
return [{"item_id": "Foo"}]
|
return "<h1>Items</h1><p>This is a list of items.</p>"
|
||||||
|
|||||||
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
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||||
|
|
||||||
__version__ = "0.129.2"
|
__version__ = "0.133.0"
|
||||||
|
|
||||||
from starlette import status as status
|
from starlette import status as status
|
||||||
|
|
||||||
|
|||||||
@@ -199,6 +199,32 @@ class ModelField:
|
|||||||
exclude_none=exclude_none,
|
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:
|
def __hash__(self) -> int:
|
||||||
# Each ModelField is unique for our purposes, to allow making a dict from
|
# Each ModelField is unique for our purposes, to allow making a dict from
|
||||||
# ModelField to its JSON Schema.
|
# ModelField to its JSON Schema.
|
||||||
|
|||||||
@@ -840,6 +840,29 @@ class FastAPI(Starlette):
|
|||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
] = None,
|
] = 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[
|
**extra: Annotated[
|
||||||
Any,
|
Any,
|
||||||
Doc(
|
Doc(
|
||||||
@@ -974,6 +997,7 @@ class FastAPI(Starlette):
|
|||||||
include_in_schema=include_in_schema,
|
include_in_schema=include_in_schema,
|
||||||
responses=responses,
|
responses=responses,
|
||||||
generate_unique_id_function=generate_unique_id_function,
|
generate_unique_id_function=generate_unique_id_function,
|
||||||
|
strict_content_type=strict_content_type,
|
||||||
)
|
)
|
||||||
self.exception_handlers: dict[
|
self.exception_handlers: dict[
|
||||||
Any, Callable[[Request, Any], Response | Awaitable[Response]]
|
Any, Callable[[Request, Any], Response | Awaitable[Response]]
|
||||||
@@ -1077,16 +1101,18 @@ class FastAPI(Starlette):
|
|||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
if self.openapi_url:
|
if self.openapi_url:
|
||||||
urls = (server_data.get("url") for server_data in self.servers)
|
|
||||||
server_urls = {url for url in urls if url}
|
|
||||||
|
|
||||||
async def openapi(req: Request) -> JSONResponse:
|
async def openapi(req: Request) -> JSONResponse:
|
||||||
root_path = req.scope.get("root_path", "").rstrip("/")
|
root_path = req.scope.get("root_path", "").rstrip("/")
|
||||||
if root_path not in server_urls:
|
schema = self.openapi()
|
||||||
if root_path and self.root_path_in_servers:
|
if root_path and self.root_path_in_servers:
|
||||||
self.servers.insert(0, {"url": root_path})
|
server_urls = {s.get("url") for s in schema.get("servers", [])}
|
||||||
server_urls.add(root_path)
|
if root_path not in server_urls:
|
||||||
return JSONResponse(self.openapi())
|
schema = dict(schema)
|
||||||
|
schema["servers"] = [{"url": root_path}] + schema.get(
|
||||||
|
"servers", []
|
||||||
|
)
|
||||||
|
return JSONResponse(schema)
|
||||||
|
|
||||||
self.add_route(self.openapi_url, openapi, include_in_schema=False)
|
self.add_route(self.openapi_url, openapi, include_in_schema=False)
|
||||||
if self.openapi_url and self.docs_url:
|
if self.openapi_url and self.docs_url:
|
||||||
|
|||||||
@@ -5,6 +5,20 @@ from annotated_doc import Doc
|
|||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
|
|
||||||
|
def _html_safe_json(value: Any) -> str:
|
||||||
|
"""Serialize a value to JSON with HTML special characters escaped.
|
||||||
|
|
||||||
|
This prevents injection when the JSON is embedded inside a <script> tag.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
json.dumps(value)
|
||||||
|
.replace("<", "\\u003c")
|
||||||
|
.replace(">", "\\u003e")
|
||||||
|
.replace("&", "\\u0026")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
swagger_ui_default_parameters: Annotated[
|
swagger_ui_default_parameters: Annotated[
|
||||||
dict[str, Any],
|
dict[str, Any],
|
||||||
Doc(
|
Doc(
|
||||||
@@ -155,7 +169,7 @@ def get_swagger_ui_html(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
for key, value in current_swagger_ui_parameters.items():
|
for key, value in current_swagger_ui_parameters.items():
|
||||||
html += f"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n"
|
html += f"{_html_safe_json(key)}: {_html_safe_json(jsonable_encoder(value))},\n"
|
||||||
|
|
||||||
if oauth2_redirect_url:
|
if oauth2_redirect_url:
|
||||||
html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
|
html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
|
||||||
@@ -169,7 +183,7 @@ def get_swagger_ui_html(
|
|||||||
|
|
||||||
if init_oauth:
|
if init_oauth:
|
||||||
html += f"""
|
html += f"""
|
||||||
ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})
|
ui.initOAuth({_html_safe_json(jsonable_encoder(init_oauth))})
|
||||||
"""
|
"""
|
||||||
|
|
||||||
html += """
|
html += """
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||||
from starlette.responses import FileResponse as FileResponse # noqa
|
from starlette.responses import FileResponse as FileResponse # noqa
|
||||||
from starlette.responses import HTMLResponse as HTMLResponse # noqa
|
from starlette.responses import HTMLResponse as HTMLResponse # noqa
|
||||||
from starlette.responses import JSONResponse as JSONResponse # 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 RedirectResponse as RedirectResponse # noqa
|
||||||
from starlette.responses import Response as Response # noqa
|
from starlette.responses import Response as Response # noqa
|
||||||
from starlette.responses import StreamingResponse as StreamingResponse # noqa
|
from starlette.responses import StreamingResponse as StreamingResponse # noqa
|
||||||
|
from typing_extensions import deprecated
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ujson
|
import ujson
|
||||||
@@ -20,12 +22,29 @@ except ImportError: # pragma: nocover
|
|||||||
orjson = None # type: ignore
|
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):
|
class UJSONResponse(JSONResponse):
|
||||||
"""
|
"""JSON response using the ujson library to serialize data to JSON.
|
||||||
JSON response using the high-performance ujson library to serialize data to JSON.
|
|
||||||
|
|
||||||
Read more about it in the
|
**Deprecated**: `UJSONResponse` is deprecated. FastAPI now serializes data
|
||||||
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
|
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:
|
def render(self, content: Any) -> bytes:
|
||||||
@@ -33,12 +52,29 @@ class UJSONResponse(JSONResponse):
|
|||||||
return ujson.dumps(content, ensure_ascii=False).encode("utf-8")
|
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):
|
class ORJSONResponse(JSONResponse):
|
||||||
"""
|
"""JSON response using the orjson library to serialize data to JSON.
|
||||||
JSON response using the high-performance orjson library to serialize data to JSON.
|
|
||||||
|
|
||||||
Read more about it in the
|
**Deprecated**: `ORJSONResponse` is deprecated. FastAPI now serializes data
|
||||||
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
|
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:
|
def render(self, content: Any) -> bytes:
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ async def serialize_response(
|
|||||||
exclude_none: bool = False,
|
exclude_none: bool = False,
|
||||||
is_coroutine: bool = True,
|
is_coroutine: bool = True,
|
||||||
endpoint_ctx: EndpointContext | None = None,
|
endpoint_ctx: EndpointContext | None = None,
|
||||||
|
dump_json: bool = False,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if field:
|
if field:
|
||||||
if is_coroutine:
|
if is_coroutine:
|
||||||
@@ -286,8 +287,8 @@ async def serialize_response(
|
|||||||
body=response_content,
|
body=response_content,
|
||||||
endpoint_ctx=ctx,
|
endpoint_ctx=ctx,
|
||||||
)
|
)
|
||||||
|
serializer = field.serialize_json if dump_json else field.serialize
|
||||||
return field.serialize(
|
return serializer(
|
||||||
value,
|
value,
|
||||||
include=include,
|
include=include,
|
||||||
exclude=exclude,
|
exclude=exclude,
|
||||||
@@ -328,6 +329,7 @@ def get_request_handler(
|
|||||||
response_model_exclude_none: bool = False,
|
response_model_exclude_none: bool = False,
|
||||||
dependency_overrides_provider: Any | None = None,
|
dependency_overrides_provider: Any | None = None,
|
||||||
embed_body_fields: bool = False,
|
embed_body_fields: bool = False,
|
||||||
|
strict_content_type: bool | DefaultPlaceholder = Default(True),
|
||||||
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
||||||
assert dependant.call is not None, "dependant.call must be a function"
|
assert dependant.call is not None, "dependant.call must be a function"
|
||||||
is_coroutine = dependant.is_coroutine_callable
|
is_coroutine = dependant.is_coroutine_callable
|
||||||
@@ -336,6 +338,10 @@ def get_request_handler(
|
|||||||
actual_response_class: type[Response] = response_class.value
|
actual_response_class: type[Response] = response_class.value
|
||||||
else:
|
else:
|
||||||
actual_response_class = response_class
|
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:
|
async def app(request: Request) -> Response:
|
||||||
response: Response | None = None
|
response: Response | None = None
|
||||||
@@ -369,7 +375,8 @@ def get_request_handler(
|
|||||||
json_body: Any = Undefined
|
json_body: Any = Undefined
|
||||||
content_type_value = request.headers.get("content-type")
|
content_type_value = request.headers.get("content-type")
|
||||||
if not content_type_value:
|
if not content_type_value:
|
||||||
json_body = await request.json()
|
if not actual_strict_content_type:
|
||||||
|
json_body = await request.json()
|
||||||
else:
|
else:
|
||||||
message = email.message.Message()
|
message = email.message.Message()
|
||||||
message["content-type"] = content_type_value
|
message["content-type"] = content_type_value
|
||||||
@@ -443,6 +450,14 @@ def get_request_handler(
|
|||||||
response_args["status_code"] = current_status_code
|
response_args["status_code"] = current_status_code
|
||||||
if solved_result.response.status_code:
|
if solved_result.response.status_code:
|
||||||
response_args["status_code"] = 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(
|
content = await serialize_response(
|
||||||
field=response_field,
|
field=response_field,
|
||||||
response_content=raw_response,
|
response_content=raw_response,
|
||||||
@@ -454,8 +469,16 @@ def get_request_handler(
|
|||||||
exclude_none=response_model_exclude_none,
|
exclude_none=response_model_exclude_none,
|
||||||
is_coroutine=is_coroutine,
|
is_coroutine=is_coroutine,
|
||||||
endpoint_ctx=endpoint_ctx,
|
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):
|
if not is_body_allowed_for_status_code(response.status_code):
|
||||||
response.body = b""
|
response.body = b""
|
||||||
response.headers.raw.extend(solved_result.response.headers.raw)
|
response.headers.raw.extend(solved_result.response.headers.raw)
|
||||||
@@ -582,6 +605,7 @@ class APIRoute(routing.Route):
|
|||||||
openapi_extra: dict[str, Any] | None = None,
|
openapi_extra: dict[str, Any] | None = None,
|
||||||
generate_unique_id_function: Callable[["APIRoute"], str]
|
generate_unique_id_function: Callable[["APIRoute"], str]
|
||||||
| DefaultPlaceholder = Default(generate_unique_id),
|
| DefaultPlaceholder = Default(generate_unique_id),
|
||||||
|
strict_content_type: bool | DefaultPlaceholder = Default(True),
|
||||||
) -> None:
|
) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
@@ -608,6 +632,7 @@ class APIRoute(routing.Route):
|
|||||||
self.callbacks = callbacks
|
self.callbacks = callbacks
|
||||||
self.openapi_extra = openapi_extra
|
self.openapi_extra = openapi_extra
|
||||||
self.generate_unique_id_function = generate_unique_id_function
|
self.generate_unique_id_function = generate_unique_id_function
|
||||||
|
self.strict_content_type = strict_content_type
|
||||||
self.tags = tags or []
|
self.tags = tags or []
|
||||||
self.responses = responses or {}
|
self.responses = responses or {}
|
||||||
self.name = get_name(endpoint) if name is None else name
|
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,
|
response_model_exclude_none=self.response_model_exclude_none,
|
||||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||||
embed_body_fields=self._embed_body_fields,
|
embed_body_fields=self._embed_body_fields,
|
||||||
|
strict_content_type=self.strict_content_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
def matches(self, scope: Scope) -> tuple[Match, Scope]:
|
def matches(self, scope: Scope) -> tuple[Match, Scope]:
|
||||||
@@ -946,6 +972,29 @@ class APIRouter(routing.Router):
|
|||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
] = Default(generate_unique_id),
|
] = 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:
|
) -> None:
|
||||||
# Determine the lifespan context to use
|
# Determine the lifespan context to use
|
||||||
if lifespan is None:
|
if lifespan is None:
|
||||||
@@ -992,6 +1041,7 @@ class APIRouter(routing.Router):
|
|||||||
self.route_class = route_class
|
self.route_class = route_class
|
||||||
self.default_response_class = default_response_class
|
self.default_response_class = default_response_class
|
||||||
self.generate_unique_id_function = generate_unique_id_function
|
self.generate_unique_id_function = generate_unique_id_function
|
||||||
|
self.strict_content_type = strict_content_type
|
||||||
|
|
||||||
def route(
|
def route(
|
||||||
self,
|
self,
|
||||||
@@ -1042,6 +1092,7 @@ class APIRouter(routing.Router):
|
|||||||
openapi_extra: dict[str, Any] | None = None,
|
openapi_extra: dict[str, Any] | None = None,
|
||||||
generate_unique_id_function: Callable[[APIRoute], str]
|
generate_unique_id_function: Callable[[APIRoute], str]
|
||||||
| DefaultPlaceholder = Default(generate_unique_id),
|
| DefaultPlaceholder = Default(generate_unique_id),
|
||||||
|
strict_content_type: bool | DefaultPlaceholder = Default(True),
|
||||||
) -> None:
|
) -> None:
|
||||||
route_class = route_class_override or self.route_class
|
route_class = route_class_override or self.route_class
|
||||||
responses = responses or {}
|
responses = responses or {}
|
||||||
@@ -1088,6 +1139,9 @@ class APIRouter(routing.Router):
|
|||||||
callbacks=current_callbacks,
|
callbacks=current_callbacks,
|
||||||
openapi_extra=openapi_extra,
|
openapi_extra=openapi_extra,
|
||||||
generate_unique_id_function=current_generate_unique_id,
|
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)
|
self.routes.append(route)
|
||||||
|
|
||||||
@@ -1463,6 +1517,11 @@ class APIRouter(routing.Router):
|
|||||||
callbacks=current_callbacks,
|
callbacks=current_callbacks,
|
||||||
openapi_extra=route.openapi_extra,
|
openapi_extra=route.openapi_extra,
|
||||||
generate_unique_id_function=current_generate_unique_id,
|
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):
|
elif isinstance(route, routing.Route):
|
||||||
methods = list(route.methods or [])
|
methods = list(route.methods or [])
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ classifiers = [
|
|||||||
"Topic :: Internet :: WWW/HTTP",
|
"Topic :: Internet :: WWW/HTTP",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"starlette>=0.40.0,<1.0.0",
|
"starlette>=0.40.0",
|
||||||
"pydantic>=2.7.0",
|
"pydantic>=2.7.0",
|
||||||
"typing-extensions>=4.8.0",
|
"typing-extensions>=4.8.0",
|
||||||
"typing-inspection>=0.4.2",
|
"typing-inspection>=0.4.2",
|
||||||
@@ -105,10 +105,6 @@ all = [
|
|||||||
"itsdangerous >=1.1.0",
|
"itsdangerous >=1.1.0",
|
||||||
# For Starlette's schema generation, would not be used with FastAPI
|
# For Starlette's schema generation, would not be used with FastAPI
|
||||||
"pyyaml >=5.3.1",
|
"pyyaml >=5.3.1",
|
||||||
# For UJSONResponse
|
|
||||||
"ujson >=5.8.0",
|
|
||||||
# For ORJSONResponse
|
|
||||||
"orjson >=3.9.3",
|
|
||||||
# To validate email fields
|
# To validate email fields
|
||||||
"email-validator >=2.0.0",
|
"email-validator >=2.0.0",
|
||||||
# Uvicorn with uvloop
|
# Uvicorn with uvloop
|
||||||
@@ -151,6 +147,10 @@ docs = [
|
|||||||
docs-tests = [
|
docs-tests = [
|
||||||
"httpx >=0.23.0,<1.0.0",
|
"httpx >=0.23.0,<1.0.0",
|
||||||
"ruff >=0.14.14",
|
"ruff >=0.14.14",
|
||||||
|
# For UJSONResponse
|
||||||
|
"ujson >=5.8.0",
|
||||||
|
# For ORJSONResponse
|
||||||
|
"orjson >=3.9.3",
|
||||||
]
|
]
|
||||||
github-actions = [
|
github-actions = [
|
||||||
"httpx >=0.27.0,<1.0.0",
|
"httpx >=0.27.0,<1.0.0",
|
||||||
@@ -242,6 +242,7 @@ relative_files = true
|
|||||||
context = '${CONTEXT}'
|
context = '${CONTEXT}'
|
||||||
dynamic_context = "test_function"
|
dynamic_context = "test_function"
|
||||||
omit = [
|
omit = [
|
||||||
|
"tests/benchmarks/*",
|
||||||
"docs_src/response_model/tutorial003_04_py39.py",
|
"docs_src/response_model/tutorial003_04_py39.py",
|
||||||
"docs_src/response_model/tutorial003_04_py310.py",
|
"docs_src/response_model/tutorial003_04_py310.py",
|
||||||
"docs_src/dependencies/tutorial013_an_py310.py", # temporary code example?
|
"docs_src/dependencies/tutorial013_an_py310.py", # temporary code example?
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import time
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
from collections.abc import Container
|
from collections.abc import Container
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from math import ceil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -15,12 +16,63 @@ from pydantic import BaseModel, SecretStr
|
|||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
github_graphql_url = "https://api.github.com/graphql"
|
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 = """
|
discussions_query = """
|
||||||
query Q($after: String, $category_id: ID) {
|
query Q($after: String, $category_id: ID) {
|
||||||
repository(name: "fastapi", owner: "fastapi") {
|
repository(name: "fastapi", owner: "fastapi") {
|
||||||
discussions(first: 100, after: $after, categoryId: $category_id) {
|
discussions(first: 30, after: $after, categoryId: $category_id) {
|
||||||
edges {
|
edges {
|
||||||
cursor
|
cursor
|
||||||
node {
|
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_token: SecretStr
|
||||||
github_repository: str
|
github_repository: str
|
||||||
httpx_timeout: int = 30
|
httpx_timeout: int = 30
|
||||||
sleep_interval: int = 5
|
speed_multiplier: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
def get_graphql_response(
|
def get_graphql_response(
|
||||||
@@ -158,11 +215,18 @@ def get_graphql_question_discussion_edges(
|
|||||||
settings: Settings,
|
settings: Settings,
|
||||||
after: str | None = None,
|
after: str | None = None,
|
||||||
) -> list[DiscussionsEdge]:
|
) -> list[DiscussionsEdge]:
|
||||||
data = get_graphql_response(
|
with rate_limiter:
|
||||||
settings=settings,
|
data = get_graphql_response(
|
||||||
query=discussions_query,
|
settings=settings,
|
||||||
after=after,
|
query=discussions_query,
|
||||||
category_id=questions_category_id,
|
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)
|
graphql_response = DiscussionsResponse.model_validate(data)
|
||||||
return graphql_response.data.repository.discussions.edges
|
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:
|
for discussion_edge in discussion_edges:
|
||||||
discussion_nodes.append(discussion_edge.node)
|
discussion_nodes.append(discussion_edge.node)
|
||||||
last_edge = discussion_edges[-1]
|
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(
|
discussion_edges = get_graphql_question_discussion_edges(
|
||||||
settings=settings, after=last_edge.cursor
|
settings=settings, after=last_edge.cursor
|
||||||
)
|
)
|
||||||
@@ -318,6 +380,7 @@ def main() -> None:
|
|||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
logging.info(f"Using config: {settings.model_dump_json()}")
|
logging.info(f"Using config: {settings.model_dump_json()}")
|
||||||
|
rate_limiter.speed_multiplier = settings.speed_multiplier
|
||||||
g = Github(settings.github_token.get_secret_value())
|
g = Github(settings.github_token.get_secret_value())
|
||||||
repo = g.get_repo(settings.github_repository)
|
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()
|
||||||
75
tests/test_openapi_cache_root_path.py
Normal file
75
tests/test_openapi_cache_root_path.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
def test_root_path_does_not_persist_across_requests():
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def read_root(): # pragma: no cover
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
# Attacker request with a spoofed root_path
|
||||||
|
attacker_client = TestClient(app, root_path="/evil-api")
|
||||||
|
response1 = attacker_client.get("/openapi.json")
|
||||||
|
data1 = response1.json()
|
||||||
|
assert any(s.get("url") == "/evil-api" for s in data1.get("servers", []))
|
||||||
|
|
||||||
|
# Subsequent legitimate request with no root_path
|
||||||
|
clean_client = TestClient(app)
|
||||||
|
response2 = clean_client.get("/openapi.json")
|
||||||
|
data2 = response2.json()
|
||||||
|
servers = [s.get("url") for s in data2.get("servers", [])]
|
||||||
|
assert "/evil-api" not in servers
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_different_root_paths_do_not_accumulate():
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def read_root(): # pragma: no cover
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
for prefix in ["/path-a", "/path-b", "/path-c"]:
|
||||||
|
c = TestClient(app, root_path=prefix)
|
||||||
|
c.get("/openapi.json")
|
||||||
|
|
||||||
|
# A clean request should not have any of them
|
||||||
|
clean_client = TestClient(app)
|
||||||
|
response = clean_client.get("/openapi.json")
|
||||||
|
data = response.json()
|
||||||
|
servers = [s.get("url") for s in data.get("servers", [])]
|
||||||
|
for prefix in ["/path-a", "/path-b", "/path-c"]:
|
||||||
|
assert prefix not in servers, (
|
||||||
|
f"root_path '{prefix}' leaked into clean request: {servers}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_legitimate_root_path_still_appears():
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def read_root(): # pragma: no cover
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
client = TestClient(app, root_path="/api/v1")
|
||||||
|
response = client.get("/openapi.json")
|
||||||
|
data = response.json()
|
||||||
|
servers = [s.get("url") for s in data.get("servers", [])]
|
||||||
|
assert "/api/v1" in servers
|
||||||
|
|
||||||
|
|
||||||
|
def test_configured_servers_not_mutated():
|
||||||
|
configured_servers = [{"url": "https://prod.example.com"}]
|
||||||
|
app = FastAPI(servers=configured_servers)
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def read_root(): # pragma: no cover
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
# Request with a rogue root_path
|
||||||
|
attacker_client = TestClient(app, root_path="/evil")
|
||||||
|
attacker_client.get("/openapi.json")
|
||||||
|
|
||||||
|
# The original servers list must be untouched
|
||||||
|
assert configured_servers == [{"url": "https://prod.example.com"}]
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
|
import warnings
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||||
from fastapi.responses import ORJSONResponse
|
from fastapi.responses import ORJSONResponse
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from sqlalchemy.sql.elements import quoted_name
|
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")
|
@app.get("/orjson_non_str_keys")
|
||||||
@@ -16,6 +21,8 @@ client = TestClient(app)
|
|||||||
|
|
||||||
|
|
||||||
def test_orjson_non_str_keys():
|
def test_orjson_non_str_keys():
|
||||||
with client:
|
with warnings.catch_warnings():
|
||||||
response = client.get("/orjson_non_str_keys")
|
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||||
|
with client:
|
||||||
|
response = client.get("/orjson_non_str_keys")
|
||||||
assert response.json() == {"msg": "Hello World", "1": 1}
|
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
|
||||||
37
tests/test_swagger_ui_escape.py
Normal file
37
tests/test_swagger_ui_escape.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from fastapi.openapi.docs import get_swagger_ui_html
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_oauth_html_chars_are_escaped():
|
||||||
|
xss_payload = "Evil</script><script>alert(1)</script>"
|
||||||
|
html = get_swagger_ui_html(
|
||||||
|
openapi_url="/openapi.json",
|
||||||
|
title="Test",
|
||||||
|
init_oauth={"appName": xss_payload},
|
||||||
|
)
|
||||||
|
body = html.body.decode()
|
||||||
|
|
||||||
|
assert "</script><script>" not in body
|
||||||
|
assert "\\u003c/script\\u003e\\u003cscript\\u003e" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_swagger_ui_parameters_html_chars_are_escaped():
|
||||||
|
html = get_swagger_ui_html(
|
||||||
|
openapi_url="/openapi.json",
|
||||||
|
title="Test",
|
||||||
|
swagger_ui_parameters={"customKey": "<img src=x onerror=alert(1)>"},
|
||||||
|
)
|
||||||
|
body = html.body.decode()
|
||||||
|
assert "<img src=x onerror=alert(1)>" not in body
|
||||||
|
assert "\\u003cimg" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_normal_init_oauth_still_works():
|
||||||
|
html = get_swagger_ui_html(
|
||||||
|
openapi_url="/openapi.json",
|
||||||
|
title="Test",
|
||||||
|
init_oauth={"clientId": "my-client", "appName": "My App"},
|
||||||
|
)
|
||||||
|
body = html.body.decode()
|
||||||
|
assert '"clientId": "my-client"' in body
|
||||||
|
assert '"appName": "My App"' in body
|
||||||
|
assert "ui.initOAuth" in body
|
||||||
@@ -189,18 +189,12 @@ def test_geo_json(client: TestClient):
|
|||||||
assert response.status_code == 200, response.text
|
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(
|
response = client.post(
|
||||||
"/items/",
|
"/items/",
|
||||||
content='{"name": "Foo", "price": 50.5}',
|
content='{"name": "Foo", "price": 50.5}',
|
||||||
)
|
)
|
||||||
assert response.status_code == 200, response.text
|
assert response.status_code == 422, response.text
|
||||||
assert response.json() == {
|
|
||||||
"name": "Foo",
|
|
||||||
"description": None,
|
|
||||||
"price": 50.5,
|
|
||||||
"tax": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_wrong_headers(client: TestClient):
|
def test_wrong_headers(client: TestClient):
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from inline_snapshot import snapshot
|
|||||||
name="client",
|
name="client",
|
||||||
params=[
|
params=[
|
||||||
pytest.param("tutorial001_py310"),
|
pytest.param("tutorial001_py310"),
|
||||||
pytest.param("tutorial010_py310"),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def get_client(request: pytest.FixtureRequest):
|
def get_client(request: pytest.FixtureRequest):
|
||||||
@@ -18,12 +17,14 @@ def get_client(request: pytest.FixtureRequest):
|
|||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||||
def test_get_custom_response(client: TestClient):
|
def test_get_custom_response(client: TestClient):
|
||||||
response = client.get("/items/")
|
response = client.get("/items/")
|
||||||
assert response.status_code == 200, response.text
|
assert response.status_code == 200, response.text
|
||||||
assert response.json() == [{"item_id": "Foo"}]
|
assert response.json() == [{"item_id": "Foo"}]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||||
def test_openapi_schema(client: TestClient):
|
def test_openapi_schema(client: TestClient):
|
||||||
response = client.get("/openapi.json")
|
response = client.get("/openapi.json")
|
||||||
assert response.status_code == 200, response.text
|
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 fastapi.testclient import TestClient
|
||||||
from inline_snapshot import snapshot
|
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)
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||||
def test_get_custom_response():
|
def test_get_custom_response():
|
||||||
response = client.get("/items/")
|
response = client.get("/items/")
|
||||||
assert response.status_code == 200, response.text
|
assert response.status_code == 200, response.text
|
||||||
assert response.json() == [{"item_id": "Foo"}]
|
assert response.json() == [{"item_id": "Foo"}]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
response = client.get("/openapi.json")
|
response = client.get("/openapi.json")
|
||||||
assert response.status_code == 200, response.text
|
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
|
||||||
136
uv.lock
generated
136
uv.lock
generated
@@ -192,7 +192,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anthropic"
|
name = "anthropic"
|
||||||
version = "0.78.0"
|
version = "0.83.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
@@ -204,9 +204,9 @@ dependencies = [
|
|||||||
{ name = "sniffio" },
|
{ name = "sniffio" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ec/51/32849a48f9b1cfe80a508fd269b20bd8f0b1357c70ba092890fde5a6a10b/anthropic-0.78.0.tar.gz", hash = "sha256:55fd978ab9b049c61857463f4c4e9e092b24f892519c6d8078cee1713d8af06e", size = 509136, upload-time = "2026-02-05T17:52:04.986Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/db/e5/02cd2919ec327b24234abb73082e6ab84c451182cc3cc60681af700f4c63/anthropic-0.83.0.tar.gz", hash = "sha256:a8732c68b41869266c3034541a31a29d8be0f8cd0a714f9edce3128b351eceb4", size = 534058, upload-time = "2026-02-19T19:26:38.904Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/03/2f50931a942e5e13f80e24d83406714672c57964be593fc046d81369335b/anthropic-0.78.0-py3-none-any.whl", hash = "sha256:2a9887d2e99d1b0f9fe08857a1e9fe5d2d4030455dbf9ac65aab052e2efaeac4", size = 405485, upload-time = "2026-02-05T17:52:03.674Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/75/b9d58e4e2a4b1fc3e75ffbab978f999baf8b7c4ba9f96e60edb918ba386b/anthropic-0.83.0-py3-none-any.whl", hash = "sha256:f069ef508c73b8f9152e8850830d92bd5ef185645dbacf234bb213344a274810", size = 456991, upload-time = "2026-02-19T19:26:40.114Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1083,12 +1083,10 @@ all = [
|
|||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "itsdangerous" },
|
{ name = "itsdangerous" },
|
||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
{ name = "orjson" },
|
|
||||||
{ name = "pydantic-extra-types" },
|
{ name = "pydantic-extra-types" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "python-multipart" },
|
{ name = "python-multipart" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "ujson" },
|
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
]
|
]
|
||||||
standard = [
|
standard = [
|
||||||
@@ -1134,6 +1132,7 @@ dev = [
|
|||||||
{ name = "mkdocs-redirects" },
|
{ name = "mkdocs-redirects" },
|
||||||
{ name = "mkdocstrings", extra = ["python"] },
|
{ name = "mkdocstrings", extra = ["python"] },
|
||||||
{ name = "mypy" },
|
{ name = "mypy" },
|
||||||
|
{ name = "orjson" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "playwright" },
|
{ name = "playwright" },
|
||||||
{ name = "prek" },
|
{ name = "prek" },
|
||||||
@@ -1151,6 +1150,7 @@ dev = [
|
|||||||
{ name = "typer" },
|
{ name = "typer" },
|
||||||
{ name = "types-orjson" },
|
{ name = "types-orjson" },
|
||||||
{ name = "types-ujson" },
|
{ name = "types-ujson" },
|
||||||
|
{ name = "ujson" },
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [
|
||||||
{ name = "black" },
|
{ name = "black" },
|
||||||
@@ -1165,15 +1165,19 @@ docs = [
|
|||||||
{ name = "mkdocs-material" },
|
{ name = "mkdocs-material" },
|
||||||
{ name = "mkdocs-redirects" },
|
{ name = "mkdocs-redirects" },
|
||||||
{ name = "mkdocstrings", extra = ["python"] },
|
{ name = "mkdocstrings", extra = ["python"] },
|
||||||
|
{ name = "orjson" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "python-slugify" },
|
{ name = "python-slugify" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
{ name = "typer" },
|
{ name = "typer" },
|
||||||
|
{ name = "ujson" },
|
||||||
]
|
]
|
||||||
docs-tests = [
|
docs-tests = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
{ name = "orjson" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
|
{ name = "ujson" },
|
||||||
]
|
]
|
||||||
github-actions = [
|
github-actions = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
@@ -1192,6 +1196,7 @@ tests = [
|
|||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "inline-snapshot" },
|
{ name = "inline-snapshot" },
|
||||||
{ name = "mypy" },
|
{ name = "mypy" },
|
||||||
|
{ name = "orjson" },
|
||||||
{ name = "pwdlib", extra = ["argon2"] },
|
{ name = "pwdlib", extra = ["argon2"] },
|
||||||
{ name = "pyjwt" },
|
{ name = "pyjwt" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
@@ -1202,6 +1207,7 @@ tests = [
|
|||||||
{ name = "strawberry-graphql" },
|
{ name = "strawberry-graphql" },
|
||||||
{ name = "types-orjson" },
|
{ name = "types-orjson" },
|
||||||
{ name = "types-ujson" },
|
{ name = "types-ujson" },
|
||||||
|
{ name = "ujson" },
|
||||||
]
|
]
|
||||||
translations = [
|
translations = [
|
||||||
{ name = "gitpython" },
|
{ name = "gitpython" },
|
||||||
@@ -1225,7 +1231,6 @@ requires-dist = [
|
|||||||
{ name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" },
|
{ name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" },
|
||||||
{ name = "jinja2", marker = "extra == 'standard'", 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 = "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", specifier = ">=2.7.0" },
|
||||||
{ name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" },
|
{ name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" },
|
||||||
{ name = "pydantic-extra-types", marker = "extra == 'standard'", specifier = ">=2.0.0" },
|
{ name = "pydantic-extra-types", marker = "extra == 'standard'", specifier = ">=2.0.0" },
|
||||||
@@ -1237,10 +1242,9 @@ requires-dist = [
|
|||||||
{ name = "python-multipart", marker = "extra == 'standard'", specifier = ">=0.0.18" },
|
{ name = "python-multipart", marker = "extra == 'standard'", specifier = ">=0.0.18" },
|
||||||
{ name = "python-multipart", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.0.18" },
|
{ name = "python-multipart", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.0.18" },
|
||||||
{ name = "pyyaml", marker = "extra == 'all'", specifier = ">=5.3.1" },
|
{ name = "pyyaml", marker = "extra == 'all'", specifier = ">=5.3.1" },
|
||||||
{ name = "starlette", specifier = ">=0.40.0,<1.0.0" },
|
{ name = "starlette", specifier = ">=0.40.0" },
|
||||||
{ name = "typing-extensions", specifier = ">=4.8.0" },
|
{ name = "typing-extensions", specifier = ">=4.8.0" },
|
||||||
{ name = "typing-inspection", specifier = ">=0.4.2" },
|
{ 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 == 'all'", specifier = ">=0.12.0" },
|
||||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", 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" },
|
{ 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 = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
|
||||||
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
|
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
|
||||||
{ name = "mypy", specifier = ">=1.14.1" },
|
{ name = "mypy", specifier = ">=1.14.1" },
|
||||||
|
{ name = "orjson", specifier = ">=3.9.3" },
|
||||||
{ name = "pillow", specifier = ">=11.3.0" },
|
{ name = "pillow", specifier = ">=11.3.0" },
|
||||||
{ name = "playwright", specifier = ">=1.57.0" },
|
{ name = "playwright", specifier = ">=1.57.0" },
|
||||||
{ name = "prek", specifier = ">=0.2.22" },
|
{ name = "prek", specifier = ">=0.2.22" },
|
||||||
@@ -1286,6 +1291,7 @@ dev = [
|
|||||||
{ name = "typer", specifier = ">=0.21.1" },
|
{ name = "typer", specifier = ">=0.21.1" },
|
||||||
{ name = "types-orjson", specifier = ">=3.6.2" },
|
{ name = "types-orjson", specifier = ">=3.6.2" },
|
||||||
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
|
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
|
||||||
|
{ name = "ujson", specifier = ">=5.8.0" },
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [
|
||||||
{ name = "black", specifier = ">=25.1.0" },
|
{ name = "black", specifier = ">=25.1.0" },
|
||||||
@@ -1300,15 +1306,19 @@ docs = [
|
|||||||
{ name = "mkdocs-material", specifier = ">=9.7.0" },
|
{ name = "mkdocs-material", specifier = ">=9.7.0" },
|
||||||
{ name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
|
{ name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
|
||||||
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
|
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
|
||||||
|
{ name = "orjson", specifier = ">=3.9.3" },
|
||||||
{ name = "pillow", specifier = ">=11.3.0" },
|
{ name = "pillow", specifier = ">=11.3.0" },
|
||||||
{ name = "python-slugify", specifier = ">=8.0.4" },
|
{ name = "python-slugify", specifier = ">=8.0.4" },
|
||||||
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
|
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
|
||||||
{ name = "ruff", specifier = ">=0.14.14" },
|
{ name = "ruff", specifier = ">=0.14.14" },
|
||||||
{ name = "typer", specifier = ">=0.21.1" },
|
{ name = "typer", specifier = ">=0.21.1" },
|
||||||
|
{ name = "ujson", specifier = ">=5.8.0" },
|
||||||
]
|
]
|
||||||
docs-tests = [
|
docs-tests = [
|
||||||
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" },
|
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" },
|
||||||
|
{ name = "orjson", specifier = ">=3.9.3" },
|
||||||
{ name = "ruff", specifier = ">=0.14.14" },
|
{ name = "ruff", specifier = ">=0.14.14" },
|
||||||
|
{ name = "ujson", specifier = ">=5.8.0" },
|
||||||
]
|
]
|
||||||
github-actions = [
|
github-actions = [
|
||||||
{ name = "httpx", specifier = ">=0.27.0,<1.0.0" },
|
{ name = "httpx", specifier = ">=0.27.0,<1.0.0" },
|
||||||
@@ -1327,6 +1337,7 @@ tests = [
|
|||||||
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" },
|
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" },
|
||||||
{ name = "inline-snapshot", specifier = ">=0.21.1" },
|
{ name = "inline-snapshot", specifier = ">=0.21.1" },
|
||||||
{ name = "mypy", specifier = ">=1.14.1" },
|
{ name = "mypy", specifier = ">=1.14.1" },
|
||||||
|
{ name = "orjson", specifier = ">=3.9.3" },
|
||||||
{ name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" },
|
{ name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" },
|
||||||
{ name = "pyjwt", specifier = ">=2.9.0" },
|
{ name = "pyjwt", specifier = ">=2.9.0" },
|
||||||
{ name = "pytest", specifier = ">=9.0.0" },
|
{ name = "pytest", specifier = ">=9.0.0" },
|
||||||
@@ -1337,6 +1348,7 @@ tests = [
|
|||||||
{ name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" },
|
{ name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" },
|
||||||
{ name = "types-orjson", specifier = ">=3.6.2" },
|
{ name = "types-orjson", specifier = ">=3.6.2" },
|
||||||
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
|
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
|
||||||
|
{ name = "ujson", specifier = ">=5.8.0" },
|
||||||
]
|
]
|
||||||
translations = [
|
translations = [
|
||||||
{ name = "gitpython", specifier = ">=3.1.46" },
|
{ name = "gitpython", specifier = ">=3.1.46" },
|
||||||
@@ -1595,7 +1607,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flask"
|
name = "flask"
|
||||||
version = "3.1.2"
|
version = "3.1.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "blinker" },
|
{ name = "blinker" },
|
||||||
@@ -1605,9 +1617,9 @@ dependencies = [
|
|||||||
{ name = "markupsafe" },
|
{ name = "markupsafe" },
|
||||||
{ name = "werkzeug" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
@@ -1910,41 +1922,37 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" },
|
{ url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "griffe"
|
|
||||||
version = "1.15.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "colorama" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "griffe-typingdoc"
|
name = "griffe-typingdoc"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "griffe" },
|
{ name = "griffelib" },
|
||||||
{ name = "typing-extensions" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
name = "griffe-warnings-deprecated"
|
name = "griffe-warnings-deprecated"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
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 = [
|
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]]
|
||||||
|
name = "griffelib"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2154,26 +2162,23 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "huggingface-hub"
|
name = "huggingface-hub"
|
||||||
version = "0.36.2"
|
version = "1.4.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "filelock" },
|
{ name = "filelock" },
|
||||||
{ name = "fsspec" },
|
{ name = "fsspec" },
|
||||||
{ name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
|
{ name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
|
||||||
|
{ name = "httpx" },
|
||||||
{ name = "packaging" },
|
{ name = "packaging" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "requests" },
|
{ name = "shellingham" },
|
||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
|
{ name = "typer-slim" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c4/fc/eb9bc06130e8bbda6a616e1b80a7aa127681c448d6b49806f61db2670b61/huggingface_hub-1.4.1.tar.gz", hash = "sha256:b41131ec35e631e7383ab26d6146b8d8972abc8b6309b963b306fbcca87f5ed5", size = 642156, upload-time = "2026-02-06T09:20:03.013Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" },
|
{ url = "https://files.pythonhosted.org/packages/d5/ae/2f6d96b4e6c5478d87d606a1934b5d436c4a2bce6bb7c6fdece891c128e3/huggingface_hub-1.4.1-py3-none-any.whl", hash = "sha256:9931d075fb7a79af5abc487106414ec5fba2c0ae86104c0c62fd6cae38873d18", size = 553326, upload-time = "2026-02-06T09:20:00.728Z" },
|
||||||
]
|
|
||||||
|
|
||||||
[package.optional-dependencies]
|
|
||||||
inference = [
|
|
||||||
{ name = "aiohttp" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2991,17 +2996,17 @@ python = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mkdocstrings-python"
|
name = "mkdocstrings-python"
|
||||||
version = "2.0.1"
|
version = "2.0.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "griffe" },
|
{ name = "griffelib" },
|
||||||
{ name = "mkdocs-autorefs" },
|
{ name = "mkdocs-autorefs" },
|
||||||
{ name = "mkdocstrings" },
|
{ name = "mkdocstrings" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
@@ -3925,33 +3930,33 @@ email = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-ai"
|
name = "pydantic-ai"
|
||||||
version = "1.56.0"
|
version = "1.62.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
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"] },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
name = "pydantic-ai-slim"
|
name = "pydantic-ai-slim"
|
||||||
version = "1.56.0"
|
version = "1.62.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
{ name = "genai-prices" },
|
{ name = "genai-prices" },
|
||||||
{ name = "griffe" },
|
{ name = "griffelib" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "opentelemetry-api" },
|
{ name = "opentelemetry-api" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-graph" },
|
{ name = "pydantic-graph" },
|
||||||
{ name = "typing-inspection" },
|
{ 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 = [
|
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]
|
[package.optional-dependencies]
|
||||||
@@ -3987,7 +3992,7 @@ groq = [
|
|||||||
{ name = "groq" },
|
{ name = "groq" },
|
||||||
]
|
]
|
||||||
huggingface = [
|
huggingface = [
|
||||||
{ name = "huggingface-hub", extra = ["inference"] },
|
{ name = "huggingface-hub" },
|
||||||
]
|
]
|
||||||
logfire = [
|
logfire = [
|
||||||
{ name = "logfire", extra = ["httpx"] },
|
{ name = "logfire", extra = ["httpx"] },
|
||||||
@@ -4139,7 +4144,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-evals"
|
name = "pydantic-evals"
|
||||||
version = "1.56.0"
|
version = "1.62.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
@@ -4149,9 +4154,9 @@ dependencies = [
|
|||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/98/f2/8c59284a2978af3fbda45ae3217218eaf8b071207a9290b54b7613983e5d/pydantic_evals-1.56.0.tar.gz", hash = "sha256:206635107127af6a3ee4b1fc8f77af6afb14683615a2d6b3609f79467c1c0d28", size = 47210, upload-time = "2026-02-06T01:13:25.714Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/23/90/080f6722412263395d1d6d066ee90fa8bc2722ce097844220c2d9c946877/pydantic_evals-1.62.0.tar.gz", hash = "sha256:198c4bee936718a4acf6f504056b113e60b34eb49021df8889a394e14c803693", size = 56434, upload-time = "2026-02-19T05:07:11.793Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/51/9875d19ff6d584aaeb574aba76b49d931b822546fc60b29c4fc0da98170d/pydantic_evals-1.56.0-py3-none-any.whl", hash = "sha256:d1efb410c97135aabd2a22453b10c981b2b9851985e9354713af67ae0973b7a9", size = 56407, upload-time = "2026-02-06T01:13:17.098Z" },
|
{ url = "https://files.pythonhosted.org/packages/d8/b9/dc8dba744ec02b16c6fd1abe3fd8ef1b00fd05c72feef5069851b811952f/pydantic_evals-1.62.0-py3-none-any.whl", hash = "sha256:0ca7e10037ed90393c54b6cff41370d6d4bac63f8c878715599c58863c303db1", size = 67341, upload-time = "2026-02-19T05:07:03.83Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4169,7 +4174,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-graph"
|
name = "pydantic-graph"
|
||||||
version = "1.56.0"
|
version = "1.62.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
@@ -4177,9 +4182,9 @@ dependencies = [
|
|||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "typing-inspection" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
@@ -5550,6 +5555,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
|
{ url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typer-slim"
|
||||||
|
version = "0.21.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-doc" },
|
||||||
|
{ name = "click" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a5/ca/0d9d822fd8a4c7e830cba36a2557b070d4b4a9558a0460377a61f8fb315d/typer_slim-0.21.2.tar.gz", hash = "sha256:78f20d793036a62aaf9c3798306142b08261d4b2a941c6e463081239f062a2f9", size = 120497, upload-time = "2026-02-10T19:33:45.836Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/03/e09325cfc40a33a82b31ba1a3f1d97e85246736856a45a43b19fcb48b1c2/typer_slim-0.21.2-py3-none-any.whl", hash = "sha256:4705082bb6c66c090f60e47c8be09a93158c139ce0aa98df7c6c47e723395e5f", size = 56790, upload-time = "2026-02-10T19:33:47.221Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-orjson"
|
name = "types-orjson"
|
||||||
version = "3.6.2"
|
version = "3.6.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user