mirror of
https://github.com/fastapi/fastapi.git
synced 2026-06-18 04:19:50 -04:00
Compare commits
7 Commits
fix-gramma
...
0.137.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a0ba7bb1f | ||
|
|
5d421ae977 | ||
|
|
6ac122071d | ||
|
|
7feb17f80a | ||
|
|
d514109e42 | ||
|
|
1c1edb9b55 | ||
|
|
32d711f6d2 |
@@ -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 -->
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
16
docs/en/docs/img/sponsors/bairesdev.svg
Normal file
16
docs/en/docs/img/sponsors/bairesdev.svg
Normal 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 |
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user