Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
1452b51446 ⬆ Bump werkzeug from 3.1.5 to 3.1.6
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.1.5 to 3.1.6.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.1.5...3.1.6)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-version: 3.1.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 16:51:40 +00:00
17 changed files with 457 additions and 871 deletions

View File

@@ -208,4 +208,4 @@ jobs:
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
allowed-skips: coverage-combine,test,benchmark
allowed-skips: coverage-combine,test

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,88 +0,0 @@
# Strict Content-Type Checking { #strict-content-type-checking }
By default, **FastAPI** uses strict `Content-Type` header checking for JSON request bodies, this means that JSON requests **must** include a valid `Content-Type` header (e.g. `application/json`) in order for the body to be parsed as JSON.
## CSRF Risk { #csrf-risk }
This default behavior provides protection against a class of **Cross-Site Request Forgery (CSRF)** attacks in a very specific scenario.
These attacks exploit the fact that browsers allow scripts to send requests without doing any CORS preflight check when they:
* don't have a `Content-Type` header (e.g. using `fetch()` with a `Blob` body)
* and don't send any authentication credentials.
This type of attack is mainly relevant when:
* the application is running locally (e.g. on `localhost`) or in an internal network
* and the application doesn't have any authentication, it expects that any request from the same network can be trusted.
## Example Attack { #example-attack }
Imagine you build a way to run a local AI agent.
It provides an API at
```
http://localhost:8000/v1/agents/multivac
```
There's also a frontend at
```
http://localhost:8000
```
/// tip
Note that both have the same host.
///
Then using the frontend you can make the AI agent do things on your behalf.
As it's running **locally**, and not in the open internet, you decide to **not have any authentication** set up, just trusting the access to the local network.
Then one of your users could install it and run it locally.
Then they could open a malicious website, e.g. something like
```
https://evilhackers.example.com
```
And that malicious website sends requests using `fetch()` with a `Blob` body to the local API at
```
http://localhost:8000/v1/agents/multivac
```
Even though the host of the malicious website and the local app is different, the browser won't trigger a CORS preflight request because:
* It's running without any authentication, it doesn't have to send any credentials.
* The browser thinks it's not sending JSON (because of the missing `Content-Type` header).
Then the malicious website could make the local AI agent send angry messages to the user's ex-boss... or worse. 😅
## Open Internet { #open-internet }
If your app is in the open internet, you wouldn't "trust the network" and let anyone send privileged requests without authentication.
Attackers could simply run a script to send requests to your API, no need for browser interaction, so you are probably already securing any privileged endpoints.
In that case **this attack / risk doesn't apply to you**.
This risk and attack is mainly relevant when the app runs on the **local network** and that is the **only assumed protection**.
## Allowing Requests Without Content-Type { #allowing-requests-without-content-type }
If you need to support clients that don't send a `Content-Type` header, you can disable strict checking by setting `strict_content_type=False`:
{* ../../docs_src/strict_content_type/tutorial001_py310.py hl[4] *}
With this setting, requests without a `Content-Type` header will have their body parsed as JSON, which is the same behavior as older versions of FastAPI.
/// info
This behavior and configuration was added in FastAPI 0.132.0.
///

View File

@@ -9,20 +9,6 @@ hide:
### Internal
* 👥 Update FastAPI People - Experts. PR [#14972](https://github.com/fastapi/fastapi/pull/14972) by [@tiangolo](https://github.com/tiangolo).
* 👷 Allow skipping `benchmark` job in `test` workflow. PR [#14974](https://github.com/fastapi/fastapi/pull/14974) by [@YuriiMotov](https://github.com/YuriiMotov).
## 0.132.0
### Breaking Changes
* 🔒️ Add `strict_content_type` checking for JSON requests. PR [#14978](https://github.com/fastapi/fastapi/pull/14978) by [@tiangolo](https://github.com/tiangolo).
* Now FastAPI checks, by default, that JSON requests have a `Content-Type` header with a valid JSON value, like `application/json`, and rejects requests that don't.
* If the clients for your app don't send a valid `Content-Type` header you can disable this with `strict_content_type=False`.
* Check the new docs: [Strict Content-Type Checking](https://fastapi.tiangolo.com/advanced/strict-content-type/).
### Internal
* ⬆ Bump flask from 3.1.2 to 3.1.3. PR [#14949](https://github.com/fastapi/fastapi/pull/14949) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Update all dependencies to use `griffelib` instead of `griffe`. PR [#14973](https://github.com/fastapi/fastapi/pull/14973) by [@svlandeg](https://github.com/svlandeg).
* 🔨 Fix `FastAPI People` workflow. PR [#14951](https://github.com/fastapi/fastapi/pull/14951) by [@YuriiMotov](https://github.com/YuriiMotov).

View File

@@ -193,7 +193,6 @@ nav:
- advanced/generate-clients.md
- advanced/advanced-python-types.md
- advanced/json-base64-bytes.md
- advanced/strict-content-type.md
- fastapi-cli.md
- Deployment:
- deployment/index.md

View File

View File

@@ -1,14 +0,0 @@
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(strict_content_type=False)
class Item(BaseModel):
name: str
price: float
@app.post("/items/")
async def create_item(item: Item):
return item

View File

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

View File

@@ -840,29 +840,6 @@ class FastAPI(Starlette):
"""
),
] = None,
strict_content_type: Annotated[
bool,
Doc(
"""
Enable strict checking for request Content-Type headers.
When `True` (the default), requests with a body that do not include
a `Content-Type` header will **not** be parsed as JSON.
This prevents potential cross-site request forgery (CSRF) attacks
that exploit the browser's ability to send requests without a
Content-Type header, bypassing CORS preflight checks. In particular
applicable for apps that need to be run locally (in localhost).
When `False`, requests without a `Content-Type` header will have
their body parsed as JSON, which maintains compatibility with
certain clients that don't send `Content-Type` headers.
Read more about it in the
[FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/).
"""
),
] = True,
**extra: Annotated[
Any,
Doc(
@@ -997,7 +974,6 @@ class FastAPI(Starlette):
include_in_schema=include_in_schema,
responses=responses,
generate_unique_id_function=generate_unique_id_function,
strict_content_type=strict_content_type,
)
self.exception_handlers: dict[
Any, Callable[[Request, Any], Response | Awaitable[Response]]

View File

@@ -329,7 +329,6 @@ def get_request_handler(
response_model_exclude_none: bool = False,
dependency_overrides_provider: Any | None = None,
embed_body_fields: bool = False,
strict_content_type: bool | DefaultPlaceholder = Default(True),
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = dependant.is_coroutine_callable
@@ -338,10 +337,6 @@ def get_request_handler(
actual_response_class: type[Response] = response_class.value
else:
actual_response_class = response_class
if isinstance(strict_content_type, DefaultPlaceholder):
actual_strict_content_type: bool = strict_content_type.value
else:
actual_strict_content_type = strict_content_type
async def app(request: Request) -> Response:
response: Response | None = None
@@ -375,8 +370,7 @@ def get_request_handler(
json_body: Any = Undefined
content_type_value = request.headers.get("content-type")
if not content_type_value:
if not actual_strict_content_type:
json_body = await request.json()
json_body = await request.json()
else:
message = email.message.Message()
message["content-type"] = content_type_value
@@ -605,7 +599,6 @@ class APIRoute(routing.Route):
openapi_extra: dict[str, Any] | None = None,
generate_unique_id_function: Callable[["APIRoute"], str]
| DefaultPlaceholder = Default(generate_unique_id),
strict_content_type: bool | DefaultPlaceholder = Default(True),
) -> None:
self.path = path
self.endpoint = endpoint
@@ -632,7 +625,6 @@ class APIRoute(routing.Route):
self.callbacks = callbacks
self.openapi_extra = openapi_extra
self.generate_unique_id_function = generate_unique_id_function
self.strict_content_type = strict_content_type
self.tags = tags or []
self.responses = responses or {}
self.name = get_name(endpoint) if name is None else name
@@ -721,7 +713,6 @@ class APIRoute(routing.Route):
response_model_exclude_none=self.response_model_exclude_none,
dependency_overrides_provider=self.dependency_overrides_provider,
embed_body_fields=self._embed_body_fields,
strict_content_type=self.strict_content_type,
)
def matches(self, scope: Scope) -> tuple[Match, Scope]:
@@ -972,29 +963,6 @@ class APIRouter(routing.Router):
"""
),
] = Default(generate_unique_id),
strict_content_type: Annotated[
bool,
Doc(
"""
Enable strict checking for request Content-Type headers.
When `True` (the default), requests with a body that do not include
a `Content-Type` header will **not** be parsed as JSON.
This prevents potential cross-site request forgery (CSRF) attacks
that exploit the browser's ability to send requests without a
Content-Type header, bypassing CORS preflight checks. In particular
applicable for apps that need to be run locally (in localhost).
When `False`, requests without a `Content-Type` header will have
their body parsed as JSON, which maintains compatibility with
certain clients that don't send `Content-Type` headers.
Read more about it in the
[FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/).
"""
),
] = Default(True),
) -> None:
# Determine the lifespan context to use
if lifespan is None:
@@ -1041,7 +1009,6 @@ class APIRouter(routing.Router):
self.route_class = route_class
self.default_response_class = default_response_class
self.generate_unique_id_function = generate_unique_id_function
self.strict_content_type = strict_content_type
def route(
self,
@@ -1092,7 +1059,6 @@ class APIRouter(routing.Router):
openapi_extra: dict[str, Any] | None = None,
generate_unique_id_function: Callable[[APIRoute], str]
| DefaultPlaceholder = Default(generate_unique_id),
strict_content_type: bool | DefaultPlaceholder = Default(True),
) -> None:
route_class = route_class_override or self.route_class
responses = responses or {}
@@ -1139,9 +1105,6 @@ class APIRouter(routing.Router):
callbacks=current_callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=current_generate_unique_id,
strict_content_type=get_value_or_default(
strict_content_type, self.strict_content_type
),
)
self.routes.append(route)
@@ -1517,11 +1480,6 @@ class APIRouter(routing.Router):
callbacks=current_callbacks,
openapi_extra=route.openapi_extra,
generate_unique_id_function=current_generate_unique_id,
strict_content_type=get_value_or_default(
route.strict_content_type,
router.strict_content_type,
self.strict_content_type,
),
)
elif isinstance(route, routing.Route):
methods = list(route.methods or [])

View File

@@ -1,44 +0,0 @@
from fastapi import FastAPI
from fastapi.testclient import TestClient
app_default = FastAPI()
@app_default.post("/items/")
async def app_default_post(data: dict):
return data
app_lax = FastAPI(strict_content_type=False)
@app_lax.post("/items/")
async def app_lax_post(data: dict):
return data
client_default = TestClient(app_default)
client_lax = TestClient(app_lax)
def test_default_strict_rejects_no_content_type():
response = client_default.post("/items/", content='{"key": "value"}')
assert response.status_code == 422
def test_default_strict_accepts_json_content_type():
response = client_default.post("/items/", json={"key": "value"})
assert response.status_code == 200
assert response.json() == {"key": "value"}
def test_lax_accepts_no_content_type():
response = client_lax.post("/items/", content='{"key": "value"}')
assert response.status_code == 200
assert response.json() == {"key": "value"}
def test_lax_accepts_json_content_type():
response = client_lax.post("/items/", json={"key": "value"})
assert response.status_code == 200
assert response.json() == {"key": "value"}

View File

@@ -1,91 +0,0 @@
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
# Lax app with nested routers, inner overrides to strict
app_nested = FastAPI(strict_content_type=False) # lax app
outer_router = APIRouter(prefix="/outer") # inherits lax from app
inner_strict = APIRouter(prefix="/strict", strict_content_type=True)
inner_default = APIRouter(prefix="/default")
@inner_strict.post("/items/")
async def inner_strict_post(data: dict):
return data
@inner_default.post("/items/")
async def inner_default_post(data: dict):
return data
outer_router.include_router(inner_strict)
outer_router.include_router(inner_default)
app_nested.include_router(outer_router)
client_nested = TestClient(app_nested)
def test_strict_inner_on_lax_app_rejects_no_content_type():
response = client_nested.post("/outer/strict/items/", content='{"key": "value"}')
assert response.status_code == 422
def test_default_inner_inherits_lax_from_app():
response = client_nested.post("/outer/default/items/", content='{"key": "value"}')
assert response.status_code == 200
assert response.json() == {"key": "value"}
def test_strict_inner_accepts_json_content_type():
response = client_nested.post("/outer/strict/items/", json={"key": "value"})
assert response.status_code == 200
def test_default_inner_accepts_json_content_type():
response = client_nested.post("/outer/default/items/", json={"key": "value"})
assert response.status_code == 200
# Strict app -> lax outer router -> strict inner router
app_mixed = FastAPI(strict_content_type=True)
mixed_outer = APIRouter(prefix="/outer", strict_content_type=False)
mixed_inner = APIRouter(prefix="/inner", strict_content_type=True)
@mixed_outer.post("/items/")
async def mixed_outer_post(data: dict):
return data
@mixed_inner.post("/items/")
async def mixed_inner_post(data: dict):
return data
mixed_outer.include_router(mixed_inner)
app_mixed.include_router(mixed_outer)
client_mixed = TestClient(app_mixed)
def test_lax_outer_on_strict_app_accepts_no_content_type():
response = client_mixed.post("/outer/items/", content='{"key": "value"}')
assert response.status_code == 200
assert response.json() == {"key": "value"}
def test_strict_inner_on_lax_outer_rejects_no_content_type():
response = client_mixed.post("/outer/inner/items/", content='{"key": "value"}')
assert response.status_code == 422
def test_lax_outer_accepts_json_content_type():
response = client_mixed.post("/outer/items/", json={"key": "value"})
assert response.status_code == 200
def test_strict_inner_on_lax_outer_accepts_json_content_type():
response = client_mixed.post("/outer/inner/items/", json={"key": "value"})
assert response.status_code == 200

View File

@@ -1,61 +0,0 @@
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
router_lax = APIRouter(prefix="/lax", strict_content_type=False)
router_strict = APIRouter(prefix="/strict", strict_content_type=True)
router_default = APIRouter(prefix="/default")
@router_lax.post("/items/")
async def router_lax_post(data: dict):
return data
@router_strict.post("/items/")
async def router_strict_post(data: dict):
return data
@router_default.post("/items/")
async def router_default_post(data: dict):
return data
app.include_router(router_lax)
app.include_router(router_strict)
app.include_router(router_default)
client = TestClient(app)
def test_lax_router_on_strict_app_accepts_no_content_type():
response = client.post("/lax/items/", content='{"key": "value"}')
assert response.status_code == 200
assert response.json() == {"key": "value"}
def test_strict_router_on_strict_app_rejects_no_content_type():
response = client.post("/strict/items/", content='{"key": "value"}')
assert response.status_code == 422
def test_default_router_inherits_strict_from_app():
response = client.post("/default/items/", content='{"key": "value"}')
assert response.status_code == 422
def test_lax_router_accepts_json_content_type():
response = client.post("/lax/items/", json={"key": "value"})
assert response.status_code == 200
def test_strict_router_accepts_json_content_type():
response = client.post("/strict/items/", json={"key": "value"})
assert response.status_code == 200
def test_default_router_accepts_json_content_type():
response = client.post("/default/items/", json={"key": "value"})
assert response.status_code == 200

View File

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

View File

@@ -1,43 +0,0 @@
import importlib
import pytest
from fastapi.testclient import TestClient
@pytest.fixture(
name="client",
params=[
"tutorial001_py310",
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.strict_content_type.{request.param}")
client = TestClient(mod.app)
return client
def test_lax_post_without_content_type_is_parsed_as_json(client: TestClient):
response = client.post(
"/items/",
content='{"name": "Foo", "price": 50.5}',
)
assert response.status_code == 200, response.text
assert response.json() == {"name": "Foo", "price": 50.5}
def test_lax_post_with_json_content_type(client: TestClient):
response = client.post(
"/items/",
json={"name": "Foo", "price": 50.5},
)
assert response.status_code == 200, response.text
assert response.json() == {"name": "Foo", "price": 50.5}
def test_lax_post_with_text_plain_is_still_rejected(client: TestClient):
response = client.post(
"/items/",
content='{"name": "Foo", "price": 50.5}',
headers={"Content-Type": "text/plain"},
)
assert response.status_code == 422, response.text

66
uv.lock generated
View File

@@ -192,7 +192,7 @@ wheels = [
[[package]]
name = "anthropic"
version = "0.78.0"
version = "0.83.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -204,9 +204,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ec/51/32849a48f9b1cfe80a508fd269b20bd8f0b1357c70ba092890fde5a6a10b/anthropic-0.78.0.tar.gz", hash = "sha256:55fd978ab9b049c61857463f4c4e9e092b24f892519c6d8078cee1713d8af06e", size = 509136, upload-time = "2026-02-05T17:52:04.986Z" }
sdist = { url = "https://files.pythonhosted.org/packages/db/e5/02cd2919ec327b24234abb73082e6ab84c451182cc3cc60681af700f4c63/anthropic-0.83.0.tar.gz", hash = "sha256:a8732c68b41869266c3034541a31a29d8be0f8cd0a714f9edce3128b351eceb4", size = 534058, upload-time = "2026-02-19T19:26:38.904Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/03/2f50931a942e5e13f80e24d83406714672c57964be593fc046d81369335b/anthropic-0.78.0-py3-none-any.whl", hash = "sha256:2a9887d2e99d1b0f9fe08857a1e9fe5d2d4030455dbf9ac65aab052e2efaeac4", size = 405485, upload-time = "2026-02-05T17:52:03.674Z" },
{ url = "https://files.pythonhosted.org/packages/5f/75/b9d58e4e2a4b1fc3e75ffbab978f999baf8b7c4ba9f96e60edb918ba386b/anthropic-0.83.0-py3-none-any.whl", hash = "sha256:f069ef508c73b8f9152e8850830d92bd5ef185645dbacf234bb213344a274810", size = 456991, upload-time = "2026-02-19T19:26:40.114Z" },
]
[[package]]
@@ -1922,14 +1922,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" },
]
[[package]]
name = "griffelib"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" },
]
[[package]]
name = "griffe-typingdoc"
version = "0.3.1"
@@ -1955,6 +1947,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/3c/c2a9eee79bf2c8002d2fa370534bee93fdca39e8b1fc82e83d552d5d2c07/griffe_warnings_deprecated-1.1.1-py3-none-any.whl", hash = "sha256:4b7d765e82ca9139ed44ffe7bdebed0d3a46ce014ad5a35a2c22e9a16288737a", size = 6565, upload-time = "2026-02-20T15:35:23.577Z" },
]
[[package]]
name = "griffelib"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" },
]
[[package]]
name = "groq"
version = "1.0.0"
@@ -2162,26 +2162,23 @@ wheels = [
[[package]]
name = "huggingface-hub"
version = "0.36.2"
version = "1.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "fsspec" },
{ name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
{ name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
{ name = "httpx" },
{ name = "packaging" },
{ name = "pyyaml" },
{ name = "requests" },
{ name = "shellingham" },
{ name = "tqdm" },
{ name = "typer-slim" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c4/fc/eb9bc06130e8bbda6a616e1b80a7aa127681c448d6b49806f61db2670b61/huggingface_hub-1.4.1.tar.gz", hash = "sha256:b41131ec35e631e7383ab26d6146b8d8972abc8b6309b963b306fbcca87f5ed5", size = 642156, upload-time = "2026-02-06T09:20:03.013Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" },
]
[package.optional-dependencies]
inference = [
{ name = "aiohttp" },
{ url = "https://files.pythonhosted.org/packages/d5/ae/2f6d96b4e6c5478d87d606a1934b5d436c4a2bce6bb7c6fdece891c128e3/huggingface_hub-1.4.1-py3-none-any.whl", hash = "sha256:9931d075fb7a79af5abc487106414ec5fba2c0ae86104c0c62fd6cae38873d18", size = 553326, upload-time = "2026-02-06T09:20:00.728Z" },
]
[[package]]
@@ -3995,7 +3992,7 @@ groq = [
{ name = "groq" },
]
huggingface = [
{ name = "huggingface-hub", extra = ["inference"] },
{ name = "huggingface-hub" },
]
logfire = [
{ name = "logfire", extra = ["httpx"] },
@@ -4147,7 +4144,7 @@ wheels = [
[[package]]
name = "pydantic-evals"
version = "1.56.0"
version = "1.62.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -4157,9 +4154,9 @@ dependencies = [
{ name = "pyyaml" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/f2/8c59284a2978af3fbda45ae3217218eaf8b071207a9290b54b7613983e5d/pydantic_evals-1.56.0.tar.gz", hash = "sha256:206635107127af6a3ee4b1fc8f77af6afb14683615a2d6b3609f79467c1c0d28", size = 47210, upload-time = "2026-02-06T01:13:25.714Z" }
sdist = { url = "https://files.pythonhosted.org/packages/23/90/080f6722412263395d1d6d066ee90fa8bc2722ce097844220c2d9c946877/pydantic_evals-1.62.0.tar.gz", hash = "sha256:198c4bee936718a4acf6f504056b113e60b34eb49021df8889a394e14c803693", size = 56434, upload-time = "2026-02-19T05:07:11.793Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/51/9875d19ff6d584aaeb574aba76b49d931b822546fc60b29c4fc0da98170d/pydantic_evals-1.56.0-py3-none-any.whl", hash = "sha256:d1efb410c97135aabd2a22453b10c981b2b9851985e9354713af67ae0973b7a9", size = 56407, upload-time = "2026-02-06T01:13:17.098Z" },
{ url = "https://files.pythonhosted.org/packages/d8/b9/dc8dba744ec02b16c6fd1abe3fd8ef1b00fd05c72feef5069851b811952f/pydantic_evals-1.62.0-py3-none-any.whl", hash = "sha256:0ca7e10037ed90393c54b6cff41370d6d4bac63f8c878715599c58863c303db1", size = 67341, upload-time = "2026-02-19T05:07:03.83Z" },
]
[[package]]
@@ -5558,6 +5555,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
]
[[package]]
name = "typer-slim"
version = "0.21.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a5/ca/0d9d822fd8a4c7e830cba36a2557b070d4b4a9558a0460377a61f8fb315d/typer_slim-0.21.2.tar.gz", hash = "sha256:78f20d793036a62aaf9c3798306142b08261d4b2a941c6e463081239f062a2f9", size = 120497, upload-time = "2026-02-10T19:33:45.836Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/03/e09325cfc40a33a82b31ba1a3f1d97e85246736856a45a43b19fcb48b1c2/typer_slim-0.21.2-py3-none-any.whl", hash = "sha256:4705082bb6c66c090f60e47c8be09a93158c139ce0aa98df7c6c47e723395e5f", size = 56790, upload-time = "2026-02-10T19:33:47.221Z" },
]
[[package]]
name = "types-orjson"
version = "3.6.2"
@@ -5990,14 +6000,14 @@ wheels = [
[[package]]
name = "werkzeug"
version = "3.1.5"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" }
sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" },
{ url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" },
]
[[package]]