Coverage for src/lexigram/web/routing/pipeline.py: 14%
49 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""Web request execution pipeline.
3Orchestrates the execution of guards, interceptors, and handlers.
4"""
6from __future__ import annotations
8from typing import TYPE_CHECKING, Any
10from lexigram.web.interceptors.pipeline import InterceptorChain
12if TYPE_CHECKING:
13 from lexigram.contracts.web.guard import GuardProtocol
14 from lexigram.contracts.web.protocols import ExceptionFilterProtocol
15 from lexigram.web.filters.pipeline import FilterPipeline
16 from lexigram.web.protocols import WebInterceptorProtocol
17 from lexigram.web.routing.execution_context import WebExecutionContext
18 from lexigram.web.serialization.serializers import ResponseSerializer
21class RequestPipeline:
22 """Orchestrates the request execution lifecycle."""
24 def __init__(
25 self,
26 guards: list[GuardProtocol] | None = None,
27 interceptors: list[WebInterceptorProtocol] | None = None,
28 filters: list[ExceptionFilterProtocol] | None = None,
29 fallback_pipeline: FilterPipeline | None = None,
30 response_serializer: ResponseSerializer | None = None,
31 ):
32 """Initialize the pipeline.
34 Args:
35 guards: List of guards to execute.
36 interceptors: List of interceptors to execute.
37 filters: List of exception filters to execute.
38 fallback_pipeline: Optional fallback pipeline (e.g. from the Router).
39 response_serializer: Optional pre-wired ResponseSerializer instance.
40 """
41 self.guards = guards or []
42 self.interceptors = interceptors or []
43 self.filters = filters or []
44 self.fallback_pipeline = fallback_pipeline
45 self.response_serializer = response_serializer
47 async def execute(self, context: WebExecutionContext) -> Any:
48 """Execute the pipeline for the given context.
50 Args:
51 context: The execution context.
53 Returns:
54 The result of the handler (possibly transformed).
55 """
56 try:
57 # 1. Execute Guards
58 for guard in self.guards:
59 if not await guard.can_activate(context):
60 from lexigram.web.exceptions import ForbiddenError
62 raise ForbiddenError("Access denied by guard")
64 # 2. Execute Interceptors & Handler
65 # We wrap the final handler call in a CallHandlerProtocol to be used by interceptors
66 async def final_handler() -> Any:
67 from lexigram.web.routing.parameter_binder import ParameterBinder
69 # Resolve parameters using binder
70 sig_info = context.route_metadata.get("sig_info")
71 if not sig_info:
72 # Fallback to computing on the fly (less efficient)
73 import inspect
75 from lexigram.web.routing.validation import (
76 _cached_get_type_hints_for_handler,
77 )
79 sig = inspect.signature(context.handler)
80 hints = _cached_get_type_hints_for_handler(context.handler)
81 sig_info = {"sig": sig, "hints": hints}
83 binder = ParameterBinder(sig_info["sig"], sig_info["hints"])
84 kwargs = await binder.bind(context)
86 return await context.handler(**kwargs)
88 # Build interceptor chain
89 chain = InterceptorChain(self.interceptors, 0, final_handler, context)
90 result = await chain.handle()
92 # 3. Post-execution: Serialize result to Response if needed
93 from starlette.responses import Response as StarletteResponse
95 if isinstance(result, StarletteResponse):
96 return result
98 from lexigram.web.serialization.serializers import ResponseSerializer
100 route = context.route_metadata.get("route")
101 serializer = self.response_serializer or ResponseSerializer()
103 return await serializer.serialize(
104 result,
105 context.request,
106 route=route,
107 )
109 except Exception as e: # noqa: BLE001 — pipeline safety net; dispatches any error to registered exception filters
110 # Match and execute appropriate filter
111 # Local filters take precedence
112 for f in reversed(self.filters):
113 if f.can_handle(e):
114 return await f.handle(e, context.request)
116 # Check for fallback pipeline (e.g. from the Router)
117 if self.fallback_pipeline:
118 return await self.fallback_pipeline.handle(e, context.request)
120 # Check for truly global filters if no fallback handled it
121 from lexigram.web.filters.pipeline import get_filter_pipeline
123 global_pipeline = get_filter_pipeline(context.container)
124 if global_pipeline and global_pipeline is not self.fallback_pipeline:
125 return await global_pipeline.handle(e, context.request)
127 # Re-raise if no filter found
128 raise
131__all__ = ["RequestPipeline"]