mirror of
https://github.com/fastapi/fastapi.git
synced 2026-02-23 10:25:02 -05:00
Compare commits
1 Commits
0.131.0
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e64e9457cb |
2
.github/labeler.yml
vendored
2
.github/labeler.yml
vendored
@@ -29,6 +29,8 @@ internal:
|
||||
- scripts/**
|
||||
- .gitignore
|
||||
- .pre-commit-config.yaml
|
||||
- pdm_build.py
|
||||
- requirements*.txt
|
||||
- uv.lock
|
||||
- docs/en/data/sponsors.yml
|
||||
- docs/en/overrides/main.html
|
||||
|
||||
10
.github/workflows/publish.yml
vendored
10
.github/workflows/publish.yml
vendored
@@ -8,6 +8,11 @@ on:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
package:
|
||||
- fastapi
|
||||
- fastapi-slim
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
@@ -21,9 +26,14 @@ jobs:
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
# Issue ref: https://github.com/actions/setup-python/issues/436
|
||||
# cache: "pip"
|
||||
# cache-dependency-path: pyproject.toml
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Build distribution
|
||||
run: uv build
|
||||
env:
|
||||
TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }}
|
||||
- name: Publish
|
||||
run: uv publish
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Custom Response - HTML, Stream, File, others { #custom-response-html-stream-file-others }
|
||||
|
||||
By default, **FastAPI** will return JSON responses.
|
||||
By default, **FastAPI** will return the responses using `JSONResponse`.
|
||||
|
||||
You can override it by returning a `Response` directly as seen in [Return a Response directly](response-directly.md){.internal-link target=_blank}.
|
||||
|
||||
@@ -10,27 +10,43 @@ But you can also declare the `Response` that you want to be used (e.g. any `Resp
|
||||
|
||||
The contents that you return from your *path operation function* will be put inside of that `Response`.
|
||||
|
||||
And if that `Response` has a JSON media type (`application/json`), like is the case with the `JSONResponse` and `UJSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*.
|
||||
|
||||
/// note
|
||||
|
||||
If you use a response class with no media type, FastAPI will expect your response to have no content, so it will not document the response format in its generated OpenAPI docs.
|
||||
|
||||
///
|
||||
|
||||
## JSON Responses { #json-responses }
|
||||
## Use `ORJSONResponse` { #use-orjsonresponse }
|
||||
|
||||
By default FastAPI returns JSON responses.
|
||||
For example, if you are squeezing performance, you can install and use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> and set the response to be `ORJSONResponse`.
|
||||
|
||||
If you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic.
|
||||
Import the `Response` class (sub-class) you want to use and declare it in the *path operation decorator*.
|
||||
|
||||
If you don't declare a response model, FastAPI will use the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} and put it in a `JSONResponse`.
|
||||
For large responses, returning a `Response` directly is much faster than returning a dictionary.
|
||||
|
||||
If you declare a `response_class` with a JSON media type (`application/json`), like is the case with the `JSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*. But the data won't be serialized to JSON bytes with Pydantic, instead it will be converted with the `jsonable_encoder` and then passed to the `JSONResponse` class, which will serialize it to bytes using the standard JSON library in Python.
|
||||
This is because by default, FastAPI will inspect every item inside and make sure it is serializable as JSON, using the same [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} explained in the tutorial. This is what allows you to return **arbitrary objects**, for example database models.
|
||||
|
||||
### JSON Performance { #json-performance }
|
||||
But if you are certain that the content that you are returning is **serializable with JSON**, you can pass it directly to the response class and avoid the extra overhead that FastAPI would have by passing your return content through the `jsonable_encoder` before passing it to the response class.
|
||||
|
||||
In short, if you want the maximum performance, use a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} and don't declare a `response_class` in the *path operation decorator*.
|
||||
{* ../../docs_src/custom_response/tutorial001b_py310.py hl[2,7] *}
|
||||
|
||||
{* ../../docs_src/response_model/tutorial001_01_py310.py ln[15:17] hl[16] *}
|
||||
/// info
|
||||
|
||||
The parameter `response_class` will also be used to define the "media type" of the response.
|
||||
|
||||
In this case, the HTTP header `Content-Type` will be set to `application/json`.
|
||||
|
||||
And it will be documented as such in OpenAPI.
|
||||
|
||||
///
|
||||
|
||||
/// tip
|
||||
|
||||
The `ORJSONResponse` is only available in FastAPI, not in Starlette.
|
||||
|
||||
///
|
||||
|
||||
## HTML Response { #html-response }
|
||||
|
||||
@@ -138,6 +154,40 @@ Takes some data and returns an `application/json` encoded response.
|
||||
|
||||
This is the default response used in **FastAPI**, as you read above.
|
||||
|
||||
### `ORJSONResponse` { #orjsonresponse }
|
||||
|
||||
A fast alternative JSON response using <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>, as you read above.
|
||||
|
||||
/// info
|
||||
|
||||
This requires installing `orjson` for example with `pip install orjson`.
|
||||
|
||||
///
|
||||
|
||||
### `UJSONResponse` { #ujsonresponse }
|
||||
|
||||
An alternative JSON response using <a href="https://github.com/ultrajson/ultrajson" class="external-link" target="_blank">`ujson`</a>.
|
||||
|
||||
/// info
|
||||
|
||||
This requires installing `ujson` for example with `pip install ujson`.
|
||||
|
||||
///
|
||||
|
||||
/// warning
|
||||
|
||||
`ujson` is less careful than Python's built-in implementation in how it handles some edge-cases.
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial001_py310.py hl[2,7] *}
|
||||
|
||||
/// tip
|
||||
|
||||
It's possible that `ORJSONResponse` might be a faster alternative.
|
||||
|
||||
///
|
||||
|
||||
### `RedirectResponse` { #redirectresponse }
|
||||
|
||||
Returns an HTTP redirect. Uses a 307 status code (Temporary Redirect) by default.
|
||||
@@ -218,7 +268,7 @@ In this case, you can return the file path directly from your *path operation* f
|
||||
|
||||
You can create your own custom response class, inheriting from `Response` and using it.
|
||||
|
||||
For example, let's say that you want to use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> with some settings.
|
||||
For example, let's say that you want to use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>, but with some custom settings not used in the included `ORJSONResponse` class.
|
||||
|
||||
Let's say you want it to return indented and formatted JSON, so you want to use the orjson option `orjson.OPT_INDENT_2`.
|
||||
|
||||
@@ -242,21 +292,13 @@ Now instead of returning:
|
||||
|
||||
Of course, you will probably find much better ways to take advantage of this than formatting JSON. 😉
|
||||
|
||||
### `orjson` or Response Model { #orjson-or-response-model }
|
||||
|
||||
If what you are looking for is performance, you are probably better off using a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} than an `orjson` response.
|
||||
|
||||
With a response model, FastAPI will use Pydantic to serialize the data to JSON, without using intermediate steps, like converting it with `jsonable_encoder`, which would happen in any other case.
|
||||
|
||||
And under the hood, Pydantic uses the same underlying Rust mechanisms as `orjson` to serialize to JSON, so you will already get the best performance with a response model.
|
||||
|
||||
## Default response class { #default-response-class }
|
||||
|
||||
When creating a **FastAPI** class instance or an `APIRouter` you can specify which response class to use by default.
|
||||
|
||||
The parameter that defines this is `default_response_class`.
|
||||
|
||||
In the example below, **FastAPI** will use `HTMLResponse` by default, in all *path operations*, instead of JSON.
|
||||
In the example below, **FastAPI** will use `ORJSONResponse` by default, in all *path operations*, instead of `JSONResponse`.
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial010_py310.py hl[2,4] *}
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
# JSON with Bytes as Base64 { #json-with-bytes-as-base64 }
|
||||
|
||||
If your app needs to receive and send JSON data, but you need to include binary data in it, you can encode it as base64.
|
||||
|
||||
## Base64 vs Files { #base64-vs-files }
|
||||
|
||||
Consider first if you can use [Request Files](../tutorial/request-files.md){.internal-link target=_blank} for uploading binary data and [Custom Response - FileResponse](./custom-response.md#fileresponse--fileresponse-){.internal-link target=_blank} for sending binary data, instead of encoding it in JSON.
|
||||
|
||||
JSON can only contain UTF-8 encoded strings, so it can't contain raw bytes.
|
||||
|
||||
Base64 can encode binary data in strings, but to do it, it needs to use more characters than the original binary data, so it would normally be less efficient than regular files.
|
||||
|
||||
Use base64 only if you definitely need to include binary data in JSON, and you can't use files for that.
|
||||
|
||||
## Pydantic `bytes` { #pydantic-bytes }
|
||||
|
||||
You can declare a Pydantic model with `bytes` fields, and then use `val_json_bytes` in the model config to tell it to use base64 to *validate* input JSON data, as part of that validation it will decode the base64 string into bytes.
|
||||
|
||||
{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:9,29:35] hl[9] *}
|
||||
|
||||
If you check the `/docs`, they will show that the field `data` expects base64 encoded bytes:
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/json-base64-bytes/image01.png">
|
||||
</div>
|
||||
|
||||
You could send a request like:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Some data",
|
||||
"data": "aGVsbG8="
|
||||
}
|
||||
```
|
||||
|
||||
/// tip
|
||||
|
||||
`aGVsbG8=` is the base64 encoding of `hello`.
|
||||
|
||||
///
|
||||
|
||||
And then Pydantic will decode the base64 string and give you the original bytes in the `data` field of the model.
|
||||
|
||||
You will receive a response like:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Some data",
|
||||
"content": "hello"
|
||||
}
|
||||
```
|
||||
|
||||
## Pydantic `bytes` for Output Data { #pydantic-bytes-for-output-data }
|
||||
|
||||
You can also use `bytes` fields with `ser_json_bytes` in the model config for output data, and Pydantic will *serialize* the bytes as base64 when generating the JSON response.
|
||||
|
||||
{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:2,12:16,29,38:41] hl[16] *}
|
||||
|
||||
## Pydantic `bytes` for Input and Output Data { #pydantic-bytes-for-input-and-output-data }
|
||||
|
||||
And of course, you can use the same model configured to use base64 to handle both input (*validate*) with `val_json_bytes` and output (*serialize*) with `ser_json_bytes` when receiving and sending JSON data.
|
||||
|
||||
{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:2,19:26,29,44:46] hl[23:26] *}
|
||||
@@ -2,23 +2,19 @@
|
||||
|
||||
When you create a **FastAPI** *path operation* you can normally return any data from it: a `dict`, a `list`, a Pydantic model, a database model, etc.
|
||||
|
||||
If you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic.
|
||||
By default, **FastAPI** would automatically convert that return value to JSON using the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}.
|
||||
|
||||
If you don't declare a response model, FastAPI will use the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} and put it in a `JSONResponse`.
|
||||
Then, behind the scenes, it would put that JSON-compatible data (e.g. a `dict`) inside of a `JSONResponse` that would be used to send the response to the client.
|
||||
|
||||
You could also create a `JSONResponse` directly and return it.
|
||||
But you can return a `JSONResponse` directly from your *path operations*.
|
||||
|
||||
/// tip
|
||||
|
||||
You will normally have much better performance using a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} than returning a `JSONResponse` directly, as that way it serializes the data using Pydantic, in Rust.
|
||||
|
||||
///
|
||||
It might be useful, for example, to return custom headers or cookies.
|
||||
|
||||
## Return a `Response` { #return-a-response }
|
||||
|
||||
You can return any `Response` or any sub-class of it.
|
||||
In fact, you can return any `Response` or any sub-class of it.
|
||||
|
||||
/// info
|
||||
/// tip
|
||||
|
||||
`JSONResponse` itself is a sub-class of `Response`.
|
||||
|
||||
@@ -60,18 +56,6 @@ You could put your XML content in a string, put that in a `Response`, and return
|
||||
|
||||
{* ../../docs_src/response_directly/tutorial002_py310.py hl[1,18] *}
|
||||
|
||||
## How a Response Model Works { #how-a-response-model-works }
|
||||
|
||||
When you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} in a path operation, **FastAPI** will use it to serialize the data to JSON, using Pydantic.
|
||||
|
||||
{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *}
|
||||
|
||||
As that will happen on the Rust side, the performance will be much better than if it was done with regular Python and the `JSONResponse` class.
|
||||
|
||||
When using a response model FastAPI won't use the `jsonable_encoder` to convert the data (which would be slower) nor the `JSONResponse` class.
|
||||
|
||||
Instead it takes the JSON bytes generated with Pydantic using the response model and returns a `Response` with the right media type for JSON directly (`application/json`).
|
||||
|
||||
## Notes { #notes }
|
||||
|
||||
When you return a `Response` directly its data is not validated, converted (serialized), or documented automatically.
|
||||
|
||||
@@ -6,10 +6,6 @@ Here are several pointers to other places in the docs, for general or frequent q
|
||||
|
||||
To ensure that you don't return more data than you should, read the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}.
|
||||
|
||||
## Optimize Response Performance - Response Model - Return Type { #optimize-response-performance-response-model-return-type }
|
||||
|
||||
To optimize performance when returning JSON data, use a return type or response model, that way Pydantic will handle the serialization to JSON on the Rust side, without going through Python. Read more in the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}.
|
||||
|
||||
## Documentation Tags - OpenAPI { #documentation-tags-openapi }
|
||||
|
||||
To add tags to your *path operations*, and group them in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 71 KiB |
@@ -22,13 +22,7 @@ from fastapi.responses import (
|
||||
|
||||
## FastAPI Responses
|
||||
|
||||
There were a couple of custom FastAPI response classes that were intended to optimize JSON performance.
|
||||
|
||||
However, they are now deprecated as you will now get better performance by using a [Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/).
|
||||
|
||||
That way, Pydantic will serialize the data into JSON bytes on the Rust side, which will achieve better performance than these custom JSON responses.
|
||||
|
||||
Read more about it in [Custom Response - HTML, Stream, File, others - `orjson` or Response Model](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model).
|
||||
There are a couple of custom FastAPI response classes, you can use them to optimize JSON performance.
|
||||
|
||||
::: fastapi.responses.UJSONResponse
|
||||
options:
|
||||
|
||||
@@ -7,35 +7,6 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.131.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* 🗑️ Deprecate `ORJSONResponse` and `UJSONResponse`. PR [#14964](https://github.com/fastapi/fastapi/pull/14964) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.130.0
|
||||
|
||||
### Features
|
||||
|
||||
* ✨ Serialize JSON response with Pydantic (in Rust), when there's a Pydantic return type or response model. PR [#14962](https://github.com/fastapi/fastapi/pull/14962) by [@tiangolo](https://github.com/tiangolo).
|
||||
* This results in 2x (or more) performance increase for JSON responses.
|
||||
* New docs: [Custom Response - JSON Performance](https://fastapi.tiangolo.com/advanced/custom-response/#json-performance).
|
||||
|
||||
## 0.129.2
|
||||
|
||||
### Internal
|
||||
|
||||
* ⬆️ Upgrade pytest. PR [#14959](https://github.com/fastapi/fastapi/pull/14959) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Fix CI, do not attempt to publish `fastapi-slim`. PR [#14958](https://github.com/fastapi/fastapi/pull/14958) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ➖ Drop support for `fastapi-slim`, no more versions will be released, use only `"fastapi[standard]"` or `fastapi`. PR [#14957](https://github.com/fastapi/fastapi/pull/14957) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🔧 Update pyproject.toml, remove unneeded lines. PR [#14956](https://github.com/fastapi/fastapi/pull/14956) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.129.1
|
||||
|
||||
### Fixes
|
||||
|
||||
* ♻️ Fix JSON Schema for bytes, use `"contentMediaType": "application/octet-stream"` instead of `"format": "binary"`. PR [#14953](https://github.com/fastapi/fastapi/pull/14953) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Docs
|
||||
|
||||
* 🔨 Add Kapa.ai widget (AI chatbot). PR [#14938](https://github.com/fastapi/fastapi/pull/14938) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
@@ -13,7 +13,6 @@ FastAPI will use this return type to:
|
||||
* Add a **JSON Schema** for the response, in the OpenAPI *path operation*.
|
||||
* This will be used by the **automatic docs**.
|
||||
* It will also be used by automatic client code generation tools.
|
||||
* **Serialize** the returned data to JSON using Pydantic, which is written in **Rust**, so it will be **much faster**.
|
||||
|
||||
But most importantly:
|
||||
|
||||
|
||||
@@ -192,7 +192,6 @@ nav:
|
||||
- advanced/wsgi.md
|
||||
- advanced/generate-clients.md
|
||||
- advanced/advanced-python-types.md
|
||||
- advanced/json-base64-bytes.md
|
||||
- fastapi-cli.md
|
||||
- Deployment:
|
||||
- deployment/index.md
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import ORJSONResponse
|
||||
|
||||
app = FastAPI(default_response_class=HTMLResponse)
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items():
|
||||
return "<h1>Items</h1><p>This is a list of items.</p>"
|
||||
return [{"item_id": "Foo"}]
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class DataInput(BaseModel):
|
||||
description: str
|
||||
data: bytes
|
||||
|
||||
model_config = {"val_json_bytes": "base64"}
|
||||
|
||||
|
||||
class DataOutput(BaseModel):
|
||||
description: str
|
||||
data: bytes
|
||||
|
||||
model_config = {"ser_json_bytes": "base64"}
|
||||
|
||||
|
||||
class DataInputOutput(BaseModel):
|
||||
description: str
|
||||
data: bytes
|
||||
|
||||
model_config = {
|
||||
"val_json_bytes": "base64",
|
||||
"ser_json_bytes": "base64",
|
||||
}
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/data")
|
||||
def post_data(body: DataInput):
|
||||
content = body.data.decode("utf-8")
|
||||
return {"description": body.description, "content": content}
|
||||
|
||||
|
||||
@app.get("/data")
|
||||
def get_data() -> DataOutput:
|
||||
data = "hello".encode("utf-8")
|
||||
return DataOutput(description="A plumbus", data=data)
|
||||
|
||||
|
||||
@app.post("/data-in-out")
|
||||
def post_data_in_out(body: DataInputOutput) -> DataInputOutput:
|
||||
return body
|
||||
2
docs_src/python_types/tutorial005_py39.py
Normal file
2
docs_src/python_types/tutorial005_py39.py
Normal file
@@ -0,0 +1,2 @@
|
||||
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
|
||||
return item_a, item_b, item_c, item_d, item_e
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.131.0"
|
||||
__version__ = "0.129.0"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-
|
||||
)
|
||||
from pydantic._internal._typing_extra import eval_type_lenient
|
||||
from pydantic.fields import FieldInfo as FieldInfo
|
||||
from pydantic.json_schema import GenerateJsonSchema as _GenerateJsonSchema
|
||||
from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
|
||||
from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
|
||||
from pydantic_core import CoreSchema as CoreSchema
|
||||
from pydantic_core import PydanticUndefined
|
||||
@@ -40,23 +40,6 @@ RequiredParam = PydanticUndefined
|
||||
Undefined = PydanticUndefined
|
||||
evaluate_forwardref = eval_type_lenient
|
||||
|
||||
|
||||
class GenerateJsonSchema(_GenerateJsonSchema):
|
||||
# TODO: remove when this is merged (or equivalent): https://github.com/pydantic/pydantic/pull/12841
|
||||
# and dropping support for any version of Pydantic before that one (so, in a very long time)
|
||||
def bytes_schema(self, schema: CoreSchema) -> JsonSchemaValue:
|
||||
json_schema = {"type": "string", "contentMediaType": "application/octet-stream"}
|
||||
bytes_mode = (
|
||||
self._config.ser_json_bytes
|
||||
if self.mode == "serialization"
|
||||
else self._config.val_json_bytes
|
||||
)
|
||||
if bytes_mode == "base64":
|
||||
json_schema["contentEncoding"] = "base64"
|
||||
self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes)
|
||||
return json_schema
|
||||
|
||||
|
||||
# TODO: remove when dropping support for Pydantic < v2.12.3
|
||||
_Attrs = {
|
||||
"default": ...,
|
||||
@@ -199,32 +182,6 @@ class ModelField:
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
def serialize_json(
|
||||
self,
|
||||
value: Any,
|
||||
*,
|
||||
include: IncEx | None = None,
|
||||
exclude: IncEx | None = None,
|
||||
by_alias: bool = True,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
exclude_none: bool = False,
|
||||
) -> bytes:
|
||||
# What calls this code passes a value that already called
|
||||
# self._type_adapter.validate_python(value)
|
||||
# This uses Pydantic's dump_json() which serializes directly to JSON
|
||||
# bytes in one pass (via Rust), avoiding the intermediate Python dict
|
||||
# step of dump_python(mode="json") + json.dumps().
|
||||
return self._type_adapter.dump_json(
|
||||
value,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
# Each ModelField is unique for our purposes, to allow making a dict from
|
||||
# ModelField to its JSON Schema.
|
||||
|
||||
@@ -139,7 +139,7 @@ class UploadFile(StarletteUploadFile):
|
||||
def __get_pydantic_json_schema__(
|
||||
cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler
|
||||
) -> dict[str, Any]:
|
||||
return {"type": "string", "contentMediaType": "application/octet-stream"}
|
||||
return {"type": "string", "format": "binary"}
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from starlette.responses import FileResponse as FileResponse # noqa
|
||||
from starlette.responses import HTMLResponse as HTMLResponse # noqa
|
||||
from starlette.responses import JSONResponse as JSONResponse # noqa
|
||||
@@ -8,7 +7,6 @@ from starlette.responses import PlainTextResponse as PlainTextResponse # noqa
|
||||
from starlette.responses import RedirectResponse as RedirectResponse # noqa
|
||||
from starlette.responses import Response as Response # noqa
|
||||
from starlette.responses import StreamingResponse as StreamingResponse # noqa
|
||||
from typing_extensions import deprecated
|
||||
|
||||
try:
|
||||
import ujson
|
||||
@@ -22,29 +20,12 @@ except ImportError: # pragma: nocover
|
||||
orjson = None # type: ignore
|
||||
|
||||
|
||||
@deprecated(
|
||||
"UJSONResponse is deprecated, FastAPI now serializes data directly to JSON "
|
||||
"bytes via Pydantic when a return type or response model is set, which is "
|
||||
"faster and doesn't need a custom response class. Read more in the FastAPI "
|
||||
"docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model "
|
||||
"and https://fastapi.tiangolo.com/tutorial/response-model/",
|
||||
category=FastAPIDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
class UJSONResponse(JSONResponse):
|
||||
"""JSON response using the ujson library to serialize data to JSON.
|
||||
"""
|
||||
JSON response using the high-performance ujson library to serialize data to JSON.
|
||||
|
||||
**Deprecated**: `UJSONResponse` is deprecated. FastAPI now serializes data
|
||||
directly to JSON bytes via Pydantic when a return type or response model is
|
||||
set, which is faster and doesn't need a custom response class.
|
||||
|
||||
Read more in the
|
||||
[FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model)
|
||||
and the
|
||||
[FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/).
|
||||
|
||||
**Note**: `ujson` is not included with FastAPI and must be installed
|
||||
separately, e.g. `pip install ujson`.
|
||||
Read more about it in the
|
||||
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
|
||||
"""
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
@@ -52,29 +33,12 @@ class UJSONResponse(JSONResponse):
|
||||
return ujson.dumps(content, ensure_ascii=False).encode("utf-8")
|
||||
|
||||
|
||||
@deprecated(
|
||||
"ORJSONResponse is deprecated, FastAPI now serializes data directly to JSON "
|
||||
"bytes via Pydantic when a return type or response model is set, which is "
|
||||
"faster and doesn't need a custom response class. Read more in the FastAPI "
|
||||
"docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model "
|
||||
"and https://fastapi.tiangolo.com/tutorial/response-model/",
|
||||
category=FastAPIDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
class ORJSONResponse(JSONResponse):
|
||||
"""JSON response using the orjson library to serialize data to JSON.
|
||||
"""
|
||||
JSON response using the high-performance orjson library to serialize data to JSON.
|
||||
|
||||
**Deprecated**: `ORJSONResponse` is deprecated. FastAPI now serializes data
|
||||
directly to JSON bytes via Pydantic when a return type or response model is
|
||||
set, which is faster and doesn't need a custom response class.
|
||||
|
||||
Read more in the
|
||||
[FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model)
|
||||
and the
|
||||
[FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/).
|
||||
|
||||
**Note**: `orjson` is not included with FastAPI and must be installed
|
||||
separately, e.g. `pip install orjson`.
|
||||
Read more about it in the
|
||||
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
|
||||
"""
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
|
||||
@@ -271,7 +271,6 @@ async def serialize_response(
|
||||
exclude_none: bool = False,
|
||||
is_coroutine: bool = True,
|
||||
endpoint_ctx: EndpointContext | None = None,
|
||||
dump_json: bool = False,
|
||||
) -> Any:
|
||||
if field:
|
||||
if is_coroutine:
|
||||
@@ -287,8 +286,8 @@ async def serialize_response(
|
||||
body=response_content,
|
||||
endpoint_ctx=ctx,
|
||||
)
|
||||
serializer = field.serialize_json if dump_json else field.serialize
|
||||
return serializer(
|
||||
|
||||
return field.serialize(
|
||||
value,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
@@ -444,14 +443,6 @@ def get_request_handler(
|
||||
response_args["status_code"] = current_status_code
|
||||
if solved_result.response.status_code:
|
||||
response_args["status_code"] = solved_result.response.status_code
|
||||
# Use the fast path (dump_json) when no custom response
|
||||
# class was set and a response field with a TypeAdapter
|
||||
# exists. Serializes directly to JSON bytes via Pydantic's
|
||||
# Rust core, skipping the intermediate Python dict +
|
||||
# json.dumps() step.
|
||||
use_dump_json = response_field is not None and isinstance(
|
||||
response_class, DefaultPlaceholder
|
||||
)
|
||||
content = await serialize_response(
|
||||
field=response_field,
|
||||
response_content=raw_response,
|
||||
@@ -463,16 +454,8 @@ def get_request_handler(
|
||||
exclude_none=response_model_exclude_none,
|
||||
is_coroutine=is_coroutine,
|
||||
endpoint_ctx=endpoint_ctx,
|
||||
dump_json=use_dump_json,
|
||||
)
|
||||
if use_dump_json:
|
||||
response = Response(
|
||||
content=content,
|
||||
media_type="application/json",
|
||||
**response_args,
|
||||
)
|
||||
else:
|
||||
response = actual_response_class(content, **response_args)
|
||||
response = actual_response_class(content, **response_args)
|
||||
if not is_body_allowed_for_status_code(response.status_code):
|
||||
response.body = b""
|
||||
response.headers.raw.extend(solved_result.response.headers.raw)
|
||||
|
||||
40
pdm_build.py
Normal file
40
pdm_build.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from pdm.backend.hooks import Context
|
||||
|
||||
TIANGOLO_BUILD_PACKAGE = os.getenv("TIANGOLO_BUILD_PACKAGE")
|
||||
|
||||
|
||||
def pdm_build_initialize(context: Context) -> None:
|
||||
metadata = context.config.metadata
|
||||
# Get main version
|
||||
version = metadata["version"]
|
||||
# Get custom config for the current package, from the env var
|
||||
all_configs_config: dict[str, Any] = context.config.data["tool"]["tiangolo"][
|
||||
"_internal-slim-build"
|
||||
]["packages"]
|
||||
|
||||
if TIANGOLO_BUILD_PACKAGE not in all_configs_config:
|
||||
return
|
||||
|
||||
config = all_configs_config[TIANGOLO_BUILD_PACKAGE]
|
||||
project_config: dict[str, Any] = config["project"]
|
||||
# Override main [project] configs with custom configs for this package
|
||||
for key, value in project_config.items():
|
||||
metadata[key] = value
|
||||
# Get custom build config for the current package
|
||||
build_config: dict[str, Any] = (
|
||||
config.get("tool", {}).get("pdm", {}).get("build", {})
|
||||
)
|
||||
# Override PDM build config with custom build config for this package
|
||||
for key, value in build_config.items():
|
||||
context.config.build_config[key] = value
|
||||
# Get main dependencies
|
||||
dependencies: list[str] = metadata.get("dependencies", [])
|
||||
# Sync versions in dependencies
|
||||
new_dependencies = []
|
||||
for dep in dependencies:
|
||||
new_dep = f"{dep}>={version}"
|
||||
new_dependencies.append(new_dep)
|
||||
metadata["dependencies"] = new_dependencies
|
||||
250
pyproject.toml
250
pyproject.toml
@@ -57,6 +57,7 @@ Issues = "https://github.com/fastapi/fastapi/issues"
|
||||
Changelog = "https://fastapi.tiangolo.com/release-notes/"
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
standard = [
|
||||
"fastapi-cli[standard] >=0.0.8",
|
||||
# For the test client
|
||||
@@ -105,6 +106,10 @@ all = [
|
||||
"itsdangerous >=1.1.0",
|
||||
# For Starlette's schema generation, would not be used with FastAPI
|
||||
"pyyaml >=5.3.1",
|
||||
# For UJSONResponse
|
||||
"ujson >=5.8.0",
|
||||
# For ORJSONResponse
|
||||
"orjson >=3.9.3",
|
||||
# To validate email fields
|
||||
"email-validator >=2.0.0",
|
||||
# Uvicorn with uvloop
|
||||
@@ -147,10 +152,6 @@ docs = [
|
||||
docs-tests = [
|
||||
"httpx >=0.23.0,<1.0.0",
|
||||
"ruff >=0.14.14",
|
||||
# For UJSONResponse
|
||||
"ujson >=5.8.0",
|
||||
# For ORJSONResponse
|
||||
"orjson >=3.9.3",
|
||||
]
|
||||
github-actions = [
|
||||
"httpx >=0.27.0,<1.0.0",
|
||||
@@ -170,7 +171,7 @@ tests = [
|
||||
"mypy >=1.14.1",
|
||||
"pwdlib[argon2] >=0.2.1",
|
||||
"pyjwt >=2.9.0",
|
||||
"pytest >=9.0.0",
|
||||
"pytest >=7.1.3,<9.0.0",
|
||||
"pytest-codspeed >=4.2.0",
|
||||
"pyyaml >=5.3.1,<7.0.0",
|
||||
"sqlmodel >=0.0.31",
|
||||
@@ -198,6 +199,32 @@ source-includes = [
|
||||
"docs/en/docs/img/favicon.png",
|
||||
]
|
||||
|
||||
[tool.tiangolo._internal-slim-build.packages.fastapi-slim.project]
|
||||
name = "fastapi-slim"
|
||||
readme = "fastapi-slim/README.md"
|
||||
dependencies = [
|
||||
"fastapi",
|
||||
]
|
||||
optional-dependencies = {}
|
||||
scripts = {}
|
||||
|
||||
[tool.tiangolo._internal-slim-build.packages.fastapi-slim.tool.pdm.build]
|
||||
# excludes needs to explicitly exclude the top level python packages,
|
||||
# otherwise PDM includes them by default
|
||||
# A "*" glob pattern can't be used here because in PDM internals, the patterns are put
|
||||
# in a set (unordered, order varies) and each excluded file is assigned one of the
|
||||
# glob patterns that matches, as the set is unordered, the matched pattern could be "*"
|
||||
# independent of the order here. And then the internal code would give it a lower score
|
||||
# than the one for a default included file.
|
||||
# By not using "*" and explicitly excluding the top level packages, they get a higher
|
||||
# score than the default inclusion
|
||||
excludes = ["fastapi", "tests", "pdm_build.py"]
|
||||
# source-includes needs to explicitly define some value because PDM will check the
|
||||
# truthy value of the list, and if empty, will include some defaults, including "tests",
|
||||
# an empty string doesn't match anything, but makes the list truthy, so that PDM
|
||||
# doesn't override it during the build.
|
||||
source-includes = [""]
|
||||
|
||||
[tool.mypy]
|
||||
plugins = ["pydantic.mypy"]
|
||||
strict = true
|
||||
@@ -218,16 +245,25 @@ disallow_incomplete_defs = false
|
||||
disallow_untyped_defs = false
|
||||
disallow_untyped_calls = false
|
||||
|
||||
[tool.pytest]
|
||||
minversion = "9.0"
|
||||
[tool.pytest.ini_options]
|
||||
addopts = [
|
||||
"--strict-config",
|
||||
"--strict-markers",
|
||||
"--ignore=docs_src",
|
||||
]
|
||||
strict_xfail = true
|
||||
xfail_strict = true
|
||||
junit_family = "xunit2"
|
||||
filterwarnings = [
|
||||
"error",
|
||||
# see https://trio.readthedocs.io/en/stable/history.html#trio-0-22-0-2022-09-28
|
||||
"ignore:You seem to already have a custom.*:RuntimeWarning:trio",
|
||||
# TODO: remove after upgrading SQLAlchemy to a version that includes the following changes
|
||||
# https://github.com/sqlalchemy/sqlalchemy/commit/59521abcc0676e936b31a523bd968fc157fef0c2
|
||||
'ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning:sqlalchemy',
|
||||
# Trio 24.1.0 raises a warning from attrs
|
||||
# Ref: https://github.com/python-trio/trio/pull/3054
|
||||
# Remove once there's a new version of Trio
|
||||
'ignore:The `hash` argument is deprecated*:DeprecationWarning:trio',
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
@@ -244,6 +280,7 @@ dynamic_context = "test_function"
|
||||
omit = [
|
||||
"docs_src/response_model/tutorial003_04_py39.py",
|
||||
"docs_src/response_model/tutorial003_04_py310.py",
|
||||
"docs_src/dependencies/tutorial008_an_py39.py", # difficult to mock
|
||||
"docs_src/dependencies/tutorial013_an_py310.py", # temporary code example?
|
||||
"docs_src/dependencies/tutorial014_an_py310.py", # temporary code example?
|
||||
# Pydantic v1 migration, no longer tested
|
||||
@@ -251,6 +288,202 @@ omit = [
|
||||
"docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py",
|
||||
"docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py",
|
||||
"docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py",
|
||||
# TODO: remove all the ignores below when all translations use the new Python 3.10 files
|
||||
"docs_src/additional_responses/tutorial001_py39.py",
|
||||
"docs_src/additional_responses/tutorial003_py39.py",
|
||||
"docs_src/advanced_middleware/tutorial001_py39.py",
|
||||
"docs_src/advanced_middleware/tutorial002_py39.py",
|
||||
"docs_src/advanced_middleware/tutorial003_py39.py",
|
||||
"docs_src/app_testing/app_a_py39/main.py",
|
||||
"docs_src/app_testing/app_a_py39/test_main.py",
|
||||
"docs_src/app_testing/tutorial001_py39.py",
|
||||
"docs_src/app_testing/tutorial002_py39.py",
|
||||
"docs_src/app_testing/tutorial003_py39.py",
|
||||
"docs_src/app_testing/tutorial004_py39.py",
|
||||
"docs_src/async_tests/app_a_py39/main.py",
|
||||
"docs_src/async_tests/app_a_py39/test_main.py",
|
||||
"docs_src/authentication_error_status_code/tutorial001_an_py39.py",
|
||||
"docs_src/background_tasks/tutorial001_py39.py",
|
||||
"docs_src/behind_a_proxy/tutorial001_01_py39.py",
|
||||
"docs_src/behind_a_proxy/tutorial001_py39.py",
|
||||
"docs_src/behind_a_proxy/tutorial002_py39.py",
|
||||
"docs_src/behind_a_proxy/tutorial003_py39.py",
|
||||
"docs_src/behind_a_proxy/tutorial004_py39.py",
|
||||
"docs_src/bigger_applications/app_an_py39/dependencies.py",
|
||||
"docs_src/bigger_applications/app_an_py39/internal/admin.py",
|
||||
"docs_src/bigger_applications/app_an_py39/main.py",
|
||||
"docs_src/bigger_applications/app_an_py39/routers/items.py",
|
||||
"docs_src/bigger_applications/app_an_py39/routers/users.py",
|
||||
"docs_src/bigger_applications/app_py39/dependencies.py",
|
||||
"docs_src/bigger_applications/app_py39/main.py",
|
||||
"docs_src/body_nested_models/tutorial008_py39.py",
|
||||
"docs_src/body_nested_models/tutorial009_py39.py",
|
||||
"docs_src/conditional_openapi/tutorial001_py39.py",
|
||||
"docs_src/configure_swagger_ui/tutorial001_py39.py",
|
||||
"docs_src/configure_swagger_ui/tutorial002_py39.py",
|
||||
"docs_src/configure_swagger_ui/tutorial003_py39.py",
|
||||
"docs_src/cors/tutorial001_py39.py",
|
||||
"docs_src/custom_docs_ui/tutorial001_py39.py",
|
||||
"docs_src/custom_docs_ui/tutorial002_py39.py",
|
||||
"docs_src/custom_response/tutorial001_py39.py",
|
||||
"docs_src/custom_response/tutorial001b_py39.py",
|
||||
"docs_src/custom_response/tutorial002_py39.py",
|
||||
"docs_src/custom_response/tutorial003_py39.py",
|
||||
"docs_src/custom_response/tutorial004_py39.py",
|
||||
"docs_src/custom_response/tutorial005_py39.py",
|
||||
"docs_src/custom_response/tutorial006_py39.py",
|
||||
"docs_src/custom_response/tutorial006b_py39.py",
|
||||
"docs_src/custom_response/tutorial006c_py39.py",
|
||||
"docs_src/custom_response/tutorial007_py39.py",
|
||||
"docs_src/custom_response/tutorial008_py39.py",
|
||||
"docs_src/custom_response/tutorial009_py39.py",
|
||||
"docs_src/custom_response/tutorial009b_py39.py",
|
||||
"docs_src/custom_response/tutorial009c_py39.py",
|
||||
"docs_src/custom_response/tutorial010_py39.py",
|
||||
"docs_src/debugging/tutorial001_py39.py",
|
||||
"docs_src/dependencies/tutorial006_an_py39.py",
|
||||
"docs_src/dependencies/tutorial006_py39.py",
|
||||
"docs_src/dependencies/tutorial007_py39.py",
|
||||
"docs_src/dependencies/tutorial008_py39.py",
|
||||
"docs_src/dependencies/tutorial008b_an_py39.py",
|
||||
"docs_src/dependencies/tutorial008b_py39.py",
|
||||
"docs_src/dependencies/tutorial008c_an_py39.py",
|
||||
"docs_src/dependencies/tutorial008c_py39.py",
|
||||
"docs_src/dependencies/tutorial008d_an_py39.py",
|
||||
"docs_src/dependencies/tutorial008d_py39.py",
|
||||
"docs_src/dependencies/tutorial008e_an_py39.py",
|
||||
"docs_src/dependencies/tutorial008e_py39.py",
|
||||
"docs_src/dependencies/tutorial010_py39.py",
|
||||
"docs_src/dependencies/tutorial011_an_py39.py",
|
||||
"docs_src/dependencies/tutorial011_py39.py",
|
||||
"docs_src/dependencies/tutorial012_an_py39.py",
|
||||
"docs_src/dependencies/tutorial012_py39.py",
|
||||
"docs_src/events/tutorial001_py39.py",
|
||||
"docs_src/events/tutorial002_py39.py",
|
||||
"docs_src/events/tutorial003_py39.py",
|
||||
"docs_src/extending_openapi/tutorial001_py39.py",
|
||||
"docs_src/extra_models/tutorial004_py39.py",
|
||||
"docs_src/extra_models/tutorial005_py39.py",
|
||||
"docs_src/first_steps/tutorial001_py39.py",
|
||||
"docs_src/first_steps/tutorial003_py39.py",
|
||||
"docs_src/generate_clients/tutorial001_py39.py",
|
||||
"docs_src/generate_clients/tutorial002_py39.py",
|
||||
"docs_src/generate_clients/tutorial003_py39.py",
|
||||
"docs_src/generate_clients/tutorial004_py39.py",
|
||||
"docs_src/graphql_/tutorial001_py39.py",
|
||||
"docs_src/handling_errors/tutorial001_py39.py",
|
||||
"docs_src/handling_errors/tutorial002_py39.py",
|
||||
"docs_src/handling_errors/tutorial003_py39.py",
|
||||
"docs_src/handling_errors/tutorial004_py39.py",
|
||||
"docs_src/handling_errors/tutorial005_py39.py",
|
||||
"docs_src/handling_errors/tutorial006_py39.py",
|
||||
"docs_src/metadata/tutorial001_1_py39.py",
|
||||
"docs_src/metadata/tutorial001_py39.py",
|
||||
"docs_src/metadata/tutorial002_py39.py",
|
||||
"docs_src/metadata/tutorial003_py39.py",
|
||||
"docs_src/metadata/tutorial004_py39.py",
|
||||
"docs_src/middleware/tutorial001_py39.py",
|
||||
"docs_src/openapi_webhooks/tutorial001_py39.py",
|
||||
"docs_src/path_operation_advanced_configuration/tutorial001_py39.py",
|
||||
"docs_src/path_operation_advanced_configuration/tutorial002_py39.py",
|
||||
"docs_src/path_operation_advanced_configuration/tutorial003_py39.py",
|
||||
"docs_src/path_operation_advanced_configuration/tutorial005_py39.py",
|
||||
"docs_src/path_operation_advanced_configuration/tutorial006_py39.py",
|
||||
"docs_src/path_operation_advanced_configuration/tutorial007_py39.py",
|
||||
"docs_src/path_operation_configuration/tutorial002b_py39.py",
|
||||
"docs_src/path_operation_configuration/tutorial006_py39.py",
|
||||
"docs_src/path_params/tutorial001_py39.py",
|
||||
"docs_src/path_params/tutorial002_py39.py",
|
||||
"docs_src/path_params/tutorial003_py39.py",
|
||||
"docs_src/path_params/tutorial003b_py39.py",
|
||||
"docs_src/path_params/tutorial004_py39.py",
|
||||
"docs_src/path_params/tutorial005_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial002_an_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial002_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial003_an_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial003_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial004_an_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial004_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial005_an_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial005_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial006_an_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial006_py39.py",
|
||||
"docs_src/python_types/tutorial001_py39.py",
|
||||
"docs_src/python_types/tutorial002_py39.py",
|
||||
"docs_src/python_types/tutorial003_py39.py",
|
||||
"docs_src/python_types/tutorial004_py39.py",
|
||||
"docs_src/python_types/tutorial005_py39.py",
|
||||
"docs_src/python_types/tutorial006_py39.py",
|
||||
"docs_src/python_types/tutorial007_py39.py",
|
||||
"docs_src/python_types/tutorial008_py39.py",
|
||||
"docs_src/python_types/tutorial008b_py39.py",
|
||||
"docs_src/python_types/tutorial009_py39.py",
|
||||
"docs_src/python_types/tutorial009b_py39.py",
|
||||
"docs_src/python_types/tutorial009c_py39.py",
|
||||
"docs_src/python_types/tutorial010_py39.py",
|
||||
"docs_src/python_types/tutorial013_py39.py",
|
||||
"docs_src/query_params/tutorial001_py39.py",
|
||||
"docs_src/query_params/tutorial005_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial005_an_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial005_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial006_an_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial006_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial012_an_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial012_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial013_an_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial013_py39.py",
|
||||
"docs_src/request_files/tutorial001_03_an_py39.py",
|
||||
"docs_src/request_files/tutorial001_03_py39.py",
|
||||
"docs_src/request_files/tutorial001_an_py39.py",
|
||||
"docs_src/request_files/tutorial001_py39.py",
|
||||
"docs_src/request_files/tutorial002_an_py39.py",
|
||||
"docs_src/request_files/tutorial002_py39.py",
|
||||
"docs_src/request_files/tutorial003_an_py39.py",
|
||||
"docs_src/request_files/tutorial003_py39.py",
|
||||
"docs_src/request_form_models/tutorial001_an_py39.py",
|
||||
"docs_src/request_form_models/tutorial001_py39.py",
|
||||
"docs_src/request_form_models/tutorial002_an_py39.py",
|
||||
"docs_src/request_form_models/tutorial002_py39.py",
|
||||
"docs_src/request_forms/tutorial001_an_py39.py",
|
||||
"docs_src/request_forms/tutorial001_py39.py",
|
||||
"docs_src/request_forms_and_files/tutorial001_an_py39.py",
|
||||
"docs_src/request_forms_and_files/tutorial001_py39.py",
|
||||
"docs_src/response_change_status_code/tutorial001_py39.py",
|
||||
"docs_src/response_cookies/tutorial001_py39.py",
|
||||
"docs_src/response_cookies/tutorial002_py39.py",
|
||||
"docs_src/response_directly/tutorial002_py39.py",
|
||||
"docs_src/response_headers/tutorial001_py39.py",
|
||||
"docs_src/response_headers/tutorial002_py39.py",
|
||||
"docs_src/response_model/tutorial003_02_py39.py",
|
||||
"docs_src/response_model/tutorial003_03_py39.py",
|
||||
"docs_src/response_status_code/tutorial001_py39.py",
|
||||
"docs_src/response_status_code/tutorial002_py39.py",
|
||||
"docs_src/security/tutorial001_an_py39.py",
|
||||
"docs_src/security/tutorial001_py39.py",
|
||||
"docs_src/security/tutorial006_an_py39.py",
|
||||
"docs_src/security/tutorial006_py39.py",
|
||||
"docs_src/security/tutorial007_an_py39.py",
|
||||
"docs_src/security/tutorial007_py39.py",
|
||||
"docs_src/settings/app01_py39/config.py",
|
||||
"docs_src/settings/app01_py39/main.py",
|
||||
"docs_src/settings/app02_an_py39/config.py",
|
||||
"docs_src/settings/app02_an_py39/main.py",
|
||||
"docs_src/settings/app02_an_py39/test_main.py",
|
||||
"docs_src/settings/app02_py39/config.py",
|
||||
"docs_src/settings/app02_py39/main.py",
|
||||
"docs_src/settings/app02_py39/test_main.py",
|
||||
"docs_src/settings/app03_an_py39/config.py",
|
||||
"docs_src/settings/app03_an_py39/main.py",
|
||||
"docs_src/settings/app03_py39/config.py",
|
||||
"docs_src/settings/app03_py39/main.py",
|
||||
"docs_src/settings/tutorial001_py39.py",
|
||||
"docs_src/static_files/tutorial001_py39.py",
|
||||
"docs_src/sub_applications/tutorial001_py39.py",
|
||||
"docs_src/templates/tutorial001_py39.py",
|
||||
"docs_src/using_request_directly/tutorial001_py39.py",
|
||||
"docs_src/websockets/tutorial001_py39.py",
|
||||
"docs_src/websockets/tutorial003_py39.py",
|
||||
"docs_src/wsgi/tutorial001_py39.py",
|
||||
]
|
||||
|
||||
[tool.coverage.report]
|
||||
@@ -315,7 +548,6 @@ ignore = [
|
||||
"docs_src/security/tutorial005_an_py39.py" = ["B904"]
|
||||
"docs_src/security/tutorial005_py310.py" = ["B904"]
|
||||
"docs_src/security/tutorial005_py39.py" = ["B904"]
|
||||
"docs_src/json_base64_bytes/tutorial001_py310.py" = ["UP012"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-third-party = ["fastapi", "pydantic", "starlette"]
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from playwright.sync_api import Playwright, sync_playwright
|
||||
|
||||
|
||||
# Run playwright codegen to generate the code below, copy paste the sections in run()
|
||||
def run(playwright: Playwright) -> None:
|
||||
browser = playwright.chromium.launch(headless=False)
|
||||
# Update the viewport manually
|
||||
context = browser.new_context(viewport={"width": 960, "height": 1080})
|
||||
page = context.new_page()
|
||||
page.goto("http://localhost:8000/docs")
|
||||
page.get_by_role("button", name="POST /data Post Data").click()
|
||||
# Manually add the screenshot
|
||||
page.screenshot(path="docs/en/docs/img/tutorial/json-base64-bytes/image01.png")
|
||||
|
||||
# ---------------------
|
||||
context.close()
|
||||
browser.close()
|
||||
|
||||
|
||||
process = subprocess.Popen(
|
||||
["fastapi", "run", "docs_src/json_base64_bytes/tutorial001_py310.py"]
|
||||
)
|
||||
try:
|
||||
for _ in range(3):
|
||||
try:
|
||||
response = httpx.get("http://localhost:8000/docs")
|
||||
except httpx.ConnectError:
|
||||
time.sleep(1)
|
||||
break
|
||||
with sync_playwright() as playwright:
|
||||
run(playwright)
|
||||
finally:
|
||||
process.terminate()
|
||||
@@ -1,73 +0,0 @@
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from fastapi.responses import ORJSONResponse, UJSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
# ORJSON
|
||||
|
||||
|
||||
def _make_orjson_app() -> FastAPI:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
|
||||
@app.get("/items")
|
||||
def get_items() -> Item:
|
||||
return Item(name="widget", price=9.99)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_orjson_response_returns_correct_data():
|
||||
app = _make_orjson_app()
|
||||
client = TestClient(app)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
response = client.get("/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "widget", "price": 9.99}
|
||||
|
||||
|
||||
def test_orjson_response_emits_deprecation_warning():
|
||||
with pytest.warns(FastAPIDeprecationWarning, match="ORJSONResponse is deprecated"):
|
||||
ORJSONResponse(content={"hello": "world"})
|
||||
|
||||
|
||||
# UJSON
|
||||
|
||||
|
||||
def _make_ujson_app() -> FastAPI:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
app = FastAPI(default_response_class=UJSONResponse)
|
||||
|
||||
@app.get("/items")
|
||||
def get_items() -> Item:
|
||||
return Item(name="widget", price=9.99)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_ujson_response_returns_correct_data():
|
||||
app = _make_ujson_app()
|
||||
client = TestClient(app)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
response = client.get("/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "widget", "price": 9.99}
|
||||
|
||||
|
||||
def test_ujson_response_emits_deprecation_warning():
|
||||
with pytest.warns(FastAPIDeprecationWarning, match="UJSONResponse is deprecated"):
|
||||
UJSONResponse(content={"hello": "world"})
|
||||
@@ -1,51 +0,0 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/default")
|
||||
def get_default() -> Item:
|
||||
return Item(name="widget", price=9.99)
|
||||
|
||||
|
||||
@app.get("/explicit", response_class=JSONResponse)
|
||||
def get_explicit() -> Item:
|
||||
return Item(name="widget", price=9.99)
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_default_response_class_skips_json_dumps():
|
||||
"""When no response_class is set, the fast path serializes directly to
|
||||
JSON bytes via Pydantic's dump_json and never calls json.dumps."""
|
||||
with patch(
|
||||
"starlette.responses.json.dumps", wraps=__import__("json").dumps
|
||||
) as mock_dumps:
|
||||
response = client.get("/default")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "widget", "price": 9.99}
|
||||
mock_dumps.assert_not_called()
|
||||
|
||||
|
||||
def test_explicit_response_class_uses_json_dumps():
|
||||
"""When response_class is explicitly set to JSONResponse, the normal path
|
||||
is used and json.dumps is called via JSONResponse.render()."""
|
||||
with patch(
|
||||
"starlette.responses.json.dumps", wraps=__import__("json").dumps
|
||||
) as mock_dumps:
|
||||
response = client.get("/explicit")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "widget", "price": 9.99}
|
||||
mock_dumps.assert_called_once()
|
||||
@@ -1,14 +1,9 @@
|
||||
import warnings
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
|
||||
|
||||
@app.get("/orjson_non_str_keys")
|
||||
@@ -21,8 +16,6 @@ client = TestClient(app)
|
||||
|
||||
|
||||
def test_orjson_non_str_keys():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
with client:
|
||||
response = client.get("/orjson_non_str_keys")
|
||||
with client:
|
||||
response = client.get("/orjson_non_str_keys")
|
||||
assert response.json() == {"msg": "Hello World", "1": 1}
|
||||
|
||||
@@ -37,10 +37,7 @@ def test_list_schema(path: str):
|
||||
"properties": {
|
||||
"p": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"title": "P",
|
||||
},
|
||||
},
|
||||
@@ -118,10 +115,7 @@ def test_list_alias_schema(path: str):
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
@@ -227,10 +221,7 @@ def test_list_validation_alias_schema(path: str):
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
@@ -347,10 +338,7 @@ def test_list_alias_and_validation_alias_schema(path: str):
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ def test_optional_schema(path: str):
|
||||
"properties": {
|
||||
"p": {
|
||||
"anyOf": [
|
||||
{"type": "string", "contentMediaType": "application/octet-stream"},
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
@@ -109,7 +109,7 @@ def test_optional_alias_schema(path: str):
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"anyOf": [
|
||||
{"type": "string", "contentMediaType": "application/octet-stream"},
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
@@ -200,7 +200,7 @@ def test_optional_validation_alias_schema(path: str):
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [
|
||||
{"type": "string", "contentMediaType": "application/octet-stream"},
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
@@ -296,7 +296,7 @@ def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [
|
||||
{"type": "string", "contentMediaType": "application/octet-stream"},
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
|
||||
@@ -41,10 +41,7 @@ def test_optional_list_schema(path: str):
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
@@ -119,10 +116,7 @@ def test_optional_list_alias_schema(path: str):
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
@@ -211,10 +205,7 @@ def test_optional_validation_alias_schema(path: str):
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
@@ -310,10 +301,7 @@ def test_optional_list_alias_and_validation_alias_schema(path: str):
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
|
||||
@@ -35,11 +35,7 @@ def test_required_schema(path: str):
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": {
|
||||
"title": "P",
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"p": {"title": "P", "type": "string", "format": "binary"},
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
@@ -113,11 +109,7 @@ def test_required_alias_schema(path: str):
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"title": "P Alias",
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"p_alias": {"title": "P Alias", "type": "string", "format": "binary"},
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
@@ -224,7 +216,7 @@ def test_required_validation_alias_schema(path: str):
|
||||
"p_val_alias": {
|
||||
"title": "P Val Alias",
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"format": "binary",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
@@ -337,7 +329,7 @@ def test_required_alias_and_validation_alias_schema(path: str):
|
||||
"p_val_alias": {
|
||||
"title": "P Val Alias",
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"format": "binary",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
|
||||
@@ -9,6 +9,7 @@ from inline_snapshot import snapshot
|
||||
name="client",
|
||||
params=[
|
||||
pytest.param("tutorial001_py310"),
|
||||
pytest.param("tutorial010_py310"),
|
||||
],
|
||||
)
|
||||
def get_client(request: pytest.FixtureRequest):
|
||||
@@ -17,14 +18,12 @@ def get_client(request: pytest.FixtureRequest):
|
||||
return client
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_get_custom_response(client: TestClient):
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
from docs_src.custom_response.tutorial001b_py310 import app
|
||||
from docs_src.custom_response.tutorial001b_py310 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_get_custom_response():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
name="client",
|
||||
params=[
|
||||
pytest.param("tutorial010_py310"),
|
||||
],
|
||||
)
|
||||
def get_client(request: pytest.FixtureRequest):
|
||||
mod = importlib.import_module(f"docs_src.custom_response.{request.param}")
|
||||
client = TestClient(mod.app)
|
||||
return client
|
||||
|
||||
|
||||
def test_get_custom_response(client: TestClient):
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.text == snapshot("<h1>Items</h1><p>This is a list of items.</p>")
|
||||
|
||||
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == snapshot(
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"text/html": {"schema": {"type": "string"}}
|
||||
},
|
||||
}
|
||||
},
|
||||
"summary": "Read Items",
|
||||
"operationId": "read_items_items__get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1,225 +0,0 @@
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
from tests.utils import needs_py310
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
name="client",
|
||||
params=[pytest.param("tutorial001_py310", marks=needs_py310)],
|
||||
)
|
||||
def get_client(request: pytest.FixtureRequest):
|
||||
mod = importlib.import_module(f"docs_src.json_base64_bytes.{request.param}")
|
||||
|
||||
client = TestClient(mod.app)
|
||||
return client
|
||||
|
||||
|
||||
def test_post_data(client: TestClient):
|
||||
response = client.post(
|
||||
"/data",
|
||||
json={
|
||||
"description": "A file",
|
||||
"data": "SGVsbG8sIFdvcmxkIQ==",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"description": "A file", "content": "Hello, World!"}
|
||||
|
||||
|
||||
def test_get_data(client: TestClient):
|
||||
response = client.get("/data")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"description": "A plumbus", "data": "aGVsbG8="}
|
||||
|
||||
|
||||
def test_post_data_in_out(client: TestClient):
|
||||
response = client.post(
|
||||
"/data-in-out",
|
||||
json={
|
||||
"description": "A plumbus",
|
||||
"data": "SGVsbG8sIFdvcmxkIQ==",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"description": "A plumbus",
|
||||
"data": "SGVsbG8sIFdvcmxkIQ==",
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == snapshot(
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/data": {
|
||||
"get": {
|
||||
"summary": "Get Data",
|
||||
"operationId": "get_data_data_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DataOutput"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"post": {
|
||||
"summary": "Post Data",
|
||||
"operationId": "post_data_data_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/DataInput"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/data-in-out": {
|
||||
"post": {
|
||||
"summary": "Post Data In Out",
|
||||
"operationId": "post_data_in_out_data_in_out_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DataInputOutput"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DataInputOutput"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"DataInput": {
|
||||
"properties": {
|
||||
"description": {"type": "string", "title": "Description"},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"contentEncoding": "base64",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"title": "Data",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["description", "data"],
|
||||
"title": "DataInput",
|
||||
},
|
||||
"DataInputOutput": {
|
||||
"properties": {
|
||||
"description": {"type": "string", "title": "Description"},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"contentEncoding": "base64",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"title": "Data",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["description", "data"],
|
||||
"title": "DataInputOutput",
|
||||
},
|
||||
"DataOutput": {
|
||||
"properties": {
|
||||
"description": {"type": "string", "title": "Description"},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"contentEncoding": "base64",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"title": "Data",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["description", "data"],
|
||||
"title": "DataOutput",
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"properties": {
|
||||
"detail": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Detail",
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "HTTPValidationError",
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"ctx": {"title": "Context", "type": "object"},
|
||||
"input": {"title": "Input"},
|
||||
"loc": {
|
||||
"items": {
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}]
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Location",
|
||||
},
|
||||
"msg": {"type": "string", "title": "Message"},
|
||||
"type": {"type": "string", "title": "Error Type"},
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"title": "ValidationError",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -162,8 +162,8 @@ def test_openapi_schema(client: TestClient):
|
||||
"properties": {
|
||||
"file": {
|
||||
"title": "File",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -175,7 +175,7 @@ def test_openapi_schema(client: TestClient):
|
||||
"file": {
|
||||
"title": "File",
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"format": "binary",
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -134,10 +134,7 @@ def test_openapi_schema(client: TestClient):
|
||||
"file": {
|
||||
"title": "File",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
}
|
||||
@@ -150,10 +147,7 @@ def test_openapi_schema(client: TestClient):
|
||||
"file": {
|
||||
"title": "File",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ def test_openapi_schema(client: TestClient):
|
||||
"title": "File",
|
||||
"type": "string",
|
||||
"description": "A file read as bytes",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"format": "binary",
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -134,9 +134,9 @@ def test_openapi_schema(client: TestClient):
|
||||
"properties": {
|
||||
"file": {
|
||||
"title": "File",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"type": "string",
|
||||
"description": "A file read as UploadFile",
|
||||
"format": "binary",
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -195,10 +195,7 @@ def test_openapi_schema(client: TestClient):
|
||||
"files": {
|
||||
"title": "Files",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -210,10 +207,7 @@ def test_openapi_schema(client: TestClient):
|
||||
"files": {
|
||||
"title": "Files",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -165,10 +165,7 @@ def test_openapi_schema(client: TestClient):
|
||||
"files": {
|
||||
"title": "Files",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"description": "Multiple files as bytes",
|
||||
}
|
||||
},
|
||||
@@ -181,10 +178,7 @@ def test_openapi_schema(client: TestClient):
|
||||
"files": {
|
||||
"title": "Files",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
},
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"description": "Multiple files as UploadFile",
|
||||
}
|
||||
},
|
||||
|
||||
@@ -198,12 +198,12 @@ def test_openapi_schema(client: TestClient):
|
||||
"file": {
|
||||
"title": "File",
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"format": "binary",
|
||||
},
|
||||
"fileb": {
|
||||
"title": "Fileb",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
},
|
||||
"token": {"title": "Token", "type": "string"},
|
||||
},
|
||||
|
||||
36
uv.lock
generated
36
uv.lock
generated
@@ -1083,10 +1083,12 @@ all = [
|
||||
{ name = "httpx" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pydantic-extra-types" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "ujson" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
standard = [
|
||||
@@ -1132,7 +1134,6 @@ dev = [
|
||||
{ name = "mkdocs-redirects" },
|
||||
{ name = "mkdocstrings", extra = ["python"] },
|
||||
{ name = "mypy" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pillow" },
|
||||
{ name = "playwright" },
|
||||
{ name = "prek" },
|
||||
@@ -1150,7 +1151,6 @@ dev = [
|
||||
{ name = "typer" },
|
||||
{ name = "types-orjson" },
|
||||
{ name = "types-ujson" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "black" },
|
||||
@@ -1165,19 +1165,15 @@ docs = [
|
||||
{ name = "mkdocs-material" },
|
||||
{ name = "mkdocs-redirects" },
|
||||
{ name = "mkdocstrings", extra = ["python"] },
|
||||
{ name = "orjson" },
|
||||
{ name = "pillow" },
|
||||
{ name = "python-slugify" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "ruff" },
|
||||
{ name = "typer" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
docs-tests = [
|
||||
{ name = "httpx" },
|
||||
{ name = "orjson" },
|
||||
{ name = "ruff" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
github-actions = [
|
||||
{ name = "httpx" },
|
||||
@@ -1196,7 +1192,6 @@ tests = [
|
||||
{ name = "httpx" },
|
||||
{ name = "inline-snapshot" },
|
||||
{ name = "mypy" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pwdlib", extra = ["argon2"] },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "pytest" },
|
||||
@@ -1207,7 +1202,6 @@ tests = [
|
||||
{ name = "strawberry-graphql" },
|
||||
{ name = "types-orjson" },
|
||||
{ name = "types-ujson" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
translations = [
|
||||
{ name = "gitpython" },
|
||||
@@ -1231,6 +1225,7 @@ requires-dist = [
|
||||
{ name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" },
|
||||
{ name = "jinja2", marker = "extra == 'standard'", specifier = ">=3.1.5" },
|
||||
{ name = "jinja2", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=3.1.5" },
|
||||
{ name = "orjson", marker = "extra == 'all'", specifier = ">=3.9.3" },
|
||||
{ name = "pydantic", specifier = ">=2.7.0" },
|
||||
{ name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" },
|
||||
{ name = "pydantic-extra-types", marker = "extra == 'standard'", specifier = ">=2.0.0" },
|
||||
@@ -1245,6 +1240,7 @@ requires-dist = [
|
||||
{ name = "starlette", specifier = ">=0.40.0,<1.0.0" },
|
||||
{ name = "typing-extensions", specifier = ">=4.8.0" },
|
||||
{ name = "typing-inspection", specifier = ">=0.4.2" },
|
||||
{ name = "ujson", marker = "extra == 'all'", specifier = ">=5.8.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.12.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.12.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.12.0" },
|
||||
@@ -1273,7 +1269,6 @@ dev = [
|
||||
{ name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
|
||||
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
|
||||
{ name = "mypy", specifier = ">=1.14.1" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "pillow", specifier = ">=11.3.0" },
|
||||
{ name = "playwright", specifier = ">=1.57.0" },
|
||||
{ name = "prek", specifier = ">=0.2.22" },
|
||||
@@ -1281,7 +1276,7 @@ dev = [
|
||||
{ name = "pydantic-ai", specifier = ">=0.4.10" },
|
||||
{ name = "pygithub", specifier = ">=2.8.1" },
|
||||
{ name = "pyjwt", specifier = ">=2.9.0" },
|
||||
{ name = "pytest", specifier = ">=9.0.0" },
|
||||
{ name = "pytest", specifier = ">=7.1.3,<9.0.0" },
|
||||
{ name = "pytest-codspeed", specifier = ">=4.2.0" },
|
||||
{ name = "python-slugify", specifier = ">=8.0.4" },
|
||||
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
|
||||
@@ -1291,7 +1286,6 @@ dev = [
|
||||
{ name = "typer", specifier = ">=0.21.1" },
|
||||
{ name = "types-orjson", specifier = ">=3.6.2" },
|
||||
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "black", specifier = ">=25.1.0" },
|
||||
@@ -1306,19 +1300,15 @@ docs = [
|
||||
{ name = "mkdocs-material", specifier = ">=9.7.0" },
|
||||
{ name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
|
||||
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "pillow", specifier = ">=11.3.0" },
|
||||
{ name = "python-slugify", specifier = ">=8.0.4" },
|
||||
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
|
||||
{ name = "ruff", specifier = ">=0.14.14" },
|
||||
{ name = "typer", specifier = ">=0.21.1" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
docs-tests = [
|
||||
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "ruff", specifier = ">=0.14.14" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
github-actions = [
|
||||
{ name = "httpx", specifier = ">=0.27.0,<1.0.0" },
|
||||
@@ -1337,10 +1327,9 @@ tests = [
|
||||
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" },
|
||||
{ name = "inline-snapshot", specifier = ">=0.21.1" },
|
||||
{ name = "mypy", specifier = ">=1.14.1" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" },
|
||||
{ name = "pyjwt", specifier = ">=2.9.0" },
|
||||
{ name = "pytest", specifier = ">=9.0.0" },
|
||||
{ name = "pytest", specifier = ">=7.1.3,<9.0.0" },
|
||||
{ name = "pytest-codspeed", specifier = ">=4.2.0" },
|
||||
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
|
||||
{ name = "ruff", specifier = ">=0.14.14" },
|
||||
@@ -1348,7 +1337,6 @@ tests = [
|
||||
{ name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" },
|
||||
{ name = "types-orjson", specifier = ">=3.6.2" },
|
||||
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
translations = [
|
||||
{ name = "gitpython", specifier = ">=3.1.46" },
|
||||
@@ -1607,7 +1595,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.1.2"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "blinker" },
|
||||
@@ -1617,9 +1605,9 @@ dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4342,7 +4330,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
version = "8.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -4353,9 +4341,9 @@ dependencies = [
|
||||
{ name = "pygments" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user