Coverage for /usr/local/lib/python3.12/site-packages/prefect/server/utilities/server.py: 60%
41 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 02:04 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 02:04 +0000
1"""
2Utilities for the Prefect REST API server.
3"""
5from collections.abc import Coroutine, Sequence
6from contextlib import AsyncExitStack
7from typing import TYPE_CHECKING, Any, Callable, get_type_hints
9from fastapi import APIRouter, Request, Response, status
10from fastapi.routing import APIRoute
11from starlette.routing import BaseRoute
14def method_paths_from_routes(routes: Sequence[BaseRoute]) -> set[str]:
15 """
16 Generate a set of strings describing the given routes in the format: <method> <path>
18 For example, "GET /logs/"
19 """
20 method_paths: set[str] = set()
21 for route in routes:
22 effective_route_contexts = getattr(route, "effective_route_contexts", None)
23 route_candidates = (
24 effective_route_contexts()
25 if callable(effective_route_contexts)
26 else (route,)
27 )
29 for route_candidate in route_candidates:
30 path = getattr(route_candidate, "path", None)
31 methods = getattr(route_candidate, "methods", None)
32 if path is None or methods is None:
33 continue
35 for method in methods:
36 method_paths.add(f"{method} {path}")
38 return method_paths
41class PrefectAPIRoute(APIRoute):
42 """
43 A FastAPIRoute class which attaches an async stack to requests that exits before
44 a response is returned.
46 Requests already have `request.scope['fastapi_astack']` which is an async stack for
47 the full scope of the request. This stack is used for managing contexts of FastAPI
48 dependencies. If we want to close a dependency before the request is complete
49 (i.e. before returning a response to the user), we need a stack with a different
50 scope. This extension adds this stack at `request.state.response_scoped_stack`.
51 """
53 def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
54 default_handler = super().get_route_handler()
56 async def handle_response_scoped_depends(request: Request) -> Response:
57 # Create a new stack scoped to exit before the response is returned
58 response = None
59 async with AsyncExitStack() as stack:
60 request.state.response_scoped_stack = stack
61 response = await default_handler(request)
63 if TYPE_CHECKING: 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true
64 assert response is not None
65 return response
67 return handle_response_scoped_depends
70class PrefectRouter(APIRouter):
71 """
72 A base class for Prefect REST API routers.
73 """
75 def __init__(self, **kwargs: Any) -> None:
76 kwargs.setdefault("route_class", PrefectAPIRoute)
77 super().__init__(**kwargs)
79 def add_api_route(
80 self, path: str, endpoint: Callable[..., Any], **kwargs: Any
81 ) -> None:
82 """
83 Add an API route.
85 For routes that return content and have not specified a `response_model`,
86 use return type annotation to infer the response model.
88 For routes that return No-Content status codes, explicitly set
89 a `response_class` to ensure nothing is returned in the response body.
90 """
91 if kwargs.get("status_code") == status.HTTP_204_NO_CONTENT:
92 # any routes that return No-Content status codes must
93 # explicitly set a response_class that will handle status codes
94 # and not return anything in the body
95 kwargs["response_class"] = Response
96 if kwargs.get("response_model") is None:
97 kwargs["response_model"] = get_type_hints(endpoint).get("return")
98 return super().add_api_route(path, endpoint, **kwargs)