Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
e64e9457cb ⬆ Bump flask from 3.1.2 to 3.1.3
Bumps [flask](https://github.com/pallets/flask) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.1.2...3.1.3)

---
updated-dependencies:
- dependency-name: flask
  dependency-version: 3.1.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-20 15:41:38 +00:00
28 changed files with 330 additions and 498 deletions

2
.github/labeler.yml vendored
View File

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

View File

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

View File

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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -7,21 +7,6 @@ hide:
## Latest Changes
## 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).

View File

@@ -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

View File

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-
)
from pydantic._internal._typing_extra import eval_type_lenient
from pydantic.fields import FieldInfo as FieldInfo
from pydantic.json_schema import GenerateJsonSchema as _GenerateJsonSchema
from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
from pydantic_core import CoreSchema as CoreSchema
from pydantic_core import PydanticUndefined
@@ -40,23 +40,6 @@ RequiredParam = PydanticUndefined
Undefined = PydanticUndefined
evaluate_forwardref = eval_type_lenient
class GenerateJsonSchema(_GenerateJsonSchema):
# TODO: remove when this is merged (or equivalent): https://github.com/pydantic/pydantic/pull/12841
# and dropping support for any version of Pydantic before that one (so, in a very long time)
def bytes_schema(self, schema: CoreSchema) -> JsonSchemaValue:
json_schema = {"type": "string", "contentMediaType": "application/octet-stream"}
bytes_mode = (
self._config.ser_json_bytes
if self.mode == "serialization"
else self._config.val_json_bytes
)
if bytes_mode == "base64":
json_schema["contentEncoding"] = "base64"
self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes)
return json_schema
# TODO: remove when dropping support for Pydantic < v2.12.3
_Attrs = {
"default": ...,

View File

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

40
pdm_build.py Normal file
View File

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

View File

@@ -57,6 +57,7 @@ Issues = "https://github.com/fastapi/fastapi/issues"
Changelog = "https://fastapi.tiangolo.com/release-notes/"
[project.optional-dependencies]
standard = [
"fastapi-cli[standard] >=0.0.8",
# For the test client
@@ -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"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

16
uv.lock generated
View File

@@ -1276,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" },
@@ -1329,7 +1329,7 @@ tests = [
{ name = "mypy", specifier = ">=1.14.1" },
{ 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" },
@@ -1595,7 +1595,7 @@ wheels = [
[[package]]
name = "flask"
version = "3.1.2"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
@@ -1605,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]]
@@ -4330,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'" },
@@ -4341,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]]