Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package 
python-prometheus-fastapi-instrumentator for openSUSE:Factory checked in at 
2026-07-28 17:52:45
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-prometheus-fastapi-instrumentator 
(Old)
 and      
/work/SRC/openSUSE:Factory/.python-prometheus-fastapi-instrumentator.new.2004 
(New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-prometheus-fastapi-instrumentator"

Tue Jul 28 17:52:45 2026 rev:2 rq:1367845 version:8.1.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-prometheus-fastapi-instrumentator/python-prometheus-fastapi-instrumentator.changes
        2026-07-14 13:47:05.189163258 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-prometheus-fastapi-instrumentator.new.2004/python-prometheus-fastapi-instrumentator.changes
      2026-07-28 17:53:39.103539447 +0200
@@ -1,0 +2,14 @@
+Sun Jul 26 18:13:36 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to version 8.1.0:
+  * Add the optional should_include_root_path parameter to the
+    Instrumentator constructor; when True the handler label of the
+    exported metrics includes the FastAPI app's effective
+    root_path (defaults to False, so existing metrics are
+    unchanged)
+  * Fix resolving route names when the FastAPI app sets a
+    root_path
+  * Fix incorrect handling of nested apps and routes in corner
+    cases introduced in 8.0.1
+
+-------------------------------------------------------------------

Old:
----
  prometheus_fastapi_instrumentator-8.0.2.tar.gz

New:
----
  prometheus_fastapi_instrumentator-8.1.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-prometheus-fastapi-instrumentator.spec ++++++
--- /var/tmp/diff_new_pack.25X6gR/_old  2026-07-28 17:53:40.519588967 +0200
+++ /var/tmp/diff_new_pack.25X6gR/_new  2026-07-28 17:53:40.519588967 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-prometheus-fastapi-instrumentator
 #
-# Copyright (c) 2026 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-prometheus-fastapi-instrumentator
-Version:        8.0.2
+Version:        8.1.0
 Release:        0
 Summary:        Instrument a FastAPI app with Prometheus metrics
 License:        ISC

++++++ prometheus_fastapi_instrumentator-8.0.2.tar.gz -> 
prometheus_fastapi_instrumentator-8.1.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/prometheus_fastapi_instrumentator-8.0.2/PKG-INFO 
new/prometheus_fastapi_instrumentator-8.1.0/PKG-INFO
--- old/prometheus_fastapi_instrumentator-8.0.2/PKG-INFO        1970-01-01 
01:00:00.000000000 +0100
+++ new/prometheus_fastapi_instrumentator-8.1.0/PKG-INFO        1970-01-01 
01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: prometheus-fastapi-instrumentator
-Version: 8.0.2
+Version: 8.1.0
 Summary: Instrument your FastAPI app with Prometheus metrics
 License: ISC
 License-File: LICENSE
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/prometheus_fastapi_instrumentator-8.0.2/pyproject.toml 
new/prometheus_fastapi_instrumentator-8.1.0/pyproject.toml
--- old/prometheus_fastapi_instrumentator-8.0.2/pyproject.toml  1970-01-01 
01:00:00.000000000 +0100
+++ new/prometheus_fastapi_instrumentator-8.1.0/pyproject.toml  1970-01-01 
01:00:00.000000000 +0100
@@ -4,7 +4,7 @@
 
 [project]
 name = "prometheus-fastapi-instrumentator"
-version = "8.0.2"
+version = "8.1.0"
 description = "Instrument your FastAPI app with Prometheus metrics"
 authors = [
     {name = "Tim Schwenke", email = "[email protected]"}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/__init__.py
 
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/__init__.py
--- 
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/__init__.py
       1970-01-01 01:00:00.000000000 +0100
+++ 
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/__init__.py
       1970-01-01 01:00:00.000000000 +0100
@@ -1,5 +1,5 @@
 from .instrumentation import PrometheusFastApiInstrumentator
 
-__version__ = "8.0.2"
+__version__ = "8.1.0"
 
 Instrumentator = PrometheusFastApiInstrumentator
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/instrumentation.py
 
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/instrumentation.py
--- 
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/instrumentation.py
        1970-01-01 01:00:00.000000000 +0100
+++ 
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/instrumentation.py
        1970-01-01 01:00:00.000000000 +0100
@@ -39,6 +39,7 @@
         should_group_status_codes: bool = True,
         should_ignore_untemplated: bool = False,
         should_group_untemplated: bool = True,
+        should_include_root_path: bool = False,
         should_round_latency_decimals: bool = False,
         should_respect_env_var: bool = False,
         should_instrument_requests_inprogress: bool = False,
@@ -65,6 +66,10 @@
             should_group_untemplated (bool): Should requests without a matching
                 template be grouped to handler `none`? Defaults to `True`.
 
+            should_include_root_path (bool): Should resolved handler templates
+                include the application's effective `root_path`? Defaults to
+                `False`.
+
             should_round_latency_decimals: Should recorded latencies be
                 rounded to a certain number of decimals?
 
@@ -123,6 +128,7 @@
         self.should_group_status_codes = should_group_status_codes
         self.should_ignore_untemplated = should_ignore_untemplated
         self.should_group_untemplated = should_group_untemplated
+        self.should_include_root_path = should_include_root_path
         self.should_round_latency_decimals = should_round_latency_decimals
         self.should_respect_env_var = should_respect_env_var
         self.should_instrument_requests_inprogress = 
should_instrument_requests_inprogress
@@ -217,6 +223,7 @@
             should_group_status_codes=self.should_group_status_codes,
             should_ignore_untemplated=self.should_ignore_untemplated,
             should_group_untemplated=self.should_group_untemplated,
+            should_include_root_path=self.should_include_root_path,
             should_round_latency_decimals=self.should_round_latency_decimals,
             should_respect_env_var=self.should_respect_env_var,
             
should_instrument_requests_inprogress=self.should_instrument_requests_inprogress,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/middleware.py
 
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/middleware.py
--- 
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/middleware.py
     1970-01-01 01:00:00.000000000 +0100
+++ 
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/middleware.py
     1970-01-01 01:00:00.000000000 +0100
@@ -16,6 +16,8 @@
 
 
 class PrometheusInstrumentatorMiddleware:
+    """ASGI middleware that records Prometheus metrics for HTTP requests."""
+
     def __init__(
         self,
         app: ASGIApp,
@@ -23,6 +25,7 @@
         should_group_status_codes: bool = True,
         should_ignore_untemplated: bool = False,
         should_group_untemplated: bool = True,
+        should_include_root_path: bool = False,
         should_round_latency_decimals: bool = False,
         should_respect_env_var: bool = False,
         should_instrument_requests_inprogress: bool = False,
@@ -70,6 +73,7 @@
         self.should_group_status_codes = should_group_status_codes
         self.should_ignore_untemplated = should_ignore_untemplated
         self.should_group_untemplated = should_group_untemplated
+        self.should_include_root_path = should_include_root_path
         self.should_round_latency_decimals = should_round_latency_decimals
         self.should_respect_env_var = should_respect_env_var
         self.should_instrument_requests_inprogress = 
should_instrument_requests_inprogress
@@ -235,9 +239,12 @@
         Returns:
             Tuple[str, bool]: Tuple with two elements. First element is either
                 template or if no template the path. Second element tells you
-                if the path is templated or not.
+                if the path is templated or not. When configured, the template
+                includes the application's effective `root_path`.
         """
-        route_name = routing.get_route_name(request)
+        route_name = routing.get_route_name(
+            request, should_include_root_path=self.should_include_root_path
+        )
         return route_name or request.url.path, True if route_name else False
 
     def _is_handler_excluded(self, handler: str, is_templated: bool) -> bool:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/routing.py
 
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/routing.py
--- 
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/routing.py
        1970-01-01 01:00:00.000000000 +0100
+++ 
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/routing.py
        1970-01-01 01:00:00.000000000 +0100
@@ -1,179 +1,135 @@
-# 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
-
-"""Helper module for routing.
-
-The two functions in this module are licensed under the BSD 3-Clause License
-instead of the ISC License like the rest of the project. Therefore the code
-is contained in a dedicated module.
-
-Based on code from 
[elastic/apm-agent-python](https://github.com/elastic/apm-agent-python/blob/527f62c0c50842f94ef90fda079853372539319a/elasticapm/contrib/starlette/__init__.py).
-"""
-
-from typing import List, Optional
+from typing import Any, List, Optional
 
 from starlette.requests import HTTPConnection
-from starlette.routing import Match, Mount, Route
+from starlette.routing import BaseRoute, Match, Mount
 from starlette.types import Scope
 
 
-def _resolve_path(route: Route) -> Optional[str]:
-    """Return the request path contributed by ``route``, or ``None`` if the
-    route is a router-like wrapper whose own routes must be traversed.
-
-    FastAPI 0.116+ (officially 0.137) wraps routers registered via
-    ``app.include_router`` in an internal ``_IncludedRouter`` class that
-    does not expose a ``path`` attribute. The configured mount path is
-    available on ``include_context.prefix``.
+def _effective_routes(routes: List[BaseRoute]) -> List[BaseRoute]:
+    """Flattens a list of routes into directly matchable routes.
+
+    FastAPI (>= 0.116) represents routers registered via `include_router`
+    with an internal `_IncludedRouter` object that has no `path` attribute
+    and does not expose its children via the `matches` API. Such routers do
+    expose `effective_route_contexts()` though, which yields context objects
+    whose resolved route already carries the fully prefixed `path`. Every
+    other route is passed through unchanged.
     """
 
-    if hasattr(route, "path"):
-        return route.path
-    include_context = getattr(route, "include_context", None)
-    if include_context is not None:
-        # An empty prefix means the wrapper contributes no path segment of
-        # its own (e.g. ``APIRouter(prefix=...)`` registered via
-        # ``include_router`` without an extra ``prefix=`` argument). The
-        # caller must still recurse into nested routes.
-        prefix = getattr(include_context, "prefix", "") or ""
-        return prefix
-    return None
+    effective: List[BaseRoute] = []
+    for route in routes:
+        contexts: Any = getattr(route, "effective_route_contexts", None)
+        if callable(contexts):
+            resolved_contexts: Any = contexts()
+            for context in resolved_contexts:
+                # Mounts must be recursed into, so the mount object (which
+                # carries the fully prefixed path and its sub-routes) is used.
+                # Plain Starlette routes expose their fully prefixed path via
+                # `starlette_route`. FastAPI routes keep `starlette_route`
+                # unset but the context itself is matchable and carries the
+                # fully prefixed `path`.
+                starlette_route = getattr(context, "starlette_route", None)
+                if starlette_route is not None:
+                    effective.append(starlette_route)
+                else:
+                    effective.append(context)
+        else:
+            effective.append(route)
+    return effective
 
 
-def _child_routes(route: Route) -> Optional[List[Route]]:
-    """Return nested routes for router-like route objects, else ``None``."""
+def _get_route_name(scope: Scope, routes: List[BaseRoute]) -> Optional[str]:
+    """Resolves the templated handler for a scope against a list of routes.
 
-    if isinstance(route, Mount):
-        return route.routes or None
-    original_router = getattr(route, "original_router", None)
-    if original_router is not None and hasattr(original_router, "routes"):
-        nested = list(original_router.routes)
-        return nested or None
-    return None
+    Matching relies on Starlette's own `matches` implementation, which
+    resolves the path relative to `scope["root_path"]`. Mounts are recursed
+    into with the child scope so their prefix is prepended to the result. A
+    full match wins immediately, a partial match (e.g. path matches but method
+    does not) is remembered as a fallback.
+    """
 
+    partial: Optional[str] = None
 
-def _strip_prefix_from_scope(scope: Scope, prefix: str) -> Scope:
-    """Return a copy of ``scope`` with the mount ``prefix`` removed from
-    ``path``.
-
-    ``starlette.routing.Mount.matches`` returns a child scope with the
-    mount prefix already stripped. FastAPI's ``_IncludedRouter`` does
-    not, so the recursion into the included router's own routes would
-    never match a leaf endpoint. Stripping the prefix here restores the
-    behaviour expected by the recursive call.
-    """
+    for route in _effective_routes(routes):
+        match, child_scope = route.matches(scope)
+        if match == Match.NONE:
+            continue
 
-    if not prefix:
-        return scope
-    path = scope.get("path", "") or ""
-    if path == prefix:
-        return {**scope, "path": ""}
-    if path.startswith(prefix + "/"):
-        return {**scope, "path": path.removeprefix(prefix)}
-    return scope
+        if isinstance(route, Mount):
+            sub_routes = route.routes
+            if not sub_routes:
+                # Opaque mounted ASGI app without introspectable sub-routes.
+                # The mount path itself is the most specific handler available.
+                name = route.path
+            else:
+                child = _get_route_name({**scope, **child_scope}, sub_routes)
+                if child is None:
+                    continue
+                name = route.path + child
+        else:
+            path = getattr(route, "path", None)
+            if path is None:
+                continue
+            name = path
 
+        if match == Match.FULL:
+            return name
+        if partial is None:
+            partial = name
 
-def _get_route_name(
-    scope: Scope, routes: List[Route], route_name: Optional[str] = None
-) -> Optional[str]:
-    """Gets route name for given scope taking mounts into account.
+    return partial
 
-    Supports plain ``Route``/``Mount`` objects as well as FastAPI's
-    internal ``_IncludedRouter`` wrapper produced by
-    ``app.include_router(...)``. When a matched route is a router-like
-    object, the function recurses into its nested routes so the final
-    label reflects the leaf endpoint.
-    """
 
-    for route in routes:
-        match, child_scope = route.matches(scope)
-        if match == Match.FULL:
-            resolved = _resolve_path(route)
-            if resolved is None:
-                # Cannot produce a stable label for this route; try the
-                # next candidate. Callers fall back to ``request.url.path``
-                # if no route yields a name.
-                continue
-            route_name = resolved
-            child_scope = {**scope, **child_scope}
-            children = _child_routes(route)
-            if children:
-                # FastAPI's ``_IncludedRouter`` does not strip the mount
-                # prefix from the scope before matching nested routes,
-                # unlike ``starlette.routing.Mount``. Strip it here so
-                # the leaf endpoint inside the included router can match.
-                if not isinstance(route, Mount):
-                    include_context = getattr(route, "include_context", None)
-                    if include_context is not None:
-                        prefix = getattr(include_context, "prefix", "") or ""
-                        if prefix:
-                            child_scope = 
_strip_prefix_from_scope(child_scope, prefix)
-                child_route_name = _get_route_name(child_scope, children)
-                if child_route_name is not None:
-                    # Concatenate the leaf path onto the parent path so the
-                    # final label is e.g. ``/api/v1/items/{item_id}`` rather
-                    # than just ``/{item_id}``.
-                    route_name = route_name + child_route_name
-                else:
-                    route_name = None
-            return route_name
-        elif match == Match.PARTIAL and route_name is None:
-            resolved = _resolve_path(route)
-            if resolved is not None:
-                route_name = resolved
-    return route_name
+def get_route_name(
+    request: HTTPConnection, should_include_root_path: bool
+) -> Optional[str]:
+    """Gets route name.
 
+    Resolves the templated route for the given request, e.g. `/api/items/123`
+    maps to `/api/items/{item_id}`. Returns `None` when no route matches.
 
-def get_route_name(request: HTTPConnection) -> Optional[str]:
-    """Gets route name for given request taking mounts into account."""
+    Works with both plain Starlette and FastAPI applications and supports
+    router prefixes, `include_router` (including nested routers), mounted
+    sub-applications and `root_path`. When the application itself is a mounted
+    sub-application that is instrumented directly, the resolved name is 
relative
+    to that sub-application.
+
+    Args:
+        request: Connection whose scope should be resolved to a route name.
+        should_include_root_path: Whether the returned route name should be
+            prefixed with the application's effective `root_path`.
+    """
 
-    app = request.app
     scope = request.scope
-    routes = app.routes
-    route_name = _get_route_name(scope, routes)
+    app = scope.get("app")
+    routes = getattr(app, "routes", None)
+    if not routes:
+        return None
+
+    name = _get_route_name(scope, routes)
+
+    if name is not None:
+        if not should_include_root_path:
+            return name
+
+        # Only the application's own `root_path` is prepended. A `root_path`
+        # injected into the scope by a parent mount (i.e. when a mounted
+        # sub-application is instrumented directly) is intentionally ignored so
+        # the resolved name stays relative to that sub-application.
+        root_path = getattr(app, "root_path", "") or ""
+        if root_path:
+            return root_path.rstrip("/") + name
+        return name
+
+    # No route matched. If the router redirects on trailing slashes, a request
+    # like `/items` against a route `/items/` (or vice versa) does not match
+    # directly but would be redirected. Report the requested path in that case.
+    router = getattr(app, "router", None)
+    if router is not None and getattr(router, "redirect_slashes", False):
+        path = scope.get("path", "")
+        toggled = path[:-1] if path.endswith("/") else path + "/"
+        if toggled and toggled != path:
+            if _get_route_name({**scope, "path": toggled}, routes) is not None:
+                return path
 
-    # 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.rstrip("/")
-            route_name = route_name + "/" if trim else route_name
-    return route_name
+    return None

Reply via email to