Compare commits

...

7 Commits

Author SHA1 Message Date
Sebastián Ramírez
6a0ba7bb1f 🔖 Release version 0.137.2 (#15790)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-18 06:56:45 +00:00
github-actions[bot]
5d421ae977 📝 Update release notes
[skip ci]
2026-06-18 06:50:13 +00:00
Sebastián Ramírez
6ac122071d Add iter_route_contexts() for advanced use cases that used to use router.routes (e.g. Jupyverse) (#15785) 2026-06-18 08:49:38 +02:00
github-actions[bot]
7feb17f80a 📝 Update release notes
[skip ci]
2026-06-17 20:50:29 +00:00
Sebastián Ramírez
d514109e42 🔧 Update sponsors: add BairesDev (#15787) 2026-06-17 22:50:01 +02:00
github-actions[bot]
1c1edb9b55 📝 Update release notes
[skip ci]
2026-06-17 20:36:51 +00:00
Sebastián Ramírez
32d711f6d2 🔨 Update sponsors script to simplify previews (#15786) 2026-06-17 22:36:24 +02:00
9 changed files with 223 additions and 37 deletions

View File

@@ -68,6 +68,7 @@ The key features are:
<a href="https://www.permit.io/blog/implement-authorization-in-fastapi?utm_source=github&utm_medium=referral&utm_campaign=fastapi" target="_blank" title="Fine-Grained Authorization for FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/permit.png"></a>
<a href="https://dribia.com/en/" target="_blank" title="Dribia - Data Science within your reach"><img src="https://fastapi.tiangolo.com/img/sponsors/dribia.png"></a>
<a href="https://www.rapidproxy.io/?ref=fastapi" target="_blank" title="Try RapidProxy for free - Residential Proxies with 90M+ Global IPs. Starting from $0.65/GB for web scraping, automation, and data collection."><img src="https://fastapi.tiangolo.com/img/sponsors/rapidproxy.png"></a>
<a href="https://www.bairesdev.com/" target="_blank" title="BairesDev | Nearshore Software Development & Staff Augmentation Company"><img src="https://fastapi.tiangolo.com/img/sponsors/bairesdev.svg"></a>
<!-- /sponsors -->

View File

@@ -1,52 +1,55 @@
keystone:
- url: https://fastapicloud.com
title: FastAPI Cloud. By the same team behind FastAPI. You code. We Cloud.
img: https://fastapi.tiangolo.com/img/sponsors/fastapicloud.png
img: /img/sponsors/fastapicloud.png
gold:
- url: https://blockbee.io?ref=fastapi
title: BlockBee Cryptocurrency Payment Gateway
img: https://fastapi.tiangolo.com/img/sponsors/blockbee.png
img: /img/sponsors/blockbee.png
- url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge
title: Auth, user management and more for your B2B product
img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png
img: /img/sponsors/propelauth.png
- url: https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi
title: Deploy & scale any full-stack web app on Render. Focus on building apps, not infra.
img: https://fastapi.tiangolo.com/img/sponsors/render.svg
img: /img/sponsors/render.svg
- url: https://www.coderabbit.ai/?utm_source=fastapi&utm_medium=badge&utm_campaign=fastapi
title: Cut Code Review Time & Bugs in Half with CodeRabbit
img: https://fastapi.tiangolo.com/img/sponsors/coderabbit.png
img: /img/sponsors/coderabbit.png
- url: https://subtotal.com/?utm_source=fastapi&utm_medium=sponsorship&utm_campaign=open-source
title: The Gold Standard in Retail Account Linking
img: https://fastapi.tiangolo.com/img/sponsors/subtotal.svg
img: /img/sponsors/subtotal.svg
- url: https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi
title: Deploy enterprise applications at startup speed
img: https://fastapi.tiangolo.com/img/sponsors/railway.png
img: /img/sponsors/railway.png
- url: https://serpapi.com/?utm_source=fastapi_website
title: "SerpApi: Web Search API"
img: https://fastapi.tiangolo.com/img/sponsors/serpapi.png
img: /img/sponsors/serpapi.png
- url: https://www.greptile.com/?utm_source=fastapi&utm_medium=sponsorship&utm_campaign=fastapi_sponsor_page
title: "Greptile: The AI Code Reviewer"
img: https://fastapi.tiangolo.com/img/sponsors/greptile.png
img: /img/sponsors/greptile.png
silver:
- url: https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display
title: Pay as you go for market data
img: https://fastapi.tiangolo.com/img/sponsors/databento.svg
img: /img/sponsors/databento.svg
- url: https://www.svix.com/
title: Svix - Webhooks as a service
img: https://fastapi.tiangolo.com/img/sponsors/svix.svg
img: /img/sponsors/svix.svg
- url: https://www.stainlessapi.com/?utm_source=fastapi&utm_medium=referral
title: Stainless | Generate best-in-class SDKs
img: https://fastapi.tiangolo.com/img/sponsors/stainless.png
img: /img/sponsors/stainless.png
- url: https://www.permit.io/blog/implement-authorization-in-fastapi?utm_source=github&utm_medium=referral&utm_campaign=fastapi
title: Fine-Grained Authorization for FastAPI
img: https://fastapi.tiangolo.com/img/sponsors/permit.png
img: /img/sponsors/permit.png
- url: https://dribia.com/en/
title: Dribia - Data Science within your reach
img: https://fastapi.tiangolo.com/img/sponsors/dribia.png
img: /img/sponsors/dribia.png
- url: https://www.rapidproxy.io/?ref=fastapi
title: Try RapidProxy for free - Residential Proxies with 90M+ Global IPs. Starting from $0.65/GB for web scraping, automation, and data collection.
img: https://fastapi.tiangolo.com/img/sponsors/rapidproxy.png
img: /img/sponsors/rapidproxy.png
- url: https://www.bairesdev.com/
title: "BairesDev | Nearshore Software Development & Staff Augmentation Company"
img: /img/sponsors/bairesdev.svg
bronze:
# - url: https://testdriven.io/courses/tdd-fastapi/
# title: Learn to build high-quality web apps with best practices
# img: https://fastapi.tiangolo.com/img/sponsors/testdriven.svg
# img: /img/sponsors/testdriven.svg

View File

@@ -0,0 +1,16 @@
<svg width="240" height="100" viewBox="0 0 240 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="240" height="100" fill="#0B1020"/>
<g transform="translate(22.1 35.15) scale(1.1)">
<path d="M16.7799 0.19017C23.9999 0.19017 29.85 6.19016 29.85 13.5902C29.85 20.9902 23.9999 26.9902 16.7799 26.9902V0.180176V0.19017Z" fill="#F66135"/>
<path d="M7.52991 0C14.4399 2.13 18.3599 9.61 16.2799 16.7C14.1999 23.79 6.91 27.8 0 25.67L7.52991 0Z" fill="#F66135"/>
<path d="M45.0299 20.8301C46.9999 20.8301 48.3899 19.8401 48.3899 17.6901C48.3899 15.5401 46.7599 14.6101 44.9099 14.6101H41.46V20.8301H45.0299ZM44.9399 11.5301C46.7799 11.5301 47.9299 10.3801 47.9299 8.70007C47.9299 7.02007 46.75 6.00008 45 6.00008H41.46V11.5301H44.9399ZM45.4199 2.58008C49.5299 2.58008 51.9199 5.03008 51.9199 8.30008C51.9199 10.2901 50.9499 11.9101 49.4399 12.8101V13.0001C50.8299 13.5901 52.62 15.1401 52.62 18.0301C52.62 22.0401 49.5299 24.3401 46.0299 24.3401H37.59V2.58008H45.4299H45.4199Z" fill="white"/>
<path d="M66.7799 16.1998C66.7799 13.1198 64.96 11.2598 62.48 11.2598C60 11.2598 58.24 13.3098 58.24 16.1998C58.24 19.0898 59.96 21.1098 62.5 21.1098C65.04 21.1098 66.7599 18.9298 66.7599 16.1998M54.25 16.2598C54.25 11.1298 57.28 7.58984 61.63 7.58984C64.38 7.58984 65.92 9.26985 66.47 10.1098H66.6799V7.99985H70.61V24.3499H66.74V22.2699H66.5299C66.0799 22.9499 64.65 24.7598 61.75 24.7598C57.3 24.7598 54.25 21.3398 54.25 16.2798V16.2598Z" fill="white"/>
<path d="M73.8899 7.99013H77.8199V24.3401H73.8899V7.99013ZM73.4299 3.45012C73.4299 2.05012 74.43 1.12012 75.85 1.12012C77.27 1.12012 78.2699 2.05012 78.2699 3.45012C78.2699 4.85012 77.27 5.81012 75.85 5.81012C74.43 5.81012 73.4299 4.91012 73.4299 3.45012Z" fill="white"/>
<path d="M81.08 7.99017H84.7999V10.6602H85.0399C85.5599 9.39017 87.01 7.93018 89.21 7.93018H90.6599V11.7802H89.0299C86.5199 11.7802 85.0099 13.6802 85.0099 16.6002V24.3402H81.08V7.99017Z" fill="white"/>
<path d="M103.92 14.4899C103.74 12.2799 102.08 10.8199 99.7799 10.8199C97.4799 10.8199 95.8799 12.4399 95.6399 14.4899H103.93H103.92ZM91.7599 16.2598C91.7599 11.0998 95.1199 7.58984 99.7799 7.58984C104.89 7.58984 107.67 11.4799 107.67 16.0699V17.3499H95.5699C95.6899 19.8399 97.3499 21.5098 99.8999 21.5098C101.84 21.5098 103.32 20.5799 103.86 19.2399H107.52C106.73 22.5699 103.89 24.7399 99.7799 24.7399C95.0899 24.7399 91.7599 21.1298 91.7599 16.2598Z" fill="white"/>
<path d="M109.49 19.4299H113.12C113.27 20.8299 114.36 21.5699 116.15 21.5699C117.94 21.5699 118.99 20.7598 118.99 19.6098C118.99 16.1298 109.86 19.4198 109.86 12.5898C109.86 9.81984 112.19 7.58984 116.12 7.58984C119.57 7.58984 122.14 9.38985 122.38 12.6599H118.9C118.72 11.4799 117.81 10.6998 116.09 10.6998C114.49 10.6998 113.46 11.4098 113.46 12.4698C113.46 15.5798 122.81 12.3198 122.81 19.3998C122.81 22.5098 120.36 24.7498 116.1 24.7498C111.84 24.7498 109.66 22.6698 109.51 19.4398" fill="white"/>
<path d="M132.8 20.7301C136.52 20.7301 139.58 18.4901 139.58 13.3901C139.58 8.29008 136.55 6.18008 132.8 6.18008H129.84V20.7301H132.8ZM125.82 2.58008H132.96C139.71 2.58008 143.76 7.05007 143.76 13.4001C143.76 20.1401 139.71 24.3401 132.96 24.3401H125.82V2.58008Z" fill="white"/>
<path d="M157.7 14.4899C157.52 12.2799 155.86 10.8199 153.56 10.8199C151.26 10.8199 149.66 12.4399 149.42 14.4899H157.71H157.7ZM145.54 16.2598C145.54 11.0998 148.9 7.58984 153.56 7.58984C158.67 7.58984 161.45 11.4799 161.45 16.0699V17.3499H149.35C149.47 19.8399 151.13 21.5098 153.68 21.5098C155.62 21.5098 157.1 20.5799 157.64 19.2399H161.3C160.51 22.5699 157.67 24.7399 153.56 24.7399C148.87 24.7399 145.54 21.1298 145.54 16.2598Z" fill="white"/>
<path d="M161.9 7.99023H166.02L169.8 19.5202H170.04L173.79 7.99023H177.99L172.16 24.3402H167.71L161.9 7.99023Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -7,6 +7,12 @@ hide:
## Latest Changes
## 0.137.2 (2026-06-18)
### Features
* ✨ Add `iter_route_contexts()` for advanced use cases that used to use `router.routes` (e.g. Jupyverse). PR [#15785](https://github.com/fastapi/fastapi/pull/15785) by [@tiangolo](https://github.com/tiangolo).
### Translations
* 🌐 Fix broken Markdown in Korean custom response docs. PR [#15774](https://github.com/fastapi/fastapi/pull/15774) by [@kooqooo](https://github.com/kooqooo).
@@ -24,6 +30,8 @@ hide:
### Internal
* 🔧 Update sponsors: add BairesDev. PR [#15787](https://github.com/fastapi/fastapi/pull/15787) by [@tiangolo](https://github.com/tiangolo).
* 🔨 Update sponsors script to simplify previews. PR [#15786](https://github.com/fastapi/fastapi/pull/15786) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump the python-packages group across 1 directory with 7 updates. PR [#15777](https://github.com/fastapi/fastapi/pull/15777) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump cryptography from 46.0.7 to 48.0.1. PR [#15779](https://github.com/fastapi/fastapi/pull/15779) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump aiohttp from 3.14.0 to 3.14.1. PR [#15781](https://github.com/fastapi/fastapi/pull/15781) by [@dependabot[bot]](https://github.com/apps/dependabot).

View File

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

View File

@@ -479,26 +479,22 @@ def get_openapi_path(
def _get_api_route_for_openapi(
route: BaseRoute, route_context: routing._EffectiveRouteContext | None
route_context: routing.RouteContext,
) -> routing._APIRouteLike | None:
if route_context is not None and isinstance(
route_context.original_route, routing.APIRoute
):
if isinstance(route_context.original_route, routing.APIRoute):
return cast(routing._APIRouteLike, route_context)
if isinstance(route, routing.APIRoute):
return cast(routing._APIRouteLike, route)
return None
def get_fields_from_routes(
routes: Sequence[BaseRoute],
routes: Sequence[BaseRoute | routing.RouteContext],
) -> list[ModelField]:
body_fields_from_routes: list[ModelField] = []
responses_from_routes: list[ModelField] = []
request_fields_from_routes: list[ModelField] = []
callback_flat_models: list[ModelField] = []
for route, route_context in routing._iter_routes_with_context(routes):
api_route = _get_api_route_for_openapi(route, route_context)
for route_context in routing.iter_route_contexts(routes):
api_route = _get_api_route_for_openapi(route_context)
if api_route is None:
continue
if api_route.include_in_schema:
@@ -531,8 +527,8 @@ def get_openapi(
openapi_version: str = "3.1.0",
summary: str | None = None,
description: str | None = None,
routes: Sequence[BaseRoute],
webhooks: Sequence[BaseRoute] | None = None,
routes: Sequence[BaseRoute | routing.RouteContext],
webhooks: Sequence[BaseRoute | routing.RouteContext] | None = None,
tags: list[dict[str, Any]] | None = None,
servers: list[dict[str, str | Any]] | None = None,
terms_of_service: str | None = None,
@@ -567,8 +563,8 @@ def get_openapi(
model_name_map=model_name_map,
separate_input_output_schemas=separate_input_output_schemas,
)
for route, route_context in routing._iter_routes_with_context(routes):
api_route = _get_api_route_for_openapi(route, route_context)
for route_context in routing.iter_route_contexts(routes):
api_route = _get_api_route_for_openapi(route_context)
if api_route is not None:
result = get_openapi_path(
route=api_route,
@@ -587,8 +583,8 @@ def get_openapi(
)
if path_definitions:
definitions.update(path_definitions)
for webhook, webhook_context in routing._iter_routes_with_context(webhooks or []):
api_webhook = _get_api_route_for_openapi(webhook, webhook_context)
for webhook_context in routing.iter_route_contexts(webhooks or []):
api_webhook = _get_api_route_for_openapi(webhook_context)
if api_webhook is not None:
result = get_openapi_path(
route=api_webhook,

View File

@@ -1454,6 +1454,47 @@ class _EffectiveRouteContext:
return URLPath(path=path, protocol="http")
@dataclass(frozen=True)
class RouteContext:
route: BaseRoute
_route_context: _EffectiveRouteContext | None = field(default=None, repr=False)
@property
def original_route(self) -> BaseRoute:
if self._route_context is not None:
return self._route_context.original_route
return self.route
@property
def _effective_route(self) -> BaseRoute | _EffectiveRouteContext:
if self._route_context is not None:
return self._route_context
return self.route
@property
def path(self) -> str | None:
return getattr(self._effective_route, "path", None)
@property
def path_format(self) -> str | None:
return getattr(self._effective_route, "path_format", None)
@property
def name(self) -> str | None:
return getattr(self._effective_route, "name", None)
@property
def methods(self) -> set[str] | None:
return getattr(self._effective_route, "methods", None)
@property
def endpoint(self) -> Callable[..., Any] | None:
return getattr(self._effective_route, "endpoint", None)
def __getattr__(self, name: str) -> Any:
return getattr(self._effective_route, name)
@dataclass
class _IncludedRouter(BaseRoute):
original_router: "APIRouter"
@@ -1654,6 +1695,20 @@ def _iter_included_route_candidates(routes: Sequence[BaseRoute]) -> Iterator[Bas
yield route
def iter_route_contexts(
routes: Sequence[BaseRoute | RouteContext],
) -> Iterator[RouteContext]:
for route in routes:
if isinstance(route, RouteContext):
yield route
continue
for original_route, route_context in _iter_routes_with_context([route]):
if route_context is None:
yield RouteContext(original_route)
else:
yield RouteContext(original_route, route_context)
def _iter_routes_with_context(
routes: Sequence[BaseRoute],
) -> Iterator[tuple[BaseRoute, _EffectiveRouteContext | None]]:

View File

@@ -311,22 +311,26 @@ index_sponsors_template = """
### Keystone Sponsor
{% for sponsor in sponsors.keystone -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a>
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor_img_url(sponsor.img) }}"></a>
{% endfor %}
### Gold Sponsors
{% for sponsor in sponsors.gold -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a>
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor_img_url(sponsor.img) }}"></a>
{% endfor %}
### Silver Sponsors
{% for sponsor in sponsors.silver -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a>
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor_img_url(sponsor.img) }}"></a>
{% endfor %}
"""
def sponsor_img_url(img: str) -> str:
return f"https://fastapi.tiangolo.com{img}"
def remove_header_permalinks(content: str):
lines: list[str] = []
for line in content.split("\n"):
@@ -355,7 +359,7 @@ def generate_readme_content() -> str:
pre_end = match_start.end()
post_start = match_end.start()
template = Template(index_sponsors_template)
message = template.render(sponsors=sponsors)
message = template.render(sponsors=sponsors, sponsor_img_url=sponsor_img_url)
pre_content = content[frontmatter_end:pre_end]
post_content = content[post_start:]
new_content = pre_content + message + post_content

View File

@@ -1,16 +1,21 @@
from typing import Annotated, cast
import pytest
from fastapi import APIRouter, Body, Depends, FastAPI, Request
from fastapi import APIRouter, Body, Depends, FastAPI, Request, Security
from fastapi.exceptions import FastAPIError
from fastapi.openapi.utils import get_openapi
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from fastapi.routing import (
APIRoute,
RouteContext,
_IncludedRouter,
_iter_included_route_candidates,
_restore_fastapi_scope_key,
iter_route_contexts,
)
from fastapi.security import HTTPBearer
from fastapi.testclient import TestClient
from pydantic import BaseModel
from starlette.routing import BaseRoute, Host, Match, Mount, NoMatchFound, Route, Router
@@ -30,6 +35,104 @@ def unique_id_b(route: APIRoute) -> str:
return f"b_{route.name}"
def test_iter_route_contexts_returns_direct_route_context():
router = APIRouter()
@router.get("/items/{item_id}")
def read_item(item_id: str): # pragma: no cover
return {"item_id": item_id}
contexts = list(iter_route_contexts(router.routes))
assert len(contexts) == 1
assert isinstance(contexts[0], RouteContext)
assert contexts[0].original_route is router.routes[0]
assert contexts[0].path == "/items/{item_id}"
assert contexts[0].path_format == "/items/{item_id}"
assert contexts[0].methods == {"GET"}
assert contexts[0].endpoint is read_item
def test_iter_route_contexts_supports_nested_conflict_detection():
existing_router = APIRouter()
nested_router = APIRouter()
@nested_router.get("/{username}")
def read_user(username: str): # pragma: no cover
return {"username": username}
existing_router.include_router(nested_router, prefix="/auth/user")
new_router = APIRouter()
@new_router.get("/auth/user/{username}")
def read_user_again(username: str): # pragma: no cover
return {"username": username}
existing_paths = {
context.path for context in iter_route_contexts(existing_router.routes)
}
new_paths = {context.path for context in iter_route_contexts(new_router.routes)}
assert existing_paths & new_paths == {"/auth/user/{username}"}
def test_get_openapi_accepts_filtered_route_contexts_with_effective_paths():
router = APIRouter()
bearer_scheme = HTTPBearer()
@router.get("/public", tags=["public"])
def read_public(token: Annotated[str, Security(bearer_scheme)]): # pragma: no cover
return {"public": True}
@router.get("/private", tags=["private"])
def read_private(): # pragma: no cover
return {"private": True}
app = FastAPI()
app.include_router(router, prefix="/api")
public_routes = [
context
for context in iter_route_contexts(app.routes)
if "public" in getattr(context, "tags", [])
]
schema = get_openapi(
title="Public API",
version="1.0.0",
routes=public_routes,
)
assert set(schema["paths"]) == {"/api/public"}
assert "HTTPBearer" in schema["components"]["securitySchemes"]
def test_get_openapi_accepts_webhook_route_contexts():
app = FastAPI()
bearer_scheme = HTTPBearer()
class Subscription(BaseModel):
username: str
@app.webhooks.post("new-subscription")
def new_subscription(
body: Subscription, token: Annotated[str, Security(bearer_scheme)]
): # pragma: no cover
return None
webhook_contexts = list(iter_route_contexts(app.webhooks.routes))
schema = get_openapi(
title="Webhook API",
version="1.0.0",
routes=[],
webhooks=webhook_contexts,
)
assert set(schema["webhooks"]) == {"new-subscription"}
assert "HTTPBearer" in schema["components"]["securitySchemes"]
assert "Subscription" in schema["components"]["schemas"]
def test_router_include_context_matches_flattened_include_metadata():
callback_router = APIRouter()