chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
View File
@@ -0,0 +1,241 @@
# This code is adapted from:
# https://github.com/elastic/apm-agent-python/blob/f570e8c2b68a8714628acac815aebcc3518b44c7/elasticapm/contrib/starlette/__init__.py
#
# BSD 3-Clause License
#
# Copyright (c) 2012, the Sentry Team, see AUTHORS for more details
# Copyright (c) 2019, Elasticsearch BV
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
from dataclasses import dataclass
from typing import Dict, List, Optional, Set
from starlette.routing import Match, Mount, Route
from starlette.types import ASGIApp, Scope
try:
# `_IncludedRouter` only exists on FastAPI >= 0.137, where routes registered
# via `include_router` are nested under it instead of being flattened into
# the parent's `routes` list. It is `None` on older versions.
from fastapi.routing import _IncludedRouter
except ImportError: # FastAPI < 0.137
_IncludedRouter = None
def _included_router_prefix(route: "Route") -> str:
"""Return the URL prefix an ``_IncludedRouter`` node applies to its routes.
Accesses ``_IncludedRouter``'s private attributes directly (rather than
defensively) so that a structural change in a future FastAPI version raises
loudly instead of silently returning a wrong route name. Both callers are
wrapped in ``try``/``except`` that fall back to the route prefix and log the
error, so a failure here degrades gracefully while staying visible.
"""
return route.include_context.prefix
@dataclass(frozen=True)
class RoutePattern:
"""Represents a route pattern with optional HTTP method restrictions.
Attributes:
methods: List of HTTP methods (e.g., ["GET", "POST"]), or None if the route
accepts all methods (e.g., WebSocket routes, ASGI apps).
path: The route path pattern (e.g., "/", "/users/{user_id}").
"""
methods: Optional[List[str]]
path: str
def _get_route_name(
scope: Scope, routes: List[Route], *, route_name: Optional[str] = None
) -> Optional[str]:
for route in routes:
if _IncludedRouter is not None and isinstance(route, _IncludedRouter):
# FastAPI >= 0.137: routes added via `include_router` live on
# `route.original_router.routes` with paths relative to the include
# prefix. Unlike `Mount`, `_IncludedRouter.matches()` doesn't return
# a prefix-stripped child scope, so we strip it ourselves and prepend
# the prefix back to the resolved child name. This mirrors the Mount
# handling below and works for both HTTP and WebSocket routes.
prefix = _included_router_prefix(route)
path = scope.get("path", "")
if prefix:
if not path.startswith(prefix):
continue
child_scope = {**scope, "path": path[len(prefix) :]}
else:
child_scope = scope
child_route_name = _get_route_name(
child_scope, route.original_router.routes
)
if child_route_name is not None:
return prefix + child_route_name
continue
match, child_scope = route.matches(scope)
if match == Match.FULL:
route_name = route.path
child_scope = {**scope, **child_scope}
if isinstance(route, Mount) and route.routes:
child_route_name = _get_route_name(
child_scope, route.routes, route_name=route_name
)
if child_route_name is None:
route_name = None
else:
route_name += child_route_name
return route_name
elif match == Match.PARTIAL and route_name is None:
route_name = route.path
return None
def get_asgi_route_name(app: ASGIApp, scope: Scope) -> Optional[str]:
"""Gets route name for given request taking mounts into account."""
routes = app.routes
route_name = _get_route_name(scope, routes)
# Starlette magically redirects requests if the path matches a route name
# with a trailing slash appended or removed. To not spam the transaction
# names list, we do the same here and put these redirects all in the
# same "redirect trailing slashes" transaction name.
if not route_name and app.router.redirect_slashes and scope["path"] != "/":
redirect_scope = dict(scope)
if scope["path"].endswith("/"):
redirect_scope["path"] = scope["path"][:-1]
trim = True
else:
redirect_scope["path"] = scope["path"] + "/"
trim = False
route_name = _get_route_name(redirect_scope, routes)
if route_name is not None:
route_name = route_name + "/" if trim else route_name[:-1]
if route_name:
root_path = scope.get("root_path", "")
if root_path:
route_name = root_path.rstrip("/") + "/" + route_name.lstrip("/")
return route_name
def extract_route_patterns(app: ASGIApp) -> List[RoutePattern]:
"""Extracts all route patterns from an ASGI app.
This function recursively traverses the app's routes (including mounted apps)
and returns a list of all route patterns. This is used to communicate available
routes from build time to proxies for accurate metrics tagging.
Args:
app: The ASGI application (typically FastAPI or Starlette)
Returns:
List of RoutePattern objects. Examples:
- RoutePattern(methods=["GET", "POST"], path="/"): GET and POST to root
- RoutePattern(methods=["GET"], path="/users/{id}"): GET to users endpoint
- RoutePattern(methods=None, path="/websocket"): No method restrictions
"""
# Use a dict to store path -> set of methods mapping
# This allows us to track which methods apply to each path
# Use None as a sentinel value to indicate "no method restrictions"
path_methods: Dict[str, Optional[Set[str]]] = {}
def _extract_from_routes(routes: List[Route], prefix: str = "") -> None:
for route in routes:
if _IncludedRouter is not None and isinstance(route, _IncludedRouter):
# FastAPI >= 0.137: recurse into routes registered via
# `include_router`, applying the include prefix (see
# `_get_route_name` for details).
_extract_from_routes(
route.original_router.routes,
prefix + _included_router_prefix(route),
)
continue
route_path = prefix + route.path
if isinstance(route, Mount):
# Recursively extract patterns from mounted apps
if hasattr(route, "routes") and route.routes:
_extract_from_routes(route.routes, route_path)
else:
# Mount without sub-routes - no method restrictions
if route_path not in path_methods:
path_methods[route_path] = None
else:
# Regular route - extract methods if available
if hasattr(route, "methods") and route.methods:
# Route has specific methods
if route_path not in path_methods:
path_methods[route_path] = set()
# Only add methods if we haven't already marked this path as "all methods"
if path_methods[route_path] is not None:
path_methods[route_path].update(route.methods)
else:
# Route has no method restrictions (accepts all methods)
# Mark this path as accepting all methods (None)
path_methods[route_path] = None
try:
if hasattr(app, "routes"):
_extract_from_routes(app.routes)
# Handle root_path if present
if hasattr(app, "root_path") and app.root_path:
root_path = app.root_path.rstrip("/")
adjusted_path_methods = {}
for path, methods in path_methods.items():
adjusted_path = (
root_path + "/" + path.lstrip("/")
if path != "/"
else root_path + path
)
adjusted_path_methods[adjusted_path] = methods
path_methods = adjusted_path_methods
except Exception:
# If extraction fails for any reason, return empty list
# This shouldn't break the system
return []
# Convert path_methods dict to list of RoutePattern objects
patterns: List[RoutePattern] = []
for path, methods in path_methods.items():
if methods is None:
# No method restrictions
patterns.append(RoutePattern(methods=None, path=path))
else:
# Convert set to sorted list for consistent ordering
methods_list = sorted(methods)
patterns.append(RoutePattern(methods=methods_list, path=path))
# Sort by path for consistent ordering
return sorted(patterns, key=lambda x: x.path)